fix: fix popover position, closes #441 (#442)

Co-authored-by: Innei <tukon479@gmail.com>
This commit is contained in:
Grogu 2023-04-28 02:46:12 +08:00 committed by GitHub
parent b56a175dcd
commit c387a631cd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 172 additions and 85 deletions

View File

@ -15,6 +15,8 @@ import {
import { CharacterCard } from "~/components/common/CharacterCard"
import { Portal } from "./Portal"
export const CharacterFloatCard: React.FC<{
siteId?: string
children: JSX.Element
@ -27,6 +29,7 @@ export const CharacterFloatCard: React.FC<{
onOpenChange: setOpen,
middleware: [offset(5), flip(), shift({ padding: 8 })],
whileElementsMounted: autoUpdate,
strategy: "fixed",
})
const { getReferenceProps, getFloatingProps } = useInteractions([
@ -51,25 +54,27 @@ export const CharacterFloatCard: React.FC<{
{children}
</span>
{isMounted && (
<span
ref={refs.setFloating}
className={
"z-10 block w-80" + (open || buttonLoading ? "" : " hidden")
}
style={{
position: strategy,
top: y ?? "0",
left: x ?? "0",
...styles,
}}
{...getFloatingProps()}
>
<CharacterCard
siteId={siteId}
open={open}
setButtonLoading={setButtonLoading}
/>
</span>
<Portal>
<span
ref={refs.setFloating}
className={
"z-10 block w-80" + (open || buttonLoading ? "" : " hidden")
}
style={{
position: strategy,
top: y ?? "0",
left: x ?? "0",
...styles,
}}
{...getFloatingProps()}
>
<CharacterCard
siteId={siteId}
open={open}
setButtonLoading={setButtonLoading}
/>
</span>
</Portal>
)}
</>
)

View File

@ -1,11 +1,12 @@
import { useTranslation } from "next-i18next"
import React from "react"
import React, { useRef } from "react"
import { Virtuoso } from "react-virtuoso"
import { Modal } from "~/components/ui/Modal"
import { Button } from "../ui/Button"
import CharacterListItem from "./CharacterListItem"
import { PortalProvider } from "./Portal"
export const CharacterList: React.FC<{
open: boolean
@ -23,26 +24,40 @@ export const CharacterList: React.FC<{
[] as any[],
) as any[]
const $modalEl = useRef<HTMLDivElement>(null)
return (
<Modal open={open} setOpen={setOpen} title={title} zIndex={20}>
<Modal
open={open}
setOpen={setOpen}
title={title}
zIndex={20}
ref={$modalEl}
>
{list?.length ? (
<Virtuoso
overscan={10}
style={{ height: flattenList.length * 64 }}
fixedItemHeight={64}
className="max-h-screen"
endReached={() => hasMore && loadMore()}
components={{
Footer: hasMore ? Loader : undefined,
}}
data={flattenList}
itemContent={(index, sub) => {
const character = sub?.character || sub?.fromCharacter
return (
<CharacterListItem key={index} sub={sub} character={character} />
)
}}
></Virtuoso>
<PortalProvider to={$modalEl.current!}>
<Virtuoso
overscan={10}
style={{ height: flattenList.length * 64 }}
fixedItemHeight={64}
className="max-h-screen"
endReached={() => hasMore && loadMore()}
components={{
Footer: hasMore ? Loader : undefined,
}}
data={flattenList}
itemContent={(index, sub) => {
const character = sub?.character || sub?.fromCharacter
return (
<CharacterListItem
key={index}
sub={sub}
character={character}
/>
)
}}
></Virtuoso>
</PortalProvider>
) : (
<div className="px-5 overflow-auto flex-1">
<div className="py-3 text-center text-zinc-300">

View File

@ -0,0 +1,48 @@
import { PropsWithChildren, createContext, useContext, useMemo } from "react"
import { createPortal } from "react-dom"
import { useIsClient } from "~/hooks/useClient"
import { isServerSide } from "~/lib/utils"
export const usePortal = () => {
const ctx = useContext(RootPortalContext)
if (isServerSide()) {
return null
}
return ctx.to || document.body
}
const RootPortalContext = createContext<{
to?: HTMLElement | undefined
}>({
to: undefined,
})
export const PortalProvider = (
props: PropsWithChildren<{
to: HTMLElement
}>,
) => (
<RootPortalContext.Provider
value={useMemo(
() => ({
to: props.to,
}),
[props.to],
)}
>
{props.children}
</RootPortalContext.Provider>
)
export const Portal = (
props: PropsWithChildren<{
to?: HTMLElement
}>,
) => {
const client = useIsClient()
const to = usePortal()
if (!client) return null
return createPortal(props.children, props.to || to || document.body)
}

View File

@ -1,11 +1,11 @@
import clsx from "clsx"
import React from "react"
import React, { forwardRef } from "react"
import { Dialog } from "@headlessui/react"
import { cn } from "~/lib/utils"
export const Modal: React.FC<{
interface ModalProps {
open: boolean
setOpen: (open: boolean) => void
children: React.ReactNode
@ -13,49 +13,58 @@ export const Modal: React.FC<{
titleIcon?: React.ReactNode
size?: "md" | "lg" | "sm"
zIndex?: number
}> = ({ open, setOpen, children, title, titleIcon, size = "md", zIndex }) => {
return (
<Dialog
open={open}
onClose={() => setOpen(false)}
className={clsx("relative", zIndex ? `z-${zIndex}` : "z-50")}
>
{/* The backdrop, rendered as a fixed sibling to the panel container */}
<div className="fixed inset-0 bg-black/30 z-40" aria-hidden={true} />
{/* Full-screen container to center the panel */}
<div
className={cn(
`fixed inset-0 flex items-center justify-center p-4 z-40`,
)}
>
{/* The actual dialog panel */}
<Dialog.Panel
className={cn(
`mx-auto rounded-lg bg-white w-full shadow-modal max-h-full overflow-y-auto flex flex-col`,
size === "md"
? `max-w-md`
: size === "lg"
? `max-w-lg`
: `max-w-sm`,
)}
>
{title && (
<Dialog.Title className="text-lg border-b h-14 flex items-center px-5 space-x-2 py-4 relative">
{titleIcon && <span>{titleIcon}</span>}
<span className="truncate flex items-center w-full">{title}</span>
<span
className="absolute right-1 w-7 h-7 text-xl cursor-pointer bg-white flex items-center justify-center"
onClick={() => setOpen(false)}
>
<i className="i-mingcute:close-line inline-block" />
</span>
</Dialog.Title>
)}
{children}
</Dialog.Panel>
</div>
</Dialog>
)
}
export const Modal = forwardRef<HTMLDivElement, ModalProps>(
({ open, setOpen, children, title, titleIcon, size = "md", zIndex }, ref) => {
return (
<Dialog
open={open}
onClose={() => setOpen(false)}
className={clsx("relative", zIndex ? `z-${zIndex}` : "z-50")}
>
{/* The backdrop, rendered as a fixed sibling to the panel container */}
<div className="fixed inset-0 bg-black/30 z-40" aria-hidden={true} />
{/* Full-screen container to center the panel */}
<div
className={cn(
`fixed inset-0 flex items-center justify-center p-4 z-40`,
)}
ref={ref}
>
{/* The actual dialog panel */}
<Dialog.Panel
className={cn(
`mx-auto rounded-lg bg-white w-full shadow-modal max-h-full overflow-y-auto flex flex-col`,
size === "md"
? `max-w-md`
: size === "lg"
? `max-w-lg`
: `max-w-sm`,
)}
>
{title && (
<Dialog.Title className="text-lg border-b h-14 flex items-center px-5 space-x-2 py-4 relative">
{titleIcon && <span>{titleIcon}</span>}
<span className="truncate flex items-center w-full">
{title}
</span>
<span
className="absolute right-1 w-7 h-7 text-xl cursor-pointer bg-white flex items-center justify-center"
onClick={() => setOpen(false)}
>
<i className="i-mingcute:close-line inline-block" />
</span>
</Dialog.Title>
)}
{children}
</Dialog.Panel>
</div>
</Dialog>
)
},
)
Modal.displayName = "Modal"

10
src/hooks/useClient.ts Normal file
View File

@ -0,0 +1,10 @@
import { useEffect, useState } from "react"
export const useIsClient = () => {
const [isClient, setIsClient] = useState(false)
useEffect(() => {
setIsClient(true)
}, [])
return isClient
}