diff --git a/apps/renderer/src/components/ui/context-menu/context-menu.tsx b/apps/renderer/src/components/ui/context-menu/context-menu.tsx index 8134de6d5..d20647f3c 100644 --- a/apps/renderer/src/components/ui/context-menu/context-menu.tsx +++ b/apps/renderer/src/components/ui/context-menu/context-menu.tsx @@ -63,7 +63,7 @@ const ContextMenuContent = React.forwardRef< @@ -18,7 +19,12 @@ const defaultCtxValue: CurrentModalContentProps = { ref: { current: null }, } -export const CurrentModalContext = createContext(defaultCtxValue) +export const CurrentModalContext = reactCreateContext(defaultCtxValue) +export const CurrentModalStateContext = createContextSelector<{ + isActive: boolean +}>({ + isActive: false, +}) export type ModalContentComponent = FC export type ModalActionsInternal = { diff --git a/apps/renderer/src/components/ui/modal/stacked/internal/use-animate.ts b/apps/renderer/src/components/ui/modal/stacked/internal/use-animate.ts new file mode 100644 index 000000000..bc9d67dd4 --- /dev/null +++ b/apps/renderer/src/components/ui/modal/stacked/internal/use-animate.ts @@ -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, + } +} diff --git a/apps/renderer/src/components/ui/modal/stacked/internal/use-drag.ts b/apps/renderer/src/components/ui/modal/stacked/internal/use-drag.ts new file mode 100644 index 000000000..e34b87752 --- /dev/null +++ b/apps/renderer/src/components/ui/modal/stacked/internal/use-drag.ts @@ -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, + { + 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 = useCallback( + (e) => { + if (draggable) { + dragController.start(e) + } + }, + [dragController, draggable], + ) + + return { + handleDrag, + handleResizeStart, + handleResizeStop, + relocateModal, + preferDragDir, + isResizeable, + resizeableStyle, + + dragController, + } +} diff --git a/apps/renderer/src/components/ui/modal/stacked/internal/use-select.ts b/apps/renderer/src/components/ui/modal/stacked/internal/use-select.ts new file mode 100644 index 000000000..9f4a7cae7 --- /dev/null +++ b/apps/renderer/src/components/ui/modal/stacked/internal/use-select.ts @@ -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, + } +} diff --git a/apps/renderer/src/components/ui/modal/stacked/modal.tsx b/apps/renderer/src/components/ui/modal/stacked/modal.tsx index be334d63c..f81e749e1 100644 --- a/apps/renderer/src/components/ui/modal/stacked/modal.tsx +++ b/apps/renderer/src/components/ui/modal/stacked/modal.tsx @@ -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(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 = 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(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 = (