Skip to content

Commit

Permalink
refactor: modal structure, add z-index 1001, fixed RSSNext#896
Browse files Browse the repository at this point in the history
Signed-off-by: Innei <tukon479@gmail.com>
  • Loading branch information
Innei committed Oct 15, 2024
1 parent e5e53b8 commit 74abfd3
Show file tree
Hide file tree
Showing 9 changed files with 206 additions and 102 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)}
Expand Down
3 changes: 2 additions & 1 deletion apps/renderer/src/components/ui/modal/stacked/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
10 changes: 8 additions & 2 deletions apps/renderer/src/components/ui/modal/stacked/context.tsx
Original file line number Diff line number Diff line change
@@ -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>
Expand All @@ -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 = {
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
}
}
53 changes: 53 additions & 0 deletions apps/renderer/src/components/ui/modal/stacked/internal/use-drag.ts
Original file line number Diff line number Diff line change
@@ -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,
}
}
Original file line number Diff line number Diff line change
@@ -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,
}
}
118 changes: 37 additions & 81 deletions apps/renderer/src/components/ui/modal/stacked/modal.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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"

Expand All @@ -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 {
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand All @@ -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) {
Expand All @@ -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}
Expand Down Expand Up @@ -340,6 +283,9 @@ export const ModalInternal = memo(
onFocus={stopPropagation}
onPointerUp={handleDetectSelectEnd}
onClick={handleClickOutsideToDismiss}
style={{
zIndex: MODAL_STACK_Z_INDEX + index,
}}
>
{DragBar}

Expand Down Expand Up @@ -418,3 +364,13 @@ export const ModalInternal = memo(
)
}),
)

const useShortcutScope = () => {
const switchHotkeyScope = useSwitchHotKeyScope()
useEffect(() => {
switchHotkeyScope("Modal")
return () => {
switchHotkeyScope("Home")
}
}, [switchHotkeyScope])
}
Loading

0 comments on commit 74abfd3

Please sign in to comment.