refactor: modal structure, add z-index 1001, fixed #896
Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
parent
e5e53b8ef8
commit
74abfd3907
|
|
@ -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,
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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])
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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={() => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue