refactor: modal structure, add z-index 1001, fixed #896

Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
Innei 2024-10-15 20:44:16 +08:00
parent e5e53b8ef8
commit 74abfd3907
No known key found for this signature in database
GPG Key ID: 0F62D33977F021F7
9 changed files with 206 additions and 102 deletions

View File

@ -63,7 +63,7 @@ const ContextMenuContent = React.forwardRef<
<ContextMenuPrimitive.Content
ref={ref}
className={cn(
"z-[999] min-w-32 overflow-hidden rounded-md border border-border bg-theme-modal-background-opaque p-1 text-theme-foreground/90 shadow-lg dark:shadow-zinc-800/60",
"z-[60] min-w-32 overflow-hidden rounded-md border border-border bg-theme-modal-background-opaque p-1 text-theme-foreground/90 shadow-lg dark:shadow-zinc-800/60",
"text-xs",
className,
)}

View File

@ -18,4 +18,5 @@ export const modalMontionConfig = {
transition: microReboundPreset,
}
export const MODAL_STACK_Z_INDEX = 40
// Radix context menu z-index 999
export const MODAL_STACK_Z_INDEX = 1001

View File

@ -1,5 +1,6 @@
import type { FC, RefObject } from "react"
import { createContext } from "react"
import { createContext as reactCreateContext } from "react"
import { createContext as createContextSelector } from "use-context-selector"
export type CurrentModalContentProps = ModalActionsInternal & {
ref: RefObject<HTMLElement | null>
@ -18,7 +19,12 @@ const defaultCtxValue: CurrentModalContentProps = {
ref: { current: null },
}
export const CurrentModalContext = createContext<CurrentModalContentProps>(defaultCtxValue)
export const CurrentModalContext = reactCreateContext<CurrentModalContentProps>(defaultCtxValue)
export const CurrentModalStateContext = createContextSelector<{
isActive: boolean
}>({
isActive: false,
})
export type ModalContentComponent<T = object> = FC<ModalActionsInternal & T>
export type ModalActionsInternal = {

View File

@ -0,0 +1,56 @@
import { useAnimationControls } from "framer-motion"
import { useCallback, useEffect } from "react"
import { nextFrame } from "~/lib/dom"
import { modalMontionConfig } from "../constants"
/**
* @internal
*/
export const useModalAnimate = (isTop: boolean) => {
const animateController = useAnimationControls()
useEffect(() => {
nextFrame(() => {
animateController.start(modalMontionConfig.animate)
})
}, [animateController])
const noticeModal = useCallback(() => {
animateController
.start({
scale: 1.05,
transition: {
duration: 0.06,
},
})
.then(() => {
animateController.start({
scale: 1,
})
})
}, [animateController])
useEffect(() => {
if (isTop) return
animateController.start({
scale: 0.96,
y: 10,
})
return () => {
try {
animateController.stop()
animateController.start({
scale: 1,
y: 0,
})
} catch {
/* empty */
}
}
}, [isTop])
return {
noticeModal,
animateController,
}
}

View File

@ -0,0 +1,53 @@
import { useDragControls } from "framer-motion"
import type { PointerEventHandler, RefObject } from "react"
import { useCallback } from "react"
import { useResizeableModal } from "../hooks"
/**
* @internal
*/
export const useModalResizeAndDrag = (
modalElementRef: RefObject<HTMLDivElement>,
{
resizeable,
draggable,
}: {
resizeable: boolean
draggable: boolean
},
) => {
const dragController = useDragControls()
const {
handleResizeStop,
handleResizeStart,
relocateModal,
preferDragDir,
isResizeable,
resizeableStyle,
} = useResizeableModal(modalElementRef, {
enableResizeable: resizeable,
dragControls: dragController,
})
const handleDrag: PointerEventHandler<HTMLDivElement> = useCallback(
(e) => {
if (draggable) {
dragController.start(e)
}
},
[dragController, draggable],
)
return {
handleDrag,
handleResizeStart,
handleResizeStop,
relocateModal,
preferDragDir,
isResizeable,
resizeableStyle,
dragController,
}
}

View File

@ -0,0 +1,28 @@
import { useCallback, useRef } from "react"
import { nextFrame } from "~/lib/dom"
/**
* @internal
*
* Handle select text in modal
*/
export const useModalSelect = () => {
const isSelectingRef = useRef(false)
const handleSelectStart = useCallback(() => {
isSelectingRef.current = true
}, [])
const handleDetectSelectEnd = useCallback(() => {
nextFrame(() => {
if (isSelectingRef.current) {
isSelectingRef.current = false
}
})
}, [])
return {
isSelectingRef,
handleSelectStart,
handleDetectSelectEnd,
}
}

View File

@ -1,10 +1,9 @@
import * as Dialog from "@radix-ui/react-dialog"
import type { BoundingBox } from "framer-motion"
import { useAnimationControls, useDragControls } from "framer-motion"
import { produce } from "immer"
import { useSetAtom } from "jotai"
import { Resizable } from "re-resizable"
import type { PointerEventHandler, PropsWithChildren, SyntheticEvent } from "react"
import type { PropsWithChildren, SyntheticEvent } from "react"
import {
createElement,
forwardRef,
@ -33,10 +32,12 @@ import { Divider } from "../../divider"
import { RootPortalProvider } from "../../portal/provider"
import { EllipsisHorizontalTextWithTooltip } from "../../typography"
import { modalStackAtom } from "./atom"
import { modalMontionConfig } from "./constants"
import { MODAL_STACK_Z_INDEX, modalMontionConfig } from "./constants"
import type { CurrentModalContentProps, ModalActionsInternal } from "./context"
import { CurrentModalContext } from "./context"
import { useResizeableModal } from "./hooks"
import { useModalAnimate } from "./internal/use-animate"
import { useModalResizeAndDrag } from "./internal/use-drag"
import { useModalSelect } from "./internal/use-select"
import { ModalOverlay } from "./overlay"
import type { ModalOverlayOptions, ModalProps } from "./types"
@ -56,7 +57,7 @@ export const ModalInternal = memo(
onClose?: (open: boolean) => void
} & PropsWithChildren
>(function Modal(
{ item, overlayOptions, onClose: onPropsClose, children, isTop, isBottom },
{ item, overlayOptions, onClose: onPropsClose, children, isTop, index, isBottom },
ref,
) {
const {
@ -113,67 +114,22 @@ export const ModalInternal = memo(
)
const modalElementRef = useRef<HTMLDivElement>(null)
const dragController = useDragControls()
const {
handleResizeStop,
handleDrag,
handleResizeStart,
handleResizeStop,
relocateModal,
preferDragDir,
isResizeable,
resizeableStyle,
} = useResizeableModal(modalElementRef, {
enableResizeable: resizeable,
dragControls: dragController,
dragController,
} = useModalResizeAndDrag(modalElementRef, {
resizeable,
draggable,
})
const animateController = useAnimationControls()
useEffect(() => {
nextFrame(() => {
animateController.start(modalMontionConfig.animate)
})
}, [animateController])
const noticeModal = useCallback(() => {
animateController
.start({
scale: 1.05,
transition: {
duration: 0.06,
},
})
.then(() => {
animateController.start({
scale: 1,
})
})
}, [animateController])
const handleDrag: PointerEventHandler<HTMLDivElement> = useCallback(
(e) => {
if (draggable) {
dragController.start(e)
}
},
[dragController, draggable],
)
useEffect(() => {
if (isTop) return
animateController.start({
scale: 0.96,
y: 10,
})
return () => {
try {
animateController.stop()
animateController.start({
scale: 1,
y: 0,
})
} catch {
/* empty */
}
}
}, [isTop])
const { noticeModal, animateController } = useModalAnimate(!!isTop)
const modalContentRef = useRef<HTMLDivElement>(null)
const ModalProps: ModalActionsInternal = useMemo(
@ -221,34 +177,17 @@ export const ModalInternal = memo(
}
}, [currentIsClosing])
const switchHotkeyScope = useSwitchHotKeyScope()
useEffect(() => {
switchHotkeyScope("Modal")
return () => {
switchHotkeyScope("Home")
}
}, [switchHotkeyScope])
useShortcutScope()
const modalStyle = resizeableStyle
const isSelectingRef = useRef(false)
const handleSelectStart = useCallback(() => {
isSelectingRef.current = true
}, [])
const handleDetectSelectEnd = useCallback(() => {
nextFrame(() => {
if (isSelectingRef.current) {
isSelectingRef.current = false
}
})
}, [])
const { handleSelectStart, handleDetectSelectEnd, isSelectingRef } = useModalSelect()
const handleClickOutsideToDismiss = useCallback(
(e: SyntheticEvent) => {
if (isSelectingRef.current) return
const fn = modal ? (clickOutsideToDismiss && canClose ? dismiss : noticeModal) : undefined
fn?.(e)
},
[canClose, clickOutsideToDismiss, dismiss, modal, noticeModal],
[canClose, clickOutsideToDismiss, dismiss, modal, noticeModal, isSelectingRef],
)
const openAutoFocus = useCallback(
@ -274,10 +213,11 @@ export const ModalInternal = memo(
const Overlay = (
<ModalOverlay
zIndex={MODAL_STACK_Z_INDEX - 1}
blur={overlayOptions?.blur}
className={cn(overlayOptions?.className, {
invisible: item.overlay ? false : !(modalSettingOverlay && isBottom),
})}
hidden={
item.overlay ? currentIsClosing : !(modalSettingOverlay && isBottom) || currentIsClosing
}
/>
)
if (CustomModalComponent) {
@ -296,6 +236,9 @@ export const ModalInternal = memo(
currentIsClosing ? "!pointer-events-none" : "!pointer-events-auto",
modalContainerClassName,
)}
style={{
zIndex: MODAL_STACK_Z_INDEX + index,
}}
onPointerUp={handleDetectSelectEnd}
onClick={handleClickOutsideToDismiss}
onFocus={stopPropagation}
@ -340,6 +283,9 @@ export const ModalInternal = memo(
onFocus={stopPropagation}
onPointerUp={handleDetectSelectEnd}
onClick={handleClickOutsideToDismiss}
style={{
zIndex: MODAL_STACK_Z_INDEX + index,
}}
>
{DragBar}
@ -418,3 +364,13 @@ export const ModalInternal = memo(
)
}),
)
const useShortcutScope = () => {
const switchHotkeyScope = useSwitchHotKeyScope()
useEffect(() => {
switchHotkeyScope("Modal")
return () => {
switchHotkeyScope("Home")
}
}, [switchHotkeyScope])
}

View File

@ -1,41 +1,45 @@
import * as Dialog from "@radix-ui/react-dialog"
import { AnimatePresence } from "framer-motion"
import type { ForwardedRef } from "react"
import { forwardRef } from "react"
import { m } from "~/components/common/Motion"
import { cn } from "~/lib/utils"
import { softSpringPreset } from "../../constants/spring"
export const ModalOverlay = forwardRef(
(
{
zIndex,
blur,
className,
hidden,
}: {
zIndex?: number
blur?: boolean
className?: string
hidden?: boolean
},
ref: ForwardedRef<HTMLDivElement>,
) => (
<Dialog.Overlay asChild>
<m.div
ref={ref}
id="modal-overlay"
className={cn(
// NOTE: pointer-events-none is required, if remove this, when modal is closing, you can not click element behind the modal
"!pointer-events-none fixed inset-0 rounded-[var(--fo-window-radius)] bg-zinc-50/80 dark:bg-neutral-900/80",
blur && "backdrop-blur-sm",
className,
<AnimatePresence>
{!hidden && (
<m.div
ref={ref}
id="modal-overlay"
className={cn(
// NOTE: pointer-events-none is required, if remove this, when modal is closing, you can not click element behind the modal
"!pointer-events-none fixed inset-0 rounded-[var(--fo-window-radius)] bg-zinc-50/80 dark:bg-neutral-900/80",
blur && "backdrop-blur-sm",
className,
)}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
style={{ zIndex }}
/>
)}
transition={softSpringPreset}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
style={{ zIndex }}
/>
</AnimatePresence>
</Dialog.Overlay>
),
)

View File

@ -182,7 +182,7 @@ const TransitionAvatar = forwardRef<
className={cn(
"pointer-events-none fixed -bottom-6 p-0 duration-200 [&_*]:border-0",
"transform-gpu will-change-[left,top,height]",
zoomIn ? "z-[999] h-14" : "z-[-1] h-6",
zoomIn ? "z-[51] h-14" : "z-[-1] h-6",
)}
hideName
onTransitionEnd={() => {