@umodeapporg/ui 0.5.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/Form/ControlledDialogPrimitives.js +4 -1
- package/dist/components/Form/ControlledDialogPrimitives.js.map +1 -1
- package/dist/components/Form/ControlledDrawer.js +4 -1
- package/dist/components/Form/ControlledDrawer.js.map +1 -1
- package/dist/components/UI/ControlledMenu.js +4 -1
- package/dist/components/UI/ControlledMenu.js.map +1 -1
- package/dist/components/UI/ControlledPopover.js +4 -1
- package/dist/components/UI/ControlledPopover.js.map +1 -1
- package/dist/components/UI/ControlledTooltip.js +4 -2
- package/dist/components/UI/ControlledTooltip.js.map +1 -1
- package/dist/components/UI/DataTable/DataTableBulkEditDialog.d.ts +3 -3
- package/dist/components/UI/DataTable/DataTableBulkEditDialog.js +179 -150
- package/dist/components/UI/DataTable/DataTableBulkEditDialog.js.map +1 -1
- package/dist/components/UI/DataTable/DataTableBulkEditRowsPicker.d.ts +13 -0
- package/dist/components/UI/DataTable/DataTableBulkEditRowsPicker.js +94 -0
- package/dist/components/UI/DataTable/DataTableBulkEditRowsPicker.js.map +1 -0
- package/dist/components/UI/DataTable/DataTableBulkEditValueEditor.d.ts +16 -0
- package/dist/components/UI/DataTable/DataTableBulkEditValueEditor.js +79 -0
- package/dist/components/UI/DataTable/DataTableBulkEditValueEditor.js.map +1 -0
- package/dist/components/UI/DataTable/bulkEditValues.d.ts +8 -0
- package/dist/components/UI/DataTable/bulkEditValues.js +13 -0
- package/dist/components/UI/DataTable/bulkEditValues.js.map +1 -0
- package/dist/components/UI/ImageZoomOverlay.js +4 -1
- package/dist/components/UI/ImageZoomOverlay.js.map +1 -1
- package/dist/components/UI/Kanban/KanbanBoard.js +4 -2
- package/dist/components/UI/Kanban/KanbanBoard.js.map +1 -1
- package/dist/helpers/portalHost.d.ts +10 -0
- package/dist/helpers/portalHost.js +7 -0
- package/dist/helpers/portalHost.js.map +1 -0
- package/dist/locales/en/ui.json.d.ts +13 -1
- package/dist/locales/en/ui.json.js +1 -1
- package/dist/locales/index.d.ts +24 -0
- package/dist/locales/pt/ui.json.d.ts +13 -1
- package/dist/locales/pt/ui.json.js +1 -1
- package/package.json +1 -1
|
@@ -4,6 +4,7 @@ import classNames from "classnames";
|
|
|
4
4
|
import { AnimatePresence, motion } from "motion/react";
|
|
5
5
|
import { createPortal } from "react-dom";
|
|
6
6
|
import { twMerge } from "tailwind-merge";
|
|
7
|
+
import { getPortalHost } from "../../helpers/portalHost.js";
|
|
7
8
|
import { scrimVariants, dialogVariants } from "../../motion/motionPresets.js";
|
|
8
9
|
import "../../motion/motionTokens.js";
|
|
9
10
|
const SIZE_CLASS = {
|
|
@@ -56,6 +57,8 @@ const ControlledDialogRoot = ({
|
|
|
56
57
|
document.addEventListener("keydown", handleKeyDown);
|
|
57
58
|
return () => document.removeEventListener("keydown", handleKeyDown);
|
|
58
59
|
}, [open, dismiss, handler]);
|
|
60
|
+
const portalHost = getPortalHost();
|
|
61
|
+
if (portalHost === null) return null;
|
|
59
62
|
return createPortal(
|
|
60
63
|
/* @__PURE__ */ jsx(AnimatePresence, { onExitComplete: () => setIsLeaving(false), children: open && /* @__PURE__ */ jsxs(motion.div, { className: "fixed inset-0 z-[9999] flex items-center justify-center", children: [
|
|
61
64
|
/* @__PURE__ */ jsx(
|
|
@@ -97,7 +100,7 @@ const ControlledDialogRoot = ({
|
|
|
97
100
|
}
|
|
98
101
|
)
|
|
99
102
|
] }, "dialog-layer") }),
|
|
100
|
-
|
|
103
|
+
portalHost
|
|
101
104
|
);
|
|
102
105
|
};
|
|
103
106
|
const ControlledDialogHeader = ({ children, className }) => /* @__PURE__ */ jsx(
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ControlledDialogPrimitives.js","sources":["../../../src/components/Form/ControlledDialogPrimitives.tsx"],"sourcesContent":["import React, { useEffect, useRef, useState } from 'react';\nimport classNames from 'classnames';\nimport { AnimatePresence, motion } from 'motion/react';\nimport { createPortal } from 'react-dom';\nimport { twMerge } from 'tailwind-merge';\n\nimport { dialogVariants, scrimVariants } from '@/motion';\n\nexport type TDialogSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl';\n\nexport interface IDialogDismissConfig {\n enabled?: boolean;\n escapeKey?: boolean;\n outsidePress?: boolean;\n}\n\nconst SIZE_CLASS: Record<TDialogSize, string> = {\n xs: 'w-full md:w-3/5 lg:w-2/5 2xl:w-1/4 min-w-[80%] md:min-w-[60%] lg:min-w-[40%] 2xl:min-w-[25%] max-w-[80%] md:max-w-[60%] lg:max-w-[40%] 2xl:max-w-[25%]',\n sm: 'w-full md:w-2/3 lg:w-2/4 2xl:w-1/3 min-w-[80%] md:min-w-[66.666667%] lg:min-w-[50%] 2xl:min-w-[33.333333%] max-w-[80%] md:max-w-[66.666667%] lg:max-w-[50%] 2xl:max-w-[33.333333%]',\n md: 'w-full md:w-3/4 lg:w-3/5 2xl:w-2/5 min-w-[90%] md:min-w-[75%] lg:min-w-[60%] 2xl:min-w-[40%] max-w-[90%] md:max-w-[75%] lg:max-w-[60%] 2xl:max-w-[40%]',\n lg: 'w-full md:w-5/6 lg:w-3/4 2xl:w-3/5 min-w-[90%] md:min-w-[83.333333%] lg:min-w-[75%] 2xl:min-w-[60%] max-w-[90%] md:max-w-[83.333333%] lg:max-w-[75%] 2xl:max-w-[60%]',\n xl: 'w-full md:w-5/6 2xl:w-3/4 min-w-[95%] md:min-w-[83.333333%] 2xl:min-w-[75%] max-w-[95%] md:max-w-[83.333333%] 2xl:max-w-[75%]',\n xxl: 'w-screen min-w-[100vw] max-w-[100vw]',\n};\n\ntype TDivPropsWithoutMotionConflicts = Omit<\n React.HTMLAttributes<HTMLDivElement>,\n | 'className'\n | 'onScroll'\n | 'onDrag'\n | 'onDragStart'\n | 'onDragEnd'\n | 'onAnimationStart'\n | 'onAnimationEnd'\n | 'onAnimationIteration'\n>;\n\ninterface IControlledDialogRootProps extends TDivPropsWithoutMotionConflicts {\n open: boolean;\n handler?: () => void;\n size?: TDialogSize;\n dismiss?: IDialogDismissConfig;\n className?: string;\n overlayClassName?: string;\n children: React.ReactNode;\n}\n\n/**\n * Portals only while open (plus its exit transition), so a closed dialog inside a mapped\n * list renders nothing at all.\n */\nexport const ControlledDialogRoot: React.FC<IControlledDialogRootProps> = ({\n open,\n handler,\n size = 'md',\n dismiss,\n className,\n overlayClassName,\n children,\n ...rest\n}) => {\n const panelRef = useRef<HTMLDivElement>(null);\n const [isLeaving, setIsLeaving] = useState(false);\n const previouslyFocusedRef = useRef<HTMLElement | null>(null);\n\n useEffect(() => {\n if (open) setIsLeaving(true);\n }, [open]);\n\n /* Held until the exit finishes, not until `open` flips: releasing it early brings the\n page scrollbar back while the panel is still leaving, and the content behind jumps. */\n useEffect(() => {\n if (!open && !isLeaving) return undefined;\n\n const { overflow } = document.body.style;\n document.body.style.overflow = 'hidden';\n return () => {\n document.body.style.overflow = overflow;\n };\n }, [open, isLeaving]);\n\n useEffect(() => {\n if (!open) return undefined;\n\n previouslyFocusedRef.current = document.activeElement as HTMLElement | null;\n const frame = requestAnimationFrame(() => panelRef.current?.focus());\n\n return () => {\n cancelAnimationFrame(frame);\n previouslyFocusedRef.current?.focus?.();\n };\n }, [open]);\n\n useEffect(() => {\n if (!open || dismiss?.enabled === false || dismiss?.escapeKey === false) return undefined;\n\n const handleKeyDown = (event: KeyboardEvent) => {\n if (event.key !== 'Escape') return;\n handler?.();\n };\n\n document.addEventListener('keydown', handleKeyDown);\n return () => document.removeEventListener('keydown', handleKeyDown);\n }, [open, dismiss, handler]);\n\n return createPortal(\n <AnimatePresence onExitComplete={() => setIsLeaving(false)}>\n {open && (\n <motion.div key=\"dialog-layer\" className=\"fixed inset-0 z-[9999] flex items-center justify-center\">\n <motion.div\n variants={scrimVariants}\n initial=\"hidden\"\n animate=\"visible\"\n exit=\"exit\"\n aria-hidden\n onClick={() => {\n if (dismiss?.enabled === false || !dismiss?.outsidePress) return;\n handler?.();\n }}\n className={classNames('absolute inset-0 glass-scrim', overlayClassName)}\n />\n\n <motion.div\n ref={panelRef}\n variants={dialogVariants}\n initial=\"hidden\"\n animate=\"visible\"\n exit=\"exit\"\n role=\"dialog\"\n aria-modal={true}\n tabIndex={-1}\n {...rest}\n className={twMerge(\n classNames(\n 'relative m-4 flex max-h-[90vh] flex-col rounded-lg bg-surface text-base font-light leading-relaxed',\n 'text-foreground-muted shadow-xl outline-none',\n SIZE_CLASS[size],\n ),\n className,\n )}\n >\n {children}\n </motion.div>\n </motion.div>\n )}\n </AnimatePresence>,\n
|
|
1
|
+
{"version":3,"file":"ControlledDialogPrimitives.js","sources":["../../../src/components/Form/ControlledDialogPrimitives.tsx"],"sourcesContent":["import React, { useEffect, useRef, useState } from 'react';\nimport classNames from 'classnames';\nimport { AnimatePresence, motion } from 'motion/react';\nimport { createPortal } from 'react-dom';\nimport { twMerge } from 'tailwind-merge';\n\nimport { getPortalHost } from '@/helpers/portalHost';\nimport { dialogVariants, scrimVariants } from '@/motion';\n\nexport type TDialogSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl';\n\nexport interface IDialogDismissConfig {\n enabled?: boolean;\n escapeKey?: boolean;\n outsidePress?: boolean;\n}\n\nconst SIZE_CLASS: Record<TDialogSize, string> = {\n xs: 'w-full md:w-3/5 lg:w-2/5 2xl:w-1/4 min-w-[80%] md:min-w-[60%] lg:min-w-[40%] 2xl:min-w-[25%] max-w-[80%] md:max-w-[60%] lg:max-w-[40%] 2xl:max-w-[25%]',\n sm: 'w-full md:w-2/3 lg:w-2/4 2xl:w-1/3 min-w-[80%] md:min-w-[66.666667%] lg:min-w-[50%] 2xl:min-w-[33.333333%] max-w-[80%] md:max-w-[66.666667%] lg:max-w-[50%] 2xl:max-w-[33.333333%]',\n md: 'w-full md:w-3/4 lg:w-3/5 2xl:w-2/5 min-w-[90%] md:min-w-[75%] lg:min-w-[60%] 2xl:min-w-[40%] max-w-[90%] md:max-w-[75%] lg:max-w-[60%] 2xl:max-w-[40%]',\n lg: 'w-full md:w-5/6 lg:w-3/4 2xl:w-3/5 min-w-[90%] md:min-w-[83.333333%] lg:min-w-[75%] 2xl:min-w-[60%] max-w-[90%] md:max-w-[83.333333%] lg:max-w-[75%] 2xl:max-w-[60%]',\n xl: 'w-full md:w-5/6 2xl:w-3/4 min-w-[95%] md:min-w-[83.333333%] 2xl:min-w-[75%] max-w-[95%] md:max-w-[83.333333%] 2xl:max-w-[75%]',\n xxl: 'w-screen min-w-[100vw] max-w-[100vw]',\n};\n\ntype TDivPropsWithoutMotionConflicts = Omit<\n React.HTMLAttributes<HTMLDivElement>,\n | 'className'\n | 'onScroll'\n | 'onDrag'\n | 'onDragStart'\n | 'onDragEnd'\n | 'onAnimationStart'\n | 'onAnimationEnd'\n | 'onAnimationIteration'\n>;\n\ninterface IControlledDialogRootProps extends TDivPropsWithoutMotionConflicts {\n open: boolean;\n handler?: () => void;\n size?: TDialogSize;\n dismiss?: IDialogDismissConfig;\n className?: string;\n overlayClassName?: string;\n children: React.ReactNode;\n}\n\n/**\n * Portals only while open (plus its exit transition), so a closed dialog inside a mapped\n * list renders nothing at all.\n */\nexport const ControlledDialogRoot: React.FC<IControlledDialogRootProps> = ({\n open,\n handler,\n size = 'md',\n dismiss,\n className,\n overlayClassName,\n children,\n ...rest\n}) => {\n const panelRef = useRef<HTMLDivElement>(null);\n const [isLeaving, setIsLeaving] = useState(false);\n const previouslyFocusedRef = useRef<HTMLElement | null>(null);\n\n useEffect(() => {\n if (open) setIsLeaving(true);\n }, [open]);\n\n /* Held until the exit finishes, not until `open` flips: releasing it early brings the\n page scrollbar back while the panel is still leaving, and the content behind jumps. */\n useEffect(() => {\n if (!open && !isLeaving) return undefined;\n\n const { overflow } = document.body.style;\n document.body.style.overflow = 'hidden';\n return () => {\n document.body.style.overflow = overflow;\n };\n }, [open, isLeaving]);\n\n useEffect(() => {\n if (!open) return undefined;\n\n previouslyFocusedRef.current = document.activeElement as HTMLElement | null;\n const frame = requestAnimationFrame(() => panelRef.current?.focus());\n\n return () => {\n cancelAnimationFrame(frame);\n previouslyFocusedRef.current?.focus?.();\n };\n }, [open]);\n\n useEffect(() => {\n if (!open || dismiss?.enabled === false || dismiss?.escapeKey === false) return undefined;\n\n const handleKeyDown = (event: KeyboardEvent) => {\n if (event.key !== 'Escape') return;\n handler?.();\n };\n\n document.addEventListener('keydown', handleKeyDown);\n return () => document.removeEventListener('keydown', handleKeyDown);\n }, [open, dismiss, handler]);\n\n const portalHost = getPortalHost();\n if (portalHost === null) return null;\n\n return createPortal(\n <AnimatePresence onExitComplete={() => setIsLeaving(false)}>\n {open && (\n <motion.div key=\"dialog-layer\" className=\"fixed inset-0 z-[9999] flex items-center justify-center\">\n <motion.div\n variants={scrimVariants}\n initial=\"hidden\"\n animate=\"visible\"\n exit=\"exit\"\n aria-hidden\n onClick={() => {\n if (dismiss?.enabled === false || !dismiss?.outsidePress) return;\n handler?.();\n }}\n className={classNames('absolute inset-0 glass-scrim', overlayClassName)}\n />\n\n <motion.div\n ref={panelRef}\n variants={dialogVariants}\n initial=\"hidden\"\n animate=\"visible\"\n exit=\"exit\"\n role=\"dialog\"\n aria-modal={true}\n tabIndex={-1}\n {...rest}\n className={twMerge(\n classNames(\n 'relative m-4 flex max-h-[90vh] flex-col rounded-lg bg-surface text-base font-light leading-relaxed',\n 'text-foreground-muted shadow-xl outline-none',\n SIZE_CLASS[size],\n ),\n className,\n )}\n >\n {children}\n </motion.div>\n </motion.div>\n )}\n </AnimatePresence>,\n portalHost,\n );\n};\n\ninterface IDialogSectionProps {\n children: React.ReactNode;\n className?: string;\n}\n\nexport const ControlledDialogHeader: React.FC<IDialogSectionProps> = ({ children, className }) => (\n <div\n className={twMerge(\n 'flex shrink-0 items-center p-4 text-2xl font-semibold leading-snug text-foreground antialiased',\n className,\n )}\n >\n {children}\n </div>\n);\n\nexport const ControlledDialogBody: React.FC<IDialogSectionProps> = ({ children, className }) => (\n <div className={twMerge('relative min-h-0 flex-1 p-4 text-base font-light leading-relaxed antialiased', className)}>\n {children}\n </div>\n);\n\nexport const ControlledDialogFooter: React.FC<IDialogSectionProps> = ({ children, className }) => (\n <div className={twMerge('flex shrink-0 flex-wrap items-center justify-end p-4 text-foreground-muted', className)}>\n {children}\n </div>\n);\n"],"names":[],"mappings":"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"}
|
|
@@ -7,6 +7,7 @@ import { useTranslation } from "react-i18next";
|
|
|
7
7
|
import ControlledButton from "./ControlledButton.js";
|
|
8
8
|
import ControlledTooltip from "../UI/ControlledTooltip.js";
|
|
9
9
|
import { isPressOnLayeredSurface } from "../../helpers/layeredSurfaces.js";
|
|
10
|
+
import { getPortalHost } from "../../helpers/portalHost.js";
|
|
10
11
|
import { createDrawerVariants, scrimVariants } from "../../motion/motionPresets.js";
|
|
11
12
|
import "../../motion/motionTokens.js";
|
|
12
13
|
const DEFAULT_SIZE = 300;
|
|
@@ -153,6 +154,8 @@ const ControlledDrawer = ({
|
|
|
153
154
|
const resolvedSize = isHorizontalPlacement ? resizedWidth ?? size : size;
|
|
154
155
|
const shouldRenderChildren = unmountOnClose ? isOpen : !lazy || hasEverOpened;
|
|
155
156
|
const drawerVariants = createDrawerVariants(placement);
|
|
157
|
+
const portalHost = getPortalHost();
|
|
158
|
+
if (portalHost === null) return null;
|
|
156
159
|
return createPortal(
|
|
157
160
|
/* @__PURE__ */ jsxs(AnimatePresence, { onExitComplete: () => setIsLeaving(false), children: [
|
|
158
161
|
isOpen && /* @__PURE__ */ jsx(
|
|
@@ -228,7 +231,7 @@ const ControlledDrawer = ({
|
|
|
228
231
|
"drawer-panel"
|
|
229
232
|
)
|
|
230
233
|
] }),
|
|
231
|
-
|
|
234
|
+
portalHost
|
|
232
235
|
);
|
|
233
236
|
};
|
|
234
237
|
export {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ControlledDrawer.js","sources":["../../../src/components/Form/ControlledDrawer.tsx"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react';\nimport * as React from 'react';\nimport classNames from 'classnames';\nimport { AnimatePresence, motion } from 'motion/react';\nimport { createPortal } from 'react-dom';\nimport { useTranslation } from 'react-i18next';\n\nimport ControlledButton from '@/components/Form/ControlledButton';\nimport ControlledTooltip from '@/components/UI/ControlledTooltip';\nimport { isPressOnLayeredSurface } from '@/helpers/layeredSurfaces';\nimport { createDrawerVariants, scrimVariants } from '@/motion';\n\nconst DEFAULT_SIZE = 300;\nconst DEFAULT_MIN_RESIZE_WIDTH = 280;\nconst DEFAULT_MAX_RESIZE_VIEWPORT_RATIO = 0.8;\nconst KEYBOARD_RESIZE_STEP = 24;\n\nexport type TDrawerPlacement = 'left' | 'right' | 'top' | 'bottom';\n\nexport interface IDrawerDismissConfig {\n enabled?: boolean;\n escapeKey?: boolean;\n outsidePress?: boolean | ((event: PointerEvent) => boolean);\n}\n\nexport interface IControlledDrawerProps {\n open?: boolean;\n onClose?: () => void;\n placement?: TDrawerPlacement;\n /** Width for left/right placements, height for top/bottom, in pixels. */\n size?: number;\n className?: string;\n overlayClassName?: string;\n children?: React.ReactNode;\n dismiss?: IDrawerDismissConfig;\n\n /** Defer mounting the subtree until the first time `open` becomes true. */\n lazy?: boolean;\n /** Discard the subtree while closed, so its state resets on the next open. */\n unmountOnClose?: boolean;\n /** Fires once, the first time the drawer opens after this component mounted. */\n onFirstOpen?: () => void;\n /** Fires whenever `open` toggles. Argument is the new value. */\n onOpenChange?: (open: boolean) => void;\n\n resizable?: boolean;\n minResizeWidth?: number;\n maxResizeViewportRatio?: number;\n}\n\nconst PLACEMENT_CLASS: Record<TDrawerPlacement, string> = {\n left: 'inset-y-0 left-0',\n right: 'inset-y-0 right-0',\n top: 'inset-x-0 top-0',\n bottom: 'inset-x-0 bottom-0',\n};\n\n/**\n * The panel is removed from the DOM once its exit transition finishes. The Material\n * Tailwind drawer kept a closed panel mounted off-screen, and its shadow bled back into\n * the viewport: with one drawer that is invisible, with one per list row the bleeds\n * stacked into visible gray lines at the screen edge.\n */\nconst ControlledDrawer: React.FC<IControlledDrawerProps> = ({\n open,\n onClose,\n placement = 'left',\n size = DEFAULT_SIZE,\n className,\n overlayClassName,\n children,\n dismiss,\n lazy,\n unmountOnClose,\n onFirstOpen,\n onOpenChange,\n resizable = true,\n minResizeWidth = DEFAULT_MIN_RESIZE_WIDTH,\n maxResizeViewportRatio = DEFAULT_MAX_RESIZE_VIEWPORT_RATIO,\n}) => {\n const { t } = useTranslation('ui');\n\n const isOpen = Boolean(open);\n const panelRef = useRef<HTMLDivElement>(null);\n const previouslyFocusedRef = useRef<HTMLElement | null>(null);\n\n const [hasEverOpened, setHasEverOpened] = useState(isOpen);\n const [isLeaving, setIsLeaving] = useState(false);\n const prevOpenRef = useRef<boolean | undefined>(open);\n const firstOpenFiredRef = useRef(false);\n\n const isHorizontalPlacement = placement === 'left' || placement === 'right';\n const canResize = resizable && isHorizontalPlacement;\n\n const [resizedWidth, setResizedWidth] = useState<number | null>(null);\n const [isResizing, setIsResizing] = useState(false);\n const resizeStartRef = useRef<{ pointerX: number; width: number } | null>(null);\n const previousBaseSizeRef = useRef(size);\n\n const clampWidth = useCallback(\n (width: number) => {\n const maxWidth = Math.max(minResizeWidth, Math.round(window.innerWidth * maxResizeViewportRatio));\n return Math.min(maxWidth, Math.max(minResizeWidth, Math.round(width)));\n },\n [minResizeWidth, maxResizeViewportRatio],\n );\n\n useEffect(() => {\n if (previousBaseSizeRef.current === size) return;\n previousBaseSizeRef.current = size;\n setResizedWidth(null);\n }, [size]);\n\n useEffect(() => {\n if (!canResize) return undefined;\n\n const handleViewportResize = () => setResizedWidth((current) => (current === null ? null : clampWidth(current)));\n\n window.addEventListener('resize', handleViewportResize);\n return () => window.removeEventListener('resize', handleViewportResize);\n }, [canResize, clampWidth]);\n\n useEffect(() => {\n const previous = prevOpenRef.current;\n if (previous === isOpen) return;\n prevOpenRef.current = isOpen;\n\n if (isOpen && !hasEverOpened) setHasEverOpened(true);\n\n if (isOpen && !firstOpenFiredRef.current) {\n firstOpenFiredRef.current = true;\n onFirstOpen?.();\n }\n\n onOpenChange?.(isOpen);\n }, [isOpen, hasEverOpened, onFirstOpen, onOpenChange]);\n\n useEffect(() => {\n if (isOpen) setIsLeaving(true);\n }, [isOpen]);\n\n /* Held until the exit finishes, not until `open` flips: releasing it early brings the\n page scrollbar back while the panel is still travelling, and the content behind jumps. */\n useEffect(() => {\n if (!isOpen && !isLeaving) return undefined;\n\n const { overflow } = document.body.style;\n document.body.style.overflow = 'hidden';\n return () => {\n document.body.style.overflow = overflow;\n };\n }, [isOpen, isLeaving]);\n\n useEffect(() => {\n if (!isOpen) return undefined;\n\n previouslyFocusedRef.current = document.activeElement as HTMLElement | null;\n const frame = requestAnimationFrame(() => panelRef.current?.focus());\n\n return () => {\n cancelAnimationFrame(frame);\n previouslyFocusedRef.current?.focus?.();\n };\n }, [isOpen]);\n\n useEffect(() => {\n if (!isOpen || dismiss?.enabled === false) return undefined;\n\n const handleKeyDown = (event: KeyboardEvent) => {\n if (event.key !== 'Escape' || dismiss?.escapeKey === false) return;\n onClose?.();\n };\n\n const handlePointerDown = (event: PointerEvent) => {\n if (dismiss?.outsidePress === false) return;\n const panel = panelRef.current;\n const target = event.target as Node | null;\n if (!panel || !target || panel.contains(target)) return;\n if (isPressOnLayeredSurface(target, panelRef.current)) return;\n if (typeof dismiss?.outsidePress === 'function' && !dismiss.outsidePress(event)) return;\n onClose?.();\n };\n\n document.addEventListener('keydown', handleKeyDown);\n document.addEventListener('pointerdown', handlePointerDown, true);\n\n return () => {\n document.removeEventListener('keydown', handleKeyDown);\n document.removeEventListener('pointerdown', handlePointerDown, true);\n };\n }, [isOpen, dismiss, onClose]);\n\n const handleResizeStart = (event: React.PointerEvent<HTMLButtonElement>) => {\n event.preventDefault();\n event.stopPropagation();\n resizeStartRef.current = { pointerX: event.clientX, width: resizedWidth ?? size };\n setIsResizing(true);\n event.currentTarget.setPointerCapture(event.pointerId);\n };\n\n const handleResizeMove = (event: React.PointerEvent<HTMLButtonElement>) => {\n const resizeStart = resizeStartRef.current;\n if (!resizeStart) return;\n const pointerDelta =\n placement === 'right' ? resizeStart.pointerX - event.clientX : event.clientX - resizeStart.pointerX;\n setResizedWidth(clampWidth(resizeStart.width + pointerDelta));\n };\n\n const handleResizeEnd = (event: React.PointerEvent<HTMLButtonElement>) => {\n if (!resizeStartRef.current) return;\n resizeStartRef.current = null;\n setIsResizing(false);\n if (event.currentTarget.hasPointerCapture(event.pointerId)) {\n event.currentTarget.releasePointerCapture(event.pointerId);\n }\n };\n\n const handleResizeKeyDown = (event: React.KeyboardEvent<HTMLButtonElement>) => {\n if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') return;\n event.preventDefault();\n const pointerDirection = event.key === 'ArrowLeft' ? -1 : 1;\n const widthDirection = placement === 'right' ? -pointerDirection : pointerDirection;\n setResizedWidth((current) => clampWidth((current ?? size) + widthDirection * KEYBOARD_RESIZE_STEP));\n };\n\n if (lazy && !hasEverOpened) return null;\n\n const resolvedSize = isHorizontalPlacement ? (resizedWidth ?? size) : size;\n const shouldRenderChildren = unmountOnClose ? isOpen : !lazy || hasEverOpened;\n const drawerVariants = createDrawerVariants(placement);\n\n return createPortal(\n <AnimatePresence onExitComplete={() => setIsLeaving(false)}>\n {isOpen && (\n <motion.div\n key=\"drawer-scrim\"\n variants={scrimVariants}\n initial=\"hidden\"\n animate=\"visible\"\n exit=\"exit\"\n aria-hidden\n onClick={() => {\n if (dismiss?.enabled === false || dismiss?.outsidePress === false) return;\n onClose?.();\n }}\n className={classNames('fixed inset-0 z-[9996] glass-scrim', overlayClassName)}\n />\n )}\n\n {isOpen && (\n <motion.div\n key=\"drawer-panel\"\n ref={panelRef}\n variants={drawerVariants}\n initial=\"hidden\"\n animate=\"visible\"\n exit=\"exit\"\n role=\"dialog\"\n aria-modal={true}\n tabIndex={-1}\n style={isHorizontalPlacement ? { width: resolvedSize } : { height: resolvedSize }}\n className={classNames(\n 'fixed z-[9997] flex max-w-full flex-col overflow-y-auto bg-surface shadow-xl outline-none',\n PLACEMENT_CLASS[placement],\n className,\n )}\n >\n {canResize && isOpen ? (\n <ControlledTooltip\n content={t('controlledDrawer.resizeHandle')}\n placement={placement === 'right' ? 'left' : 'right'}\n className=\"z-[9999]\"\n >\n <ControlledButton\n defaultHtmlButton\n onPointerDown={handleResizeStart}\n onPointerMove={handleResizeMove}\n onPointerUp={handleResizeEnd}\n onPointerCancel={handleResizeEnd}\n onKeyDown={handleResizeKeyDown}\n onDoubleClick={() => setResizedWidth(null)}\n className={classNames(\n 'group/resize absolute inset-y-0 z-10 flex w-3 cursor-col-resize touch-none select-none items-center justify-center focus:outline-none',\n placement === 'right' ? 'left-0' : 'right-0',\n )}\n >\n <span\n className={classNames(\n 'h-full w-0.5 rounded-full transition-colors duration-base',\n isResizing ? 'bg-brand-hover' : 'bg-transparent group-hover/resize:bg-brand-hover',\n )}\n />\n </ControlledButton>\n </ControlledTooltip>\n ) : null}\n\n {shouldRenderChildren ? children : null}\n </motion.div>\n )}\n </AnimatePresence>,\n document.body,\n );\n};\n\nexport default ControlledDrawer;\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ControlledDrawer.js","sources":["../../../src/components/Form/ControlledDrawer.tsx"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react';\nimport * as React from 'react';\nimport classNames from 'classnames';\nimport { AnimatePresence, motion } from 'motion/react';\nimport { createPortal } from 'react-dom';\nimport { useTranslation } from 'react-i18next';\n\nimport ControlledButton from '@/components/Form/ControlledButton';\nimport ControlledTooltip from '@/components/UI/ControlledTooltip';\nimport { isPressOnLayeredSurface } from '@/helpers/layeredSurfaces';\nimport { getPortalHost } from '@/helpers/portalHost';\nimport { createDrawerVariants, scrimVariants } from '@/motion';\n\nconst DEFAULT_SIZE = 300;\nconst DEFAULT_MIN_RESIZE_WIDTH = 280;\nconst DEFAULT_MAX_RESIZE_VIEWPORT_RATIO = 0.8;\nconst KEYBOARD_RESIZE_STEP = 24;\n\nexport type TDrawerPlacement = 'left' | 'right' | 'top' | 'bottom';\n\nexport interface IDrawerDismissConfig {\n enabled?: boolean;\n escapeKey?: boolean;\n outsidePress?: boolean | ((event: PointerEvent) => boolean);\n}\n\nexport interface IControlledDrawerProps {\n open?: boolean;\n onClose?: () => void;\n placement?: TDrawerPlacement;\n /** Width for left/right placements, height for top/bottom, in pixels. */\n size?: number;\n className?: string;\n overlayClassName?: string;\n children?: React.ReactNode;\n dismiss?: IDrawerDismissConfig;\n\n /** Defer mounting the subtree until the first time `open` becomes true. */\n lazy?: boolean;\n /** Discard the subtree while closed, so its state resets on the next open. */\n unmountOnClose?: boolean;\n /** Fires once, the first time the drawer opens after this component mounted. */\n onFirstOpen?: () => void;\n /** Fires whenever `open` toggles. Argument is the new value. */\n onOpenChange?: (open: boolean) => void;\n\n resizable?: boolean;\n minResizeWidth?: number;\n maxResizeViewportRatio?: number;\n}\n\nconst PLACEMENT_CLASS: Record<TDrawerPlacement, string> = {\n left: 'inset-y-0 left-0',\n right: 'inset-y-0 right-0',\n top: 'inset-x-0 top-0',\n bottom: 'inset-x-0 bottom-0',\n};\n\n/**\n * The panel is removed from the DOM once its exit transition finishes. The Material\n * Tailwind drawer kept a closed panel mounted off-screen, and its shadow bled back into\n * the viewport: with one drawer that is invisible, with one per list row the bleeds\n * stacked into visible gray lines at the screen edge.\n */\nconst ControlledDrawer: React.FC<IControlledDrawerProps> = ({\n open,\n onClose,\n placement = 'left',\n size = DEFAULT_SIZE,\n className,\n overlayClassName,\n children,\n dismiss,\n lazy,\n unmountOnClose,\n onFirstOpen,\n onOpenChange,\n resizable = true,\n minResizeWidth = DEFAULT_MIN_RESIZE_WIDTH,\n maxResizeViewportRatio = DEFAULT_MAX_RESIZE_VIEWPORT_RATIO,\n}) => {\n const { t } = useTranslation('ui');\n\n const isOpen = Boolean(open);\n const panelRef = useRef<HTMLDivElement>(null);\n const previouslyFocusedRef = useRef<HTMLElement | null>(null);\n\n const [hasEverOpened, setHasEverOpened] = useState(isOpen);\n const [isLeaving, setIsLeaving] = useState(false);\n const prevOpenRef = useRef<boolean | undefined>(open);\n const firstOpenFiredRef = useRef(false);\n\n const isHorizontalPlacement = placement === 'left' || placement === 'right';\n const canResize = resizable && isHorizontalPlacement;\n\n const [resizedWidth, setResizedWidth] = useState<number | null>(null);\n const [isResizing, setIsResizing] = useState(false);\n const resizeStartRef = useRef<{ pointerX: number; width: number } | null>(null);\n const previousBaseSizeRef = useRef(size);\n\n const clampWidth = useCallback(\n (width: number) => {\n const maxWidth = Math.max(minResizeWidth, Math.round(window.innerWidth * maxResizeViewportRatio));\n return Math.min(maxWidth, Math.max(minResizeWidth, Math.round(width)));\n },\n [minResizeWidth, maxResizeViewportRatio],\n );\n\n useEffect(() => {\n if (previousBaseSizeRef.current === size) return;\n previousBaseSizeRef.current = size;\n setResizedWidth(null);\n }, [size]);\n\n useEffect(() => {\n if (!canResize) return undefined;\n\n const handleViewportResize = () => setResizedWidth((current) => (current === null ? null : clampWidth(current)));\n\n window.addEventListener('resize', handleViewportResize);\n return () => window.removeEventListener('resize', handleViewportResize);\n }, [canResize, clampWidth]);\n\n useEffect(() => {\n const previous = prevOpenRef.current;\n if (previous === isOpen) return;\n prevOpenRef.current = isOpen;\n\n if (isOpen && !hasEverOpened) setHasEverOpened(true);\n\n if (isOpen && !firstOpenFiredRef.current) {\n firstOpenFiredRef.current = true;\n onFirstOpen?.();\n }\n\n onOpenChange?.(isOpen);\n }, [isOpen, hasEverOpened, onFirstOpen, onOpenChange]);\n\n useEffect(() => {\n if (isOpen) setIsLeaving(true);\n }, [isOpen]);\n\n /* Held until the exit finishes, not until `open` flips: releasing it early brings the\n page scrollbar back while the panel is still travelling, and the content behind jumps. */\n useEffect(() => {\n if (!isOpen && !isLeaving) return undefined;\n\n const { overflow } = document.body.style;\n document.body.style.overflow = 'hidden';\n return () => {\n document.body.style.overflow = overflow;\n };\n }, [isOpen, isLeaving]);\n\n useEffect(() => {\n if (!isOpen) return undefined;\n\n previouslyFocusedRef.current = document.activeElement as HTMLElement | null;\n const frame = requestAnimationFrame(() => panelRef.current?.focus());\n\n return () => {\n cancelAnimationFrame(frame);\n previouslyFocusedRef.current?.focus?.();\n };\n }, [isOpen]);\n\n useEffect(() => {\n if (!isOpen || dismiss?.enabled === false) return undefined;\n\n const handleKeyDown = (event: KeyboardEvent) => {\n if (event.key !== 'Escape' || dismiss?.escapeKey === false) return;\n onClose?.();\n };\n\n const handlePointerDown = (event: PointerEvent) => {\n if (dismiss?.outsidePress === false) return;\n const panel = panelRef.current;\n const target = event.target as Node | null;\n if (!panel || !target || panel.contains(target)) return;\n if (isPressOnLayeredSurface(target, panelRef.current)) return;\n if (typeof dismiss?.outsidePress === 'function' && !dismiss.outsidePress(event)) return;\n onClose?.();\n };\n\n document.addEventListener('keydown', handleKeyDown);\n document.addEventListener('pointerdown', handlePointerDown, true);\n\n return () => {\n document.removeEventListener('keydown', handleKeyDown);\n document.removeEventListener('pointerdown', handlePointerDown, true);\n };\n }, [isOpen, dismiss, onClose]);\n\n const handleResizeStart = (event: React.PointerEvent<HTMLButtonElement>) => {\n event.preventDefault();\n event.stopPropagation();\n resizeStartRef.current = { pointerX: event.clientX, width: resizedWidth ?? size };\n setIsResizing(true);\n event.currentTarget.setPointerCapture(event.pointerId);\n };\n\n const handleResizeMove = (event: React.PointerEvent<HTMLButtonElement>) => {\n const resizeStart = resizeStartRef.current;\n if (!resizeStart) return;\n const pointerDelta =\n placement === 'right' ? resizeStart.pointerX - event.clientX : event.clientX - resizeStart.pointerX;\n setResizedWidth(clampWidth(resizeStart.width + pointerDelta));\n };\n\n const handleResizeEnd = (event: React.PointerEvent<HTMLButtonElement>) => {\n if (!resizeStartRef.current) return;\n resizeStartRef.current = null;\n setIsResizing(false);\n if (event.currentTarget.hasPointerCapture(event.pointerId)) {\n event.currentTarget.releasePointerCapture(event.pointerId);\n }\n };\n\n const handleResizeKeyDown = (event: React.KeyboardEvent<HTMLButtonElement>) => {\n if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') return;\n event.preventDefault();\n const pointerDirection = event.key === 'ArrowLeft' ? -1 : 1;\n const widthDirection = placement === 'right' ? -pointerDirection : pointerDirection;\n setResizedWidth((current) => clampWidth((current ?? size) + widthDirection * KEYBOARD_RESIZE_STEP));\n };\n\n if (lazy && !hasEverOpened) return null;\n\n const resolvedSize = isHorizontalPlacement ? (resizedWidth ?? size) : size;\n const shouldRenderChildren = unmountOnClose ? isOpen : !lazy || hasEverOpened;\n const drawerVariants = createDrawerVariants(placement);\n\n const portalHost = getPortalHost();\n if (portalHost === null) return null;\n\n return createPortal(\n <AnimatePresence onExitComplete={() => setIsLeaving(false)}>\n {isOpen && (\n <motion.div\n key=\"drawer-scrim\"\n variants={scrimVariants}\n initial=\"hidden\"\n animate=\"visible\"\n exit=\"exit\"\n aria-hidden\n onClick={() => {\n if (dismiss?.enabled === false || dismiss?.outsidePress === false) return;\n onClose?.();\n }}\n className={classNames('fixed inset-0 z-[9996] glass-scrim', overlayClassName)}\n />\n )}\n\n {isOpen && (\n <motion.div\n key=\"drawer-panel\"\n ref={panelRef}\n variants={drawerVariants}\n initial=\"hidden\"\n animate=\"visible\"\n exit=\"exit\"\n role=\"dialog\"\n aria-modal={true}\n tabIndex={-1}\n style={isHorizontalPlacement ? { width: resolvedSize } : { height: resolvedSize }}\n className={classNames(\n 'fixed z-[9997] flex max-w-full flex-col overflow-y-auto bg-surface shadow-xl outline-none',\n PLACEMENT_CLASS[placement],\n className,\n )}\n >\n {canResize && isOpen ? (\n <ControlledTooltip\n content={t('controlledDrawer.resizeHandle')}\n placement={placement === 'right' ? 'left' : 'right'}\n className=\"z-[9999]\"\n >\n <ControlledButton\n defaultHtmlButton\n onPointerDown={handleResizeStart}\n onPointerMove={handleResizeMove}\n onPointerUp={handleResizeEnd}\n onPointerCancel={handleResizeEnd}\n onKeyDown={handleResizeKeyDown}\n onDoubleClick={() => setResizedWidth(null)}\n className={classNames(\n 'group/resize absolute inset-y-0 z-10 flex w-3 cursor-col-resize touch-none select-none items-center justify-center focus:outline-none',\n placement === 'right' ? 'left-0' : 'right-0',\n )}\n >\n <span\n className={classNames(\n 'h-full w-0.5 rounded-full transition-colors duration-base',\n isResizing ? 'bg-brand-hover' : 'bg-transparent group-hover/resize:bg-brand-hover',\n )}\n />\n </ControlledButton>\n </ControlledTooltip>\n ) : null}\n\n {shouldRenderChildren ? children : null}\n </motion.div>\n )}\n </AnimatePresence>,\n portalHost,\n );\n};\n\nexport default ControlledDrawer;\n"],"names":[],"mappings":"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"}
|
|
@@ -8,6 +8,7 @@ import ControlledTooltip from "./ControlledTooltip.js";
|
|
|
8
8
|
import { MaterialSymbol } from "./MaterialSymbol/index.js";
|
|
9
9
|
import { composeRefs } from "../../helpers/composeRefs.js";
|
|
10
10
|
import { isPressOnLayeredSurface } from "../../helpers/layeredSurfaces.js";
|
|
11
|
+
import { getPortalHost } from "../../helpers/portalHost.js";
|
|
11
12
|
import { useAnchoredFloating } from "../../hooks/useAnchoredFloating.js";
|
|
12
13
|
import { floatingVariants } from "../../motion/motionPresets.js";
|
|
13
14
|
import "../../motion/motionTokens.js";
|
|
@@ -105,6 +106,8 @@ const ControlledMenuList = React.forwardRef(
|
|
|
105
106
|
const nextIndex = currentIndex < 0 ? 0 : (currentIndex + step + items.length) % items.length;
|
|
106
107
|
items[nextIndex]?.focus();
|
|
107
108
|
};
|
|
109
|
+
const portalHost = getPortalHost();
|
|
110
|
+
if (portalHost === null) return null;
|
|
108
111
|
return createPortal(
|
|
109
112
|
/* @__PURE__ */ jsx(AnimatePresence, { children: open && /* @__PURE__ */ jsx(
|
|
110
113
|
motion.div,
|
|
@@ -136,7 +139,7 @@ const ControlledMenuList = React.forwardRef(
|
|
|
136
139
|
},
|
|
137
140
|
"menu-list"
|
|
138
141
|
) }),
|
|
139
|
-
|
|
142
|
+
portalHost
|
|
140
143
|
);
|
|
141
144
|
}
|
|
142
145
|
);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ControlledMenu.js","sources":["../../../src/components/UI/ControlledMenu.tsx"],"sourcesContent":["import React, {\n cloneElement,\n createContext,\n isValidElement,\n useCallback,\n useContext,\n useEffect,\n useId,\n useMemo,\n useState,\n} from 'react';\nimport classNames from 'classnames';\nimport { AnimatePresence, motion } from 'motion/react';\nimport { createPortal } from 'react-dom';\nimport { twMerge } from 'tailwind-merge';\n\nimport ControlledTooltip from '@/components/UI/ControlledTooltip';\nimport { MaterialSymbol, SymbolCodepoints } from '@/components/UI/MaterialSymbol';\nimport { composeRefs } from '@/helpers/composeRefs';\nimport { isPressOnLayeredSurface } from '@/helpers/layeredSurfaces';\nimport { TFloatingPlacement, useAnchoredFloating } from '@/hooks/useAnchoredFloating';\nimport { floatingVariants } from '@/motion';\n\nexport type TMenuPlacement = TFloatingPlacement;\nexport type TMenuItemTone = 'default' | 'danger';\n\nexport interface IMenuDismissConfig {\n /** Closes the menu when an item is pressed. Multi-select surfaces pass false. */\n itemPress?: boolean;\n escapeKey?: boolean;\n outsidePress?: boolean;\n}\n\nexport interface IControlledMenuItem {\n key: string;\n label: string;\n icon?: SymbolCodepoints;\n hint?: string;\n onClick?: () => void;\n disabled?: boolean;\n tone?: TMenuItemTone;\n}\n\ninterface IMenuContext {\n open: boolean;\n setOpen: (open: boolean) => void;\n dismiss?: IMenuDismissConfig;\n listId: string;\n setReference: (node: HTMLElement | null) => void;\n setFloating: (node: HTMLElement | null) => void;\n floatingRef: React.MutableRefObject<HTMLElement | null>;\n referenceRef: React.MutableRefObject<HTMLElement | null>;\n}\n\nconst MenuContext = createContext<IMenuContext | null>(null);\n\nconst useMenuContext = (): IMenuContext => {\n const context = useContext(MenuContext);\n if (!context) throw new Error('Menu parts must be rendered inside ControlledMenuRoot');\n return context;\n};\n\ninterface IControlledMenuRootProps {\n children: React.ReactNode;\n open?: boolean;\n handler?: (open: boolean) => void;\n placement?: TMenuPlacement;\n offsetPx?: number;\n dismiss?: IMenuDismissConfig;\n matchTriggerWidth?: boolean;\n}\n\nexport const ControlledMenuRoot: React.FC<IControlledMenuRootProps> = ({\n children,\n open,\n handler,\n placement = 'bottom-start',\n offsetPx = 6,\n dismiss,\n matchTriggerWidth = false,\n}) => {\n const listId = useId();\n const [uncontrolledOpen, setUncontrolledOpen] = useState(false);\n\n const isControlled = open !== undefined;\n const isOpen = isControlled ? Boolean(open) : uncontrolledOpen;\n\n const setOpen = useCallback(\n (next: boolean) => {\n if (!isControlled) setUncontrolledOpen(next);\n handler?.(next);\n },\n [isControlled, handler],\n );\n\n const { setReference, setFloating, floatingRef, referenceRef } = useAnchoredFloating({\n open: isOpen,\n placement,\n offsetPx,\n matchReferenceWidth: matchTriggerWidth,\n });\n\n const value = useMemo(\n () => ({ open: isOpen, setOpen, dismiss, listId, setReference, setFloating, floatingRef, referenceRef }),\n [isOpen, setOpen, dismiss, listId, setReference, setFloating, floatingRef, referenceRef],\n );\n\n return <MenuContext.Provider value={value}>{children}</MenuContext.Provider>;\n};\n\ninterface IHandlerChildProps {\n onClick?: (event: React.MouseEvent<HTMLElement>) => void;\n ref?: React.Ref<HTMLElement>;\n}\n\n/**\n * Repassa ao filho o que ele recebe de fora, além do que injeta.\n *\n * O gatilho é o alvo natural de um `ControlledTooltip`, e o tooltip se prende clonando o filho e\n * injetando `onPointerEnter`. Aceitando só `children`, este componente engolia essa injeção e o\n * tooltip simplesmente nunca abria, mesmo com a ordem que a documentação manda usar\n * (tooltip POR FORA do handler). Os `onClick` são encadeados: o de fora roda antes de abrir.\n */\nexport const ControlledMenuHandler = React.forwardRef<\n HTMLElement,\n { children: React.ReactNode } & React.HTMLAttributes<HTMLElement>\n>(({ children, onClick: onOuterClick, ...rest }, forwardedRef) => {\n const { open, setOpen, setReference, listId } = useMenuContext();\n\n if (!isValidElement(children)) return <>{children}</>;\n\n const child = children as React.ReactElement<IHandlerChildProps>;\n\n return cloneElement(child, {\n ...rest,\n ref: composeRefs<HTMLElement>(setReference, forwardedRef, (child as { ref?: React.Ref<HTMLElement> }).ref),\n 'aria-haspopup': 'menu',\n 'aria-expanded': open,\n 'aria-controls': open ? listId : undefined,\n onClick: (event: React.MouseEvent<HTMLElement>) => {\n onOuterClick?.(event);\n child.props.onClick?.(event);\n setOpen(!open);\n },\n } as IHandlerChildProps);\n});\n\nControlledMenuHandler.displayName = 'ControlledMenuHandler';\n\ninterface IControlledMenuListProps extends Omit<\n React.HTMLAttributes<HTMLDivElement>,\n 'className' | 'onDrag' | 'onDragStart' | 'onDragEnd' | 'onAnimationStart' | 'onAnimationEnd' | 'onAnimationIteration'\n> {\n children: React.ReactNode;\n className?: string;\n}\n\nconst FOCUSABLE_ITEM_SELECTOR = '[role=\"menuitem\"]:not([aria-disabled=\"true\"])';\n\nexport const ControlledMenuList = React.forwardRef<HTMLDivElement, IControlledMenuListProps>(\n ({ children, className, onClick, onKeyDown, ...rest }, forwardedRef) => {\n const { open, setOpen, dismiss, listId, setFloating, floatingRef, referenceRef } = useMenuContext();\n\n useEffect(() => {\n if (!open) return undefined;\n\n const handleKeyDown = (event: KeyboardEvent) => {\n if (event.key !== 'Escape' || dismiss?.escapeKey === false) return;\n setOpen(false);\n };\n\n const handlePointerDown = (event: PointerEvent) => {\n if (dismiss?.outsidePress === false) return;\n const target = event.target as Node | null;\n if (!target) return;\n if (floatingRef.current?.contains(target) || referenceRef.current?.contains(target)) return;\n if (isPressOnLayeredSurface(target, floatingRef.current)) return;\n setOpen(false);\n };\n\n document.addEventListener('keydown', handleKeyDown);\n document.addEventListener('pointerdown', handlePointerDown, true);\n\n return () => {\n document.removeEventListener('keydown', handleKeyDown);\n document.removeEventListener('pointerdown', handlePointerDown, true);\n };\n }, [open, setOpen, dismiss, floatingRef, referenceRef]);\n\n const handleListKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {\n onKeyDown?.(event);\n if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp') return;\n\n const list = floatingRef.current;\n if (!list) return;\n\n const items = Array.from(list.querySelectorAll<HTMLElement>(FOCUSABLE_ITEM_SELECTOR));\n if (items.length === 0) return;\n\n event.preventDefault();\n const currentIndex = items.indexOf(document.activeElement as HTMLElement);\n const step = event.key === 'ArrowDown' ? 1 : -1;\n const nextIndex = currentIndex < 0 ? 0 : (currentIndex + step + items.length) % items.length;\n items[nextIndex]?.focus();\n };\n\n return createPortal(\n <AnimatePresence>\n {open && (\n <motion.div\n key=\"menu-list\"\n id={listId}\n variants={floatingVariants}\n initial=\"hidden\"\n animate=\"visible\"\n exit=\"exit\"\n role=\"menu\"\n ref={(node) => {\n setFloating(node);\n if (typeof forwardedRef === 'function') forwardedRef(node);\n else if (forwardedRef) forwardedRef.current = node;\n }}\n onKeyDown={handleListKeyDown}\n onClick={(event) => {\n onClick?.(event);\n if (dismiss?.itemPress === false) return;\n const target = event.target as HTMLElement | null;\n if (target?.closest('[role=\"menuitem\"]')) setOpen(false);\n }}\n {...rest}\n className={twMerge(\n 'fixed left-0 top-0 z-[9999] min-w-44 overflow-auto rounded-lg border border-border p-1 text-sm text-foreground glass',\n className,\n )}\n >\n {children}\n </motion.div>\n )}\n </AnimatePresence>,\n document.body,\n );\n },\n);\n\nControlledMenuList.displayName = 'ControlledMenuList';\n\ninterface IControlledMenuItemProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'className'> {\n children: React.ReactNode;\n className?: string;\n disabled?: boolean;\n}\n\nexport const ControlledMenuItem: React.FC<IControlledMenuItemProps> = ({\n children,\n className,\n disabled = false,\n onClick,\n ...rest\n}) => (\n <div\n role=\"menuitem\"\n tabIndex={disabled ? -1 : 0}\n aria-disabled={disabled || undefined}\n onClick={disabled ? undefined : onClick}\n onKeyDown={(event) => {\n if (disabled) return;\n if (event.key !== 'Enter' && event.key !== ' ') return;\n event.preventDefault();\n (event.currentTarget as HTMLElement).click();\n }}\n {...rest}\n className={twMerge(\n classNames(\n 'flex w-full cursor-pointer select-none items-center rounded-md px-3 py-2 text-sm outline-none',\n 'transition-colors hover:bg-muted-hover focus:bg-muted-hover',\n { 'pointer-events-none opacity-50': disabled },\n ),\n className,\n )}\n >\n {children}\n </div>\n);\n\ninterface IControlledMenuProps {\n trigger: React.ReactNode;\n items?: Array<IControlledMenuItem>;\n children?: React.ReactNode;\n triggerTooltip?: string;\n placement?: TMenuPlacement;\n open?: boolean;\n onOpenChange?: (open: boolean) => void;\n disabled?: boolean;\n dismissOnItemClick?: boolean;\n listClassName?: string;\n tooltipClassName?: string;\n listRef?: React.Ref<HTMLDivElement>;\n}\n\nconst ITEM_TONE_CLASS: Record<TMenuItemTone, string> = {\n default: 'text-foreground-muted',\n danger: 'text-danger-emphasis',\n};\n\nconst ICON_TONE_CLASS: Record<TMenuItemTone, string> = {\n default: 'text-foreground-subtle',\n danger: 'text-danger-emphasis',\n};\n\n/**\n * A tooltip on the trigger must sit OUTSIDE the handler: the handler clones its child to\n * inject the click handler and the reference ref, and a tooltip in that position swallows\n * both, so the menu stops opening.\n */\nconst ControlledMenu: React.FC<IControlledMenuProps> = ({\n trigger,\n items,\n children,\n triggerTooltip,\n placement = 'bottom-start',\n open,\n onOpenChange,\n disabled = false,\n dismissOnItemClick = true,\n listClassName,\n tooltipClassName,\n listRef,\n}) => {\n const menuItems = useMemo(() => (Array.isArray(items) ? items : []), [items]);\n const dismiss = useMemo(() => ({ itemPress: dismissOnItemClick }), [dismissOnItemClick]);\n const handle = <ControlledMenuHandler>{trigger}</ControlledMenuHandler>;\n\n if (disabled) return <>{trigger}</>;\n\n return (\n <ControlledMenuRoot open={open} handler={onOpenChange} placement={placement} dismiss={dismiss}>\n {triggerTooltip ? (\n <ControlledTooltip content={triggerTooltip} className={tooltipClassName}>\n {handle}\n </ControlledTooltip>\n ) : (\n handle\n )}\n\n <ControlledMenuList ref={listRef} className={listClassName}>\n {children ??\n menuItems.map((item) => {\n const tone = item.tone ?? 'default';\n\n return (\n <ControlledMenuItem\n key={item.key}\n disabled={item.disabled}\n onClick={item.onClick}\n className={classNames('items-start gap-x-2', ITEM_TONE_CLASS[tone])}\n >\n {item.icon && <MaterialSymbol icon={item.icon} size={16} className={ICON_TONE_CLASS[tone]} />}\n\n <span className=\"flex flex-col items-start gap-y-0.5\">\n <span>{item.label}</span>\n {item.hint && <span className=\"text-xs text-foreground-subtle\">{item.hint}</span>}\n </span>\n </ControlledMenuItem>\n );\n })}\n </ControlledMenuList>\n </ControlledMenuRoot>\n );\n};\n\nexport default ControlledMenu;\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ControlledMenu.js","sources":["../../../src/components/UI/ControlledMenu.tsx"],"sourcesContent":["import React, {\n cloneElement,\n createContext,\n isValidElement,\n useCallback,\n useContext,\n useEffect,\n useId,\n useMemo,\n useState,\n} from 'react';\nimport classNames from 'classnames';\nimport { AnimatePresence, motion } from 'motion/react';\nimport { createPortal } from 'react-dom';\nimport { twMerge } from 'tailwind-merge';\n\nimport ControlledTooltip from '@/components/UI/ControlledTooltip';\nimport { MaterialSymbol, SymbolCodepoints } from '@/components/UI/MaterialSymbol';\nimport { composeRefs } from '@/helpers/composeRefs';\nimport { isPressOnLayeredSurface } from '@/helpers/layeredSurfaces';\nimport { getPortalHost } from '@/helpers/portalHost';\nimport { TFloatingPlacement, useAnchoredFloating } from '@/hooks/useAnchoredFloating';\nimport { floatingVariants } from '@/motion';\n\nexport type TMenuPlacement = TFloatingPlacement;\nexport type TMenuItemTone = 'default' | 'danger';\n\nexport interface IMenuDismissConfig {\n /** Closes the menu when an item is pressed. Multi-select surfaces pass false. */\n itemPress?: boolean;\n escapeKey?: boolean;\n outsidePress?: boolean;\n}\n\nexport interface IControlledMenuItem {\n key: string;\n label: string;\n icon?: SymbolCodepoints;\n hint?: string;\n onClick?: () => void;\n disabled?: boolean;\n tone?: TMenuItemTone;\n}\n\ninterface IMenuContext {\n open: boolean;\n setOpen: (open: boolean) => void;\n dismiss?: IMenuDismissConfig;\n listId: string;\n setReference: (node: HTMLElement | null) => void;\n setFloating: (node: HTMLElement | null) => void;\n floatingRef: React.MutableRefObject<HTMLElement | null>;\n referenceRef: React.MutableRefObject<HTMLElement | null>;\n}\n\nconst MenuContext = createContext<IMenuContext | null>(null);\n\nconst useMenuContext = (): IMenuContext => {\n const context = useContext(MenuContext);\n if (!context) throw new Error('Menu parts must be rendered inside ControlledMenuRoot');\n return context;\n};\n\ninterface IControlledMenuRootProps {\n children: React.ReactNode;\n open?: boolean;\n handler?: (open: boolean) => void;\n placement?: TMenuPlacement;\n offsetPx?: number;\n dismiss?: IMenuDismissConfig;\n matchTriggerWidth?: boolean;\n}\n\nexport const ControlledMenuRoot: React.FC<IControlledMenuRootProps> = ({\n children,\n open,\n handler,\n placement = 'bottom-start',\n offsetPx = 6,\n dismiss,\n matchTriggerWidth = false,\n}) => {\n const listId = useId();\n const [uncontrolledOpen, setUncontrolledOpen] = useState(false);\n\n const isControlled = open !== undefined;\n const isOpen = isControlled ? Boolean(open) : uncontrolledOpen;\n\n const setOpen = useCallback(\n (next: boolean) => {\n if (!isControlled) setUncontrolledOpen(next);\n handler?.(next);\n },\n [isControlled, handler],\n );\n\n const { setReference, setFloating, floatingRef, referenceRef } = useAnchoredFloating({\n open: isOpen,\n placement,\n offsetPx,\n matchReferenceWidth: matchTriggerWidth,\n });\n\n const value = useMemo(\n () => ({ open: isOpen, setOpen, dismiss, listId, setReference, setFloating, floatingRef, referenceRef }),\n [isOpen, setOpen, dismiss, listId, setReference, setFloating, floatingRef, referenceRef],\n );\n\n return <MenuContext.Provider value={value}>{children}</MenuContext.Provider>;\n};\n\ninterface IHandlerChildProps {\n onClick?: (event: React.MouseEvent<HTMLElement>) => void;\n ref?: React.Ref<HTMLElement>;\n}\n\n/**\n * Repassa ao filho o que ele recebe de fora, além do que injeta.\n *\n * O gatilho é o alvo natural de um `ControlledTooltip`, e o tooltip se prende clonando o filho e\n * injetando `onPointerEnter`. Aceitando só `children`, este componente engolia essa injeção e o\n * tooltip simplesmente nunca abria, mesmo com a ordem que a documentação manda usar\n * (tooltip POR FORA do handler). Os `onClick` são encadeados: o de fora roda antes de abrir.\n */\nexport const ControlledMenuHandler = React.forwardRef<\n HTMLElement,\n { children: React.ReactNode } & React.HTMLAttributes<HTMLElement>\n>(({ children, onClick: onOuterClick, ...rest }, forwardedRef) => {\n const { open, setOpen, setReference, listId } = useMenuContext();\n\n if (!isValidElement(children)) return <>{children}</>;\n\n const child = children as React.ReactElement<IHandlerChildProps>;\n\n return cloneElement(child, {\n ...rest,\n ref: composeRefs<HTMLElement>(setReference, forwardedRef, (child as { ref?: React.Ref<HTMLElement> }).ref),\n 'aria-haspopup': 'menu',\n 'aria-expanded': open,\n 'aria-controls': open ? listId : undefined,\n onClick: (event: React.MouseEvent<HTMLElement>) => {\n onOuterClick?.(event);\n child.props.onClick?.(event);\n setOpen(!open);\n },\n } as IHandlerChildProps);\n});\n\nControlledMenuHandler.displayName = 'ControlledMenuHandler';\n\ninterface IControlledMenuListProps extends Omit<\n React.HTMLAttributes<HTMLDivElement>,\n 'className' | 'onDrag' | 'onDragStart' | 'onDragEnd' | 'onAnimationStart' | 'onAnimationEnd' | 'onAnimationIteration'\n> {\n children: React.ReactNode;\n className?: string;\n}\n\nconst FOCUSABLE_ITEM_SELECTOR = '[role=\"menuitem\"]:not([aria-disabled=\"true\"])';\n\nexport const ControlledMenuList = React.forwardRef<HTMLDivElement, IControlledMenuListProps>(\n ({ children, className, onClick, onKeyDown, ...rest }, forwardedRef) => {\n const { open, setOpen, dismiss, listId, setFloating, floatingRef, referenceRef } = useMenuContext();\n\n useEffect(() => {\n if (!open) return undefined;\n\n const handleKeyDown = (event: KeyboardEvent) => {\n if (event.key !== 'Escape' || dismiss?.escapeKey === false) return;\n setOpen(false);\n };\n\n const handlePointerDown = (event: PointerEvent) => {\n if (dismiss?.outsidePress === false) return;\n const target = event.target as Node | null;\n if (!target) return;\n if (floatingRef.current?.contains(target) || referenceRef.current?.contains(target)) return;\n if (isPressOnLayeredSurface(target, floatingRef.current)) return;\n setOpen(false);\n };\n\n document.addEventListener('keydown', handleKeyDown);\n document.addEventListener('pointerdown', handlePointerDown, true);\n\n return () => {\n document.removeEventListener('keydown', handleKeyDown);\n document.removeEventListener('pointerdown', handlePointerDown, true);\n };\n }, [open, setOpen, dismiss, floatingRef, referenceRef]);\n\n const handleListKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {\n onKeyDown?.(event);\n if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp') return;\n\n const list = floatingRef.current;\n if (!list) return;\n\n const items = Array.from(list.querySelectorAll<HTMLElement>(FOCUSABLE_ITEM_SELECTOR));\n if (items.length === 0) return;\n\n event.preventDefault();\n const currentIndex = items.indexOf(document.activeElement as HTMLElement);\n const step = event.key === 'ArrowDown' ? 1 : -1;\n const nextIndex = currentIndex < 0 ? 0 : (currentIndex + step + items.length) % items.length;\n items[nextIndex]?.focus();\n };\n\n const portalHost = getPortalHost();\n if (portalHost === null) return null;\n\n return createPortal(\n <AnimatePresence>\n {open && (\n <motion.div\n key=\"menu-list\"\n id={listId}\n variants={floatingVariants}\n initial=\"hidden\"\n animate=\"visible\"\n exit=\"exit\"\n role=\"menu\"\n ref={(node) => {\n setFloating(node);\n if (typeof forwardedRef === 'function') forwardedRef(node);\n else if (forwardedRef) forwardedRef.current = node;\n }}\n onKeyDown={handleListKeyDown}\n onClick={(event) => {\n onClick?.(event);\n if (dismiss?.itemPress === false) return;\n const target = event.target as HTMLElement | null;\n if (target?.closest('[role=\"menuitem\"]')) setOpen(false);\n }}\n {...rest}\n className={twMerge(\n 'fixed left-0 top-0 z-[9999] min-w-44 overflow-auto rounded-lg border border-border p-1 text-sm text-foreground glass',\n className,\n )}\n >\n {children}\n </motion.div>\n )}\n </AnimatePresence>,\n portalHost,\n );\n },\n);\n\nControlledMenuList.displayName = 'ControlledMenuList';\n\ninterface IControlledMenuItemProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'className'> {\n children: React.ReactNode;\n className?: string;\n disabled?: boolean;\n}\n\nexport const ControlledMenuItem: React.FC<IControlledMenuItemProps> = ({\n children,\n className,\n disabled = false,\n onClick,\n ...rest\n}) => (\n <div\n role=\"menuitem\"\n tabIndex={disabled ? -1 : 0}\n aria-disabled={disabled || undefined}\n onClick={disabled ? undefined : onClick}\n onKeyDown={(event) => {\n if (disabled) return;\n if (event.key !== 'Enter' && event.key !== ' ') return;\n event.preventDefault();\n (event.currentTarget as HTMLElement).click();\n }}\n {...rest}\n className={twMerge(\n classNames(\n 'flex w-full cursor-pointer select-none items-center rounded-md px-3 py-2 text-sm outline-none',\n 'transition-colors hover:bg-muted-hover focus:bg-muted-hover',\n { 'pointer-events-none opacity-50': disabled },\n ),\n className,\n )}\n >\n {children}\n </div>\n);\n\ninterface IControlledMenuProps {\n trigger: React.ReactNode;\n items?: Array<IControlledMenuItem>;\n children?: React.ReactNode;\n triggerTooltip?: string;\n placement?: TMenuPlacement;\n open?: boolean;\n onOpenChange?: (open: boolean) => void;\n disabled?: boolean;\n dismissOnItemClick?: boolean;\n listClassName?: string;\n tooltipClassName?: string;\n listRef?: React.Ref<HTMLDivElement>;\n}\n\nconst ITEM_TONE_CLASS: Record<TMenuItemTone, string> = {\n default: 'text-foreground-muted',\n danger: 'text-danger-emphasis',\n};\n\nconst ICON_TONE_CLASS: Record<TMenuItemTone, string> = {\n default: 'text-foreground-subtle',\n danger: 'text-danger-emphasis',\n};\n\n/**\n * A tooltip on the trigger must sit OUTSIDE the handler: the handler clones its child to\n * inject the click handler and the reference ref, and a tooltip in that position swallows\n * both, so the menu stops opening.\n */\nconst ControlledMenu: React.FC<IControlledMenuProps> = ({\n trigger,\n items,\n children,\n triggerTooltip,\n placement = 'bottom-start',\n open,\n onOpenChange,\n disabled = false,\n dismissOnItemClick = true,\n listClassName,\n tooltipClassName,\n listRef,\n}) => {\n const menuItems = useMemo(() => (Array.isArray(items) ? items : []), [items]);\n const dismiss = useMemo(() => ({ itemPress: dismissOnItemClick }), [dismissOnItemClick]);\n const handle = <ControlledMenuHandler>{trigger}</ControlledMenuHandler>;\n\n if (disabled) return <>{trigger}</>;\n\n return (\n <ControlledMenuRoot open={open} handler={onOpenChange} placement={placement} dismiss={dismiss}>\n {triggerTooltip ? (\n <ControlledTooltip content={triggerTooltip} className={tooltipClassName}>\n {handle}\n </ControlledTooltip>\n ) : (\n handle\n )}\n\n <ControlledMenuList ref={listRef} className={listClassName}>\n {children ??\n menuItems.map((item) => {\n const tone = item.tone ?? 'default';\n\n return (\n <ControlledMenuItem\n key={item.key}\n disabled={item.disabled}\n onClick={item.onClick}\n className={classNames('items-start gap-x-2', ITEM_TONE_CLASS[tone])}\n >\n {item.icon && <MaterialSymbol icon={item.icon} size={16} className={ICON_TONE_CLASS[tone]} />}\n\n <span className=\"flex flex-col items-start gap-y-0.5\">\n <span>{item.label}</span>\n {item.hint && <span className=\"text-xs text-foreground-subtle\">{item.hint}</span>}\n </span>\n </ControlledMenuItem>\n );\n })}\n </ControlledMenuList>\n </ControlledMenuRoot>\n );\n};\n\nexport default ControlledMenu;\n"],"names":[],"mappings":"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"}
|
|
@@ -7,6 +7,7 @@ import { twMerge } from "tailwind-merge";
|
|
|
7
7
|
import ControlledTooltip from "./ControlledTooltip.js";
|
|
8
8
|
import { composeRefs } from "../../helpers/composeRefs.js";
|
|
9
9
|
import { isPressOnLayeredSurface } from "../../helpers/layeredSurfaces.js";
|
|
10
|
+
import { getPortalHost } from "../../helpers/portalHost.js";
|
|
10
11
|
import { useAnchoredFloating } from "../../hooks/useAnchoredFloating.js";
|
|
11
12
|
import { floatingVariants } from "../../motion/motionPresets.js";
|
|
12
13
|
import "../../motion/motionTokens.js";
|
|
@@ -96,6 +97,8 @@ const ControlledPopoverContent = ({
|
|
|
96
97
|
document.removeEventListener("pointerdown", handlePointerDown, true);
|
|
97
98
|
};
|
|
98
99
|
}, [open, setOpen, floatingRef, referenceRef]);
|
|
100
|
+
const portalHost = getPortalHost();
|
|
101
|
+
if (portalHost === null) return null;
|
|
99
102
|
return createPortal(
|
|
100
103
|
/* @__PURE__ */ jsx(AnimatePresence, { children: open && /* @__PURE__ */ jsx(
|
|
101
104
|
motion.div,
|
|
@@ -116,7 +119,7 @@ const ControlledPopoverContent = ({
|
|
|
116
119
|
},
|
|
117
120
|
"popover-content"
|
|
118
121
|
) }),
|
|
119
|
-
|
|
122
|
+
portalHost
|
|
120
123
|
);
|
|
121
124
|
};
|
|
122
125
|
const ControlledPopover = ({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ControlledPopover.js","sources":["../../../src/components/UI/ControlledPopover.tsx"],"sourcesContent":["import React, { cloneElement, createContext, isValidElement, useContext, useEffect, useId, useState } from 'react';\nimport classNames from 'classnames';\nimport { AnimatePresence, motion } from 'motion/react';\nimport { createPortal } from 'react-dom';\nimport { twMerge } from 'tailwind-merge';\n\nimport ControlledTooltip from '@/components/UI/ControlledTooltip';\nimport { composeRefs } from '@/helpers/composeRefs';\nimport { isPressOnLayeredSurface } from '@/helpers/layeredSurfaces';\nimport { TFloatingPlacement, useAnchoredFloating } from '@/hooks/useAnchoredFloating';\nimport { floatingVariants } from '@/motion';\n\nexport type TPopoverPlacement = TFloatingPlacement;\n\ninterface IPopoverContext {\n open: boolean;\n setOpen: (open: boolean) => void;\n placement: TFloatingPlacement;\n offsetPx: number;\n contentId: string;\n setReference: (node: HTMLElement | null) => void;\n setFloating: (node: HTMLElement | null) => void;\n floatingRef: React.MutableRefObject<HTMLElement | null>;\n referenceRef: React.MutableRefObject<HTMLElement | null>;\n}\n\nconst PopoverContext = createContext<IPopoverContext | null>(null);\n\nconst usePopoverContext = (): IPopoverContext => {\n const context = useContext(PopoverContext);\n if (!context) throw new Error('Popover parts must be rendered inside ControlledPopoverRoot');\n return context;\n};\n\ninterface IControlledPopoverRootProps {\n children: React.ReactNode;\n open?: boolean;\n handler?: (open: boolean) => void;\n placement?: TPopoverPlacement;\n offsetPx?: number;\n}\n\nexport const ControlledPopoverRoot: React.FC<IControlledPopoverRootProps> = ({\n children,\n open,\n handler,\n placement = 'bottom-start',\n offsetPx = 6,\n}) => {\n const contentId = useId();\n const [uncontrolledOpen, setUncontrolledOpen] = useState(false);\n\n const isControlled = open !== undefined;\n const isOpen = isControlled ? Boolean(open) : uncontrolledOpen;\n\n const setOpen = (next: boolean) => {\n if (!isControlled) setUncontrolledOpen(next);\n handler?.(next);\n };\n\n const { setReference, setFloating, floatingRef, referenceRef } = useAnchoredFloating({\n open: isOpen,\n placement,\n offsetPx,\n });\n\n return (\n <PopoverContext.Provider\n value={{\n open: isOpen,\n setOpen,\n placement,\n offsetPx,\n contentId,\n setReference,\n setFloating,\n floatingRef,\n referenceRef,\n }}\n >\n {children}\n </PopoverContext.Provider>\n );\n};\n\ninterface IHandlerChildProps {\n onClick?: (event: React.MouseEvent<HTMLElement>) => void;\n ref?: React.Ref<HTMLElement>;\n}\n\n/**\n * Repassa ao filho o que ele recebe de fora, além do que injeta.\n *\n * O gatilho é o alvo natural de um `ControlledTooltip`, e o tooltip se prende clonando o filho e\n * injetando `onPointerEnter`. Aceitando só `children`, este componente engolia essa injeção e o\n * tooltip simplesmente nunca abria, mesmo com a ordem que a documentação manda usar\n * (tooltip POR FORA do handler). Os `onClick` são encadeados: o de fora roda antes de abrir.\n */\nexport const ControlledPopoverHandler = React.forwardRef<\n HTMLElement,\n { children: React.ReactNode } & React.HTMLAttributes<HTMLElement>\n>(({ children, onClick: onOuterClick, ...rest }, forwardedRef) => {\n const { open, setOpen, setReference, contentId } = usePopoverContext();\n\n if (!isValidElement(children)) return <>{children}</>;\n\n const child = children as React.ReactElement<IHandlerChildProps>;\n\n return cloneElement(child, {\n ...rest,\n ref: composeRefs<HTMLElement>(setReference, forwardedRef, (child as { ref?: React.Ref<HTMLElement> }).ref),\n 'aria-expanded': open,\n 'aria-controls': open ? contentId : undefined,\n onClick: (event: React.MouseEvent<HTMLElement>) => {\n onOuterClick?.(event);\n child.props.onClick?.(event);\n setOpen(!open);\n },\n } as IHandlerChildProps);\n});\n\nControlledPopoverHandler.displayName = 'ControlledPopoverHandler';\n\ninterface IControlledPopoverContentProps extends Omit<\n React.HTMLAttributes<HTMLDivElement>,\n 'className' | 'onDrag' | 'onDragStart' | 'onDragEnd' | 'onAnimationStart' | 'onAnimationEnd' | 'onAnimationIteration'\n> {\n children: React.ReactNode;\n className?: string;\n}\n\nexport const ControlledPopoverContent: React.FC<IControlledPopoverContentProps> = ({\n children,\n className,\n ...rest\n}) => {\n const { open, setOpen, contentId, setFloating, floatingRef, referenceRef } = usePopoverContext();\n\n useEffect(() => {\n if (!open) return undefined;\n\n const handleKeyDown = (event: KeyboardEvent) => {\n if (event.key === 'Escape') setOpen(false);\n };\n\n const handlePointerDown = (event: PointerEvent) => {\n const target = event.target as Node | null;\n if (!target) return;\n if (floatingRef.current?.contains(target) || referenceRef.current?.contains(target)) return;\n if (isPressOnLayeredSurface(target, floatingRef.current)) return;\n setOpen(false);\n };\n\n document.addEventListener('keydown', handleKeyDown);\n document.addEventListener('pointerdown', handlePointerDown, true);\n\n return () => {\n document.removeEventListener('keydown', handleKeyDown);\n document.removeEventListener('pointerdown', handlePointerDown, true);\n };\n }, [open, setOpen, floatingRef, referenceRef]);\n\n return createPortal(\n <AnimatePresence>\n {open && (\n <motion.div\n key=\"popover-content\"\n id={contentId}\n ref={setFloating}\n variants={floatingVariants}\n initial=\"hidden\"\n animate=\"visible\"\n exit=\"exit\"\n role=\"dialog\"\n {...rest}\n className={twMerge(\n 'fixed left-0 top-0 z-[9999] overflow-auto rounded-lg border border-border p-2 text-sm text-foreground glass',\n className,\n )}\n >\n {children}\n </motion.div>\n )}\n </AnimatePresence>,\n document.body,\n );\n};\n\ninterface IControlledPopoverProps {\n trigger: React.ReactNode;\n children: React.ReactNode;\n triggerTooltip?: string;\n placement?: TPopoverPlacement;\n open?: boolean;\n onOpenChange?: (open: boolean) => void;\n contentClassName?: string;\n tooltipClassName?: string;\n}\n\n/**\n * A tooltip on the trigger must sit OUTSIDE the handler: the handler clones its child to\n * inject the click handler and the reference ref, and a tooltip in that position swallows\n * both, so the popover stops opening.\n */\nconst ControlledPopover: React.FC<IControlledPopoverProps> = ({\n trigger,\n children,\n triggerTooltip,\n placement = 'bottom-start',\n open,\n onOpenChange,\n contentClassName,\n tooltipClassName,\n}) => {\n const handle = <ControlledPopoverHandler>{trigger}</ControlledPopoverHandler>;\n\n return (\n <ControlledPopoverRoot open={open} handler={onOpenChange} placement={placement}>\n {triggerTooltip ? (\n <ControlledTooltip content={triggerTooltip} className={classNames(tooltipClassName)}>\n {handle}\n </ControlledTooltip>\n ) : (\n handle\n )}\n\n <ControlledPopoverContent className={contentClassName}>{children}</ControlledPopoverContent>\n </ControlledPopoverRoot>\n );\n};\n\nexport default ControlledPopover;\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ControlledPopover.js","sources":["../../../src/components/UI/ControlledPopover.tsx"],"sourcesContent":["import React, { cloneElement, createContext, isValidElement, useContext, useEffect, useId, useState } from 'react';\nimport classNames from 'classnames';\nimport { AnimatePresence, motion } from 'motion/react';\nimport { createPortal } from 'react-dom';\nimport { twMerge } from 'tailwind-merge';\n\nimport ControlledTooltip from '@/components/UI/ControlledTooltip';\nimport { composeRefs } from '@/helpers/composeRefs';\nimport { isPressOnLayeredSurface } from '@/helpers/layeredSurfaces';\nimport { getPortalHost } from '@/helpers/portalHost';\nimport { TFloatingPlacement, useAnchoredFloating } from '@/hooks/useAnchoredFloating';\nimport { floatingVariants } from '@/motion';\n\nexport type TPopoverPlacement = TFloatingPlacement;\n\ninterface IPopoverContext {\n open: boolean;\n setOpen: (open: boolean) => void;\n placement: TFloatingPlacement;\n offsetPx: number;\n contentId: string;\n setReference: (node: HTMLElement | null) => void;\n setFloating: (node: HTMLElement | null) => void;\n floatingRef: React.MutableRefObject<HTMLElement | null>;\n referenceRef: React.MutableRefObject<HTMLElement | null>;\n}\n\nconst PopoverContext = createContext<IPopoverContext | null>(null);\n\nconst usePopoverContext = (): IPopoverContext => {\n const context = useContext(PopoverContext);\n if (!context) throw new Error('Popover parts must be rendered inside ControlledPopoverRoot');\n return context;\n};\n\ninterface IControlledPopoverRootProps {\n children: React.ReactNode;\n open?: boolean;\n handler?: (open: boolean) => void;\n placement?: TPopoverPlacement;\n offsetPx?: number;\n}\n\nexport const ControlledPopoverRoot: React.FC<IControlledPopoverRootProps> = ({\n children,\n open,\n handler,\n placement = 'bottom-start',\n offsetPx = 6,\n}) => {\n const contentId = useId();\n const [uncontrolledOpen, setUncontrolledOpen] = useState(false);\n\n const isControlled = open !== undefined;\n const isOpen = isControlled ? Boolean(open) : uncontrolledOpen;\n\n const setOpen = (next: boolean) => {\n if (!isControlled) setUncontrolledOpen(next);\n handler?.(next);\n };\n\n const { setReference, setFloating, floatingRef, referenceRef } = useAnchoredFloating({\n open: isOpen,\n placement,\n offsetPx,\n });\n\n return (\n <PopoverContext.Provider\n value={{\n open: isOpen,\n setOpen,\n placement,\n offsetPx,\n contentId,\n setReference,\n setFloating,\n floatingRef,\n referenceRef,\n }}\n >\n {children}\n </PopoverContext.Provider>\n );\n};\n\ninterface IHandlerChildProps {\n onClick?: (event: React.MouseEvent<HTMLElement>) => void;\n ref?: React.Ref<HTMLElement>;\n}\n\n/**\n * Repassa ao filho o que ele recebe de fora, além do que injeta.\n *\n * O gatilho é o alvo natural de um `ControlledTooltip`, e o tooltip se prende clonando o filho e\n * injetando `onPointerEnter`. Aceitando só `children`, este componente engolia essa injeção e o\n * tooltip simplesmente nunca abria, mesmo com a ordem que a documentação manda usar\n * (tooltip POR FORA do handler). Os `onClick` são encadeados: o de fora roda antes de abrir.\n */\nexport const ControlledPopoverHandler = React.forwardRef<\n HTMLElement,\n { children: React.ReactNode } & React.HTMLAttributes<HTMLElement>\n>(({ children, onClick: onOuterClick, ...rest }, forwardedRef) => {\n const { open, setOpen, setReference, contentId } = usePopoverContext();\n\n if (!isValidElement(children)) return <>{children}</>;\n\n const child = children as React.ReactElement<IHandlerChildProps>;\n\n return cloneElement(child, {\n ...rest,\n ref: composeRefs<HTMLElement>(setReference, forwardedRef, (child as { ref?: React.Ref<HTMLElement> }).ref),\n 'aria-expanded': open,\n 'aria-controls': open ? contentId : undefined,\n onClick: (event: React.MouseEvent<HTMLElement>) => {\n onOuterClick?.(event);\n child.props.onClick?.(event);\n setOpen(!open);\n },\n } as IHandlerChildProps);\n});\n\nControlledPopoverHandler.displayName = 'ControlledPopoverHandler';\n\ninterface IControlledPopoverContentProps extends Omit<\n React.HTMLAttributes<HTMLDivElement>,\n 'className' | 'onDrag' | 'onDragStart' | 'onDragEnd' | 'onAnimationStart' | 'onAnimationEnd' | 'onAnimationIteration'\n> {\n children: React.ReactNode;\n className?: string;\n}\n\nexport const ControlledPopoverContent: React.FC<IControlledPopoverContentProps> = ({\n children,\n className,\n ...rest\n}) => {\n const { open, setOpen, contentId, setFloating, floatingRef, referenceRef } = usePopoverContext();\n\n useEffect(() => {\n if (!open) return undefined;\n\n const handleKeyDown = (event: KeyboardEvent) => {\n if (event.key === 'Escape') setOpen(false);\n };\n\n const handlePointerDown = (event: PointerEvent) => {\n const target = event.target as Node | null;\n if (!target) return;\n if (floatingRef.current?.contains(target) || referenceRef.current?.contains(target)) return;\n if (isPressOnLayeredSurface(target, floatingRef.current)) return;\n setOpen(false);\n };\n\n document.addEventListener('keydown', handleKeyDown);\n document.addEventListener('pointerdown', handlePointerDown, true);\n\n return () => {\n document.removeEventListener('keydown', handleKeyDown);\n document.removeEventListener('pointerdown', handlePointerDown, true);\n };\n }, [open, setOpen, floatingRef, referenceRef]);\n\n const portalHost = getPortalHost();\n if (portalHost === null) return null;\n\n return createPortal(\n <AnimatePresence>\n {open && (\n <motion.div\n key=\"popover-content\"\n id={contentId}\n ref={setFloating}\n variants={floatingVariants}\n initial=\"hidden\"\n animate=\"visible\"\n exit=\"exit\"\n role=\"dialog\"\n {...rest}\n className={twMerge(\n 'fixed left-0 top-0 z-[9999] overflow-auto rounded-lg border border-border p-2 text-sm text-foreground glass',\n className,\n )}\n >\n {children}\n </motion.div>\n )}\n </AnimatePresence>,\n portalHost,\n );\n};\n\ninterface IControlledPopoverProps {\n trigger: React.ReactNode;\n children: React.ReactNode;\n triggerTooltip?: string;\n placement?: TPopoverPlacement;\n open?: boolean;\n onOpenChange?: (open: boolean) => void;\n contentClassName?: string;\n tooltipClassName?: string;\n}\n\n/**\n * A tooltip on the trigger must sit OUTSIDE the handler: the handler clones its child to\n * inject the click handler and the reference ref, and a tooltip in that position swallows\n * both, so the popover stops opening.\n */\nconst ControlledPopover: React.FC<IControlledPopoverProps> = ({\n trigger,\n children,\n triggerTooltip,\n placement = 'bottom-start',\n open,\n onOpenChange,\n contentClassName,\n tooltipClassName,\n}) => {\n const handle = <ControlledPopoverHandler>{trigger}</ControlledPopoverHandler>;\n\n return (\n <ControlledPopoverRoot open={open} handler={onOpenChange} placement={placement}>\n {triggerTooltip ? (\n <ControlledTooltip content={triggerTooltip} className={classNames(tooltipClassName)}>\n {handle}\n </ControlledTooltip>\n ) : (\n handle\n )}\n\n <ControlledPopoverContent className={contentClassName}>{children}</ControlledPopoverContent>\n </ControlledPopoverRoot>\n );\n};\n\nexport default ControlledPopover;\n"],"names":[],"mappings":"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"}
|
|
@@ -4,6 +4,7 @@ import { AnimatePresence, motion } from "motion/react";
|
|
|
4
4
|
import { createPortal } from "react-dom";
|
|
5
5
|
import { twMerge } from "tailwind-merge";
|
|
6
6
|
import { composeRefs } from "../../helpers/composeRefs.js";
|
|
7
|
+
import { getPortalHost } from "../../helpers/portalHost.js";
|
|
7
8
|
import { useAnchoredFloating } from "../../hooks/useAnchoredFloating.js";
|
|
8
9
|
import { tooltipVariants } from "../../motion/motionPresets.js";
|
|
9
10
|
import "../../motion/motionTokens.js";
|
|
@@ -110,9 +111,10 @@ const ControlledTooltip = ({
|
|
|
110
111
|
trigger.props.onFocus?.(event);
|
|
111
112
|
}
|
|
112
113
|
});
|
|
114
|
+
const portalHost = getPortalHost();
|
|
113
115
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
114
116
|
triggerWithTracking,
|
|
115
|
-
createPortal(
|
|
117
|
+
portalHost !== null && createPortal(
|
|
116
118
|
/* @__PURE__ */ jsx(AnimatePresence, { children: resolvedOpen && /* @__PURE__ */ jsx(
|
|
117
119
|
motion.div,
|
|
118
120
|
{
|
|
@@ -130,7 +132,7 @@ const ControlledTooltip = ({
|
|
|
130
132
|
},
|
|
131
133
|
"tooltip-bubble"
|
|
132
134
|
) }),
|
|
133
|
-
|
|
135
|
+
portalHost
|
|
134
136
|
)
|
|
135
137
|
] });
|
|
136
138
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ControlledTooltip.js","sources":["../../../src/components/UI/ControlledTooltip.tsx"],"sourcesContent":["import { cloneElement, isValidElement, useCallback, useEffect, useRef, useState } from 'react';\nimport { AnimatePresence, motion } from 'motion/react';\nimport type { FocusEvent, MouseEvent, PointerEvent, ReactElement, ReactNode } from 'react';\nimport { createPortal } from 'react-dom';\nimport { twMerge } from 'tailwind-merge';\n\nimport { composeRefs } from '@/helpers/composeRefs';\nimport { TFloatingPlacement, useAnchoredFloating } from '@/hooks/useAnchoredFloating';\nimport { tooltipVariants } from '@/motion';\n\nexport interface IControlledTooltipProps {\n content: ReactNode;\n children: ReactNode;\n className?: string;\n placement?: TFloatingPlacement;\n offset?: number;\n /** Lifts the touch block: a click-to-open tooltip is a deliberate mobile pattern. */\n interactive?: boolean;\n open?: boolean;\n handler?: (open: boolean) => void;\n}\n\ntype TTooltipTriggerProps = {\n onPointerEnter?: (event: PointerEvent<HTMLElement>) => void;\n onMouseEnter?: (event: MouseEvent<HTMLElement>) => void;\n onFocus?: (event: FocusEvent<HTMLElement>) => void;\n ref?: React.Ref<HTMLElement>;\n};\n\nconst ORPHAN_TOOLTIP_CHECK_INTERVAL_MS = 250;\n\nconst isTouchFirstDevice = (): boolean => window.matchMedia('(hover: none), (pointer: coarse)').matches;\n\n/**\n * Owns the open state instead of delegating to the positioning library, because a raw\n * hover interaction opens on touch (the browser's compatibility `mouseenter`) and never\n * closes when the trigger stops receiving `mouseleave`: detached from the DOM, covered by\n * a dialog, or re-rendered. Both leave a bubble parked on the screen.\n */\nconst ControlledTooltip: React.FC<IControlledTooltipProps> = ({\n content,\n children,\n className,\n placement = 'top',\n offset = 6,\n interactive,\n open,\n handler,\n}) => {\n const [isOpen, setIsOpen] = useState(false);\n const triggerRef = useRef<HTMLElement | null>(null);\n const triggerPointerTypeRef = useRef<string | null>(null);\n const pointerPositionRef = useRef<{ x: number; y: number } | null>(null);\n\n const isControlledByHost = open !== undefined;\n const opensOnHover = !interactive;\n const resolvedOpen = isControlledByHost ? Boolean(open) : isOpen;\n\n const { setReference, setFloating } = useAnchoredFloating({\n open: resolvedOpen,\n placement,\n offsetPx: offset,\n constrainToViewport: false,\n });\n\n const closeTooltip = useCallback(() => {\n setIsOpen(false);\n handler?.(false);\n }, [handler]);\n\n const openTooltip = useCallback(() => {\n const pointerType = triggerPointerTypeRef.current;\n const openedByTouch = pointerType === null ? isTouchFirstDevice() : pointerType !== 'mouse';\n if (opensOnHover && openedByTouch) return;\n\n setIsOpen(true);\n handler?.(true);\n }, [handler, opensOnHover]);\n\n useEffect(() => {\n if (isControlledByHost || !resolvedOpen || !opensOnHover) return undefined;\n\n const belongsToTooltip = (element: Node | null): boolean => {\n const trigger = triggerRef.current;\n if (!element || !trigger) return false;\n if (trigger.contains(element)) return true;\n return element instanceof Element && element.closest('[role=\"tooltip\"]') !== null;\n };\n\n const closeWhenPointerLeftTrigger = (event: globalThis.PointerEvent) => {\n pointerPositionRef.current = { x: event.clientX, y: event.clientY };\n if (!triggerRef.current) return;\n if (belongsToTooltip(event.target instanceof Node ? event.target : null)) return;\n closeTooltip();\n };\n\n const closeWhenPageIsHidden = () => {\n if (document.hidden) closeTooltip();\n };\n\n const orphanTooltipCheck = window.setInterval(() => {\n const trigger = triggerRef.current;\n if (!trigger) return;\n if (!trigger.isConnected) {\n closeTooltip();\n return;\n }\n\n const pointerPosition = pointerPositionRef.current;\n if (!pointerPosition) return;\n if (!belongsToTooltip(document.elementFromPoint(pointerPosition.x, pointerPosition.y))) closeTooltip();\n }, ORPHAN_TOOLTIP_CHECK_INTERVAL_MS);\n\n document.addEventListener('pointermove', closeWhenPointerLeftTrigger, { capture: true, passive: true });\n document.addEventListener('pointerdown', closeTooltip, { capture: true, passive: true });\n document.addEventListener('visibilitychange', closeWhenPageIsHidden);\n window.addEventListener('blur', closeTooltip);\n\n return () => {\n window.clearInterval(orphanTooltipCheck);\n document.removeEventListener('pointermove', closeWhenPointerLeftTrigger, { capture: true });\n document.removeEventListener('pointerdown', closeTooltip, { capture: true });\n document.removeEventListener('visibilitychange', closeWhenPageIsHidden);\n window.removeEventListener('blur', closeTooltip);\n };\n }, [closeTooltip, isControlledByHost, resolvedOpen, opensOnHover]);\n\n if (!isValidElement(children)) return <>{children}</>;\n\n const trigger = children as ReactElement<TTooltipTriggerProps>;\n\n const triggerWithTracking = cloneElement(trigger, {\n ref: composeRefs<HTMLElement>(\n (node) => {\n triggerRef.current = node;\n },\n setReference,\n (trigger as { ref?: React.Ref<HTMLElement> }).ref,\n ),\n onPointerEnter: (event: PointerEvent<HTMLElement>) => {\n triggerPointerTypeRef.current = event.pointerType || null;\n triggerRef.current = event.currentTarget;\n pointerPositionRef.current = { x: event.clientX, y: event.clientY };\n trigger.props.onPointerEnter?.(event);\n if (!isControlledByHost) openTooltip();\n },\n onMouseEnter: (event: MouseEvent<HTMLElement>) => {\n triggerRef.current = event.currentTarget;\n pointerPositionRef.current = { x: event.clientX, y: event.clientY };\n trigger.props.onMouseEnter?.(event);\n },\n onFocus: (event: FocusEvent<HTMLElement>) => {\n triggerRef.current = event.currentTarget;\n trigger.props.onFocus?.(event);\n },\n } as TTooltipTriggerProps);\n\n return (\n <>\n {triggerWithTracking}\n\n {createPortal(\n <AnimatePresence>\n {resolvedOpen && (\n <motion.div\n key=\"tooltip-bubble\"\n ref={setFloating}\n variants={tooltipVariants}\n initial=\"hidden\"\n animate=\"visible\"\n exit=\"exit\"\n role=\"tooltip\"\n className={twMerge(\n 'fixed left-0 top-0 z-[999] max-w-xs rounded-lg bg-ink px-3 py-2 text-xs font-normal text-ink-fg shadow-lg',\n className,\n )}\n >\n {content}\n </motion.div>\n )}\n </AnimatePresence>,\n document.body,\n )}\n </>\n );\n};\n\nexport default ControlledTooltip;\n"],"names":["trigger"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ControlledTooltip.js","sources":["../../../src/components/UI/ControlledTooltip.tsx"],"sourcesContent":["import { cloneElement, isValidElement, useCallback, useEffect, useRef, useState } from 'react';\nimport { AnimatePresence, motion } from 'motion/react';\nimport type { FocusEvent, MouseEvent, PointerEvent, ReactElement, ReactNode } from 'react';\nimport { createPortal } from 'react-dom';\nimport { twMerge } from 'tailwind-merge';\n\nimport { composeRefs } from '@/helpers/composeRefs';\nimport { getPortalHost } from '@/helpers/portalHost';\nimport { TFloatingPlacement, useAnchoredFloating } from '@/hooks/useAnchoredFloating';\nimport { tooltipVariants } from '@/motion';\n\nexport interface IControlledTooltipProps {\n content: ReactNode;\n children: ReactNode;\n className?: string;\n placement?: TFloatingPlacement;\n offset?: number;\n /** Lifts the touch block: a click-to-open tooltip is a deliberate mobile pattern. */\n interactive?: boolean;\n open?: boolean;\n handler?: (open: boolean) => void;\n}\n\ntype TTooltipTriggerProps = {\n onPointerEnter?: (event: PointerEvent<HTMLElement>) => void;\n onMouseEnter?: (event: MouseEvent<HTMLElement>) => void;\n onFocus?: (event: FocusEvent<HTMLElement>) => void;\n ref?: React.Ref<HTMLElement>;\n};\n\nconst ORPHAN_TOOLTIP_CHECK_INTERVAL_MS = 250;\n\nconst isTouchFirstDevice = (): boolean => window.matchMedia('(hover: none), (pointer: coarse)').matches;\n\n/**\n * Owns the open state instead of delegating to the positioning library, because a raw\n * hover interaction opens on touch (the browser's compatibility `mouseenter`) and never\n * closes when the trigger stops receiving `mouseleave`: detached from the DOM, covered by\n * a dialog, or re-rendered. Both leave a bubble parked on the screen.\n */\nconst ControlledTooltip: React.FC<IControlledTooltipProps> = ({\n content,\n children,\n className,\n placement = 'top',\n offset = 6,\n interactive,\n open,\n handler,\n}) => {\n const [isOpen, setIsOpen] = useState(false);\n const triggerRef = useRef<HTMLElement | null>(null);\n const triggerPointerTypeRef = useRef<string | null>(null);\n const pointerPositionRef = useRef<{ x: number; y: number } | null>(null);\n\n const isControlledByHost = open !== undefined;\n const opensOnHover = !interactive;\n const resolvedOpen = isControlledByHost ? Boolean(open) : isOpen;\n\n const { setReference, setFloating } = useAnchoredFloating({\n open: resolvedOpen,\n placement,\n offsetPx: offset,\n constrainToViewport: false,\n });\n\n const closeTooltip = useCallback(() => {\n setIsOpen(false);\n handler?.(false);\n }, [handler]);\n\n const openTooltip = useCallback(() => {\n const pointerType = triggerPointerTypeRef.current;\n const openedByTouch = pointerType === null ? isTouchFirstDevice() : pointerType !== 'mouse';\n if (opensOnHover && openedByTouch) return;\n\n setIsOpen(true);\n handler?.(true);\n }, [handler, opensOnHover]);\n\n useEffect(() => {\n if (isControlledByHost || !resolvedOpen || !opensOnHover) return undefined;\n\n const belongsToTooltip = (element: Node | null): boolean => {\n const trigger = triggerRef.current;\n if (!element || !trigger) return false;\n if (trigger.contains(element)) return true;\n return element instanceof Element && element.closest('[role=\"tooltip\"]') !== null;\n };\n\n const closeWhenPointerLeftTrigger = (event: globalThis.PointerEvent) => {\n pointerPositionRef.current = { x: event.clientX, y: event.clientY };\n if (!triggerRef.current) return;\n if (belongsToTooltip(event.target instanceof Node ? event.target : null)) return;\n closeTooltip();\n };\n\n const closeWhenPageIsHidden = () => {\n if (document.hidden) closeTooltip();\n };\n\n const orphanTooltipCheck = window.setInterval(() => {\n const trigger = triggerRef.current;\n if (!trigger) return;\n if (!trigger.isConnected) {\n closeTooltip();\n return;\n }\n\n const pointerPosition = pointerPositionRef.current;\n if (!pointerPosition) return;\n if (!belongsToTooltip(document.elementFromPoint(pointerPosition.x, pointerPosition.y))) closeTooltip();\n }, ORPHAN_TOOLTIP_CHECK_INTERVAL_MS);\n\n document.addEventListener('pointermove', closeWhenPointerLeftTrigger, { capture: true, passive: true });\n document.addEventListener('pointerdown', closeTooltip, { capture: true, passive: true });\n document.addEventListener('visibilitychange', closeWhenPageIsHidden);\n window.addEventListener('blur', closeTooltip);\n\n return () => {\n window.clearInterval(orphanTooltipCheck);\n document.removeEventListener('pointermove', closeWhenPointerLeftTrigger, { capture: true });\n document.removeEventListener('pointerdown', closeTooltip, { capture: true });\n document.removeEventListener('visibilitychange', closeWhenPageIsHidden);\n window.removeEventListener('blur', closeTooltip);\n };\n }, [closeTooltip, isControlledByHost, resolvedOpen, opensOnHover]);\n\n if (!isValidElement(children)) return <>{children}</>;\n\n const trigger = children as ReactElement<TTooltipTriggerProps>;\n\n const triggerWithTracking = cloneElement(trigger, {\n ref: composeRefs<HTMLElement>(\n (node) => {\n triggerRef.current = node;\n },\n setReference,\n (trigger as { ref?: React.Ref<HTMLElement> }).ref,\n ),\n onPointerEnter: (event: PointerEvent<HTMLElement>) => {\n triggerPointerTypeRef.current = event.pointerType || null;\n triggerRef.current = event.currentTarget;\n pointerPositionRef.current = { x: event.clientX, y: event.clientY };\n trigger.props.onPointerEnter?.(event);\n if (!isControlledByHost) openTooltip();\n },\n onMouseEnter: (event: MouseEvent<HTMLElement>) => {\n triggerRef.current = event.currentTarget;\n pointerPositionRef.current = { x: event.clientX, y: event.clientY };\n trigger.props.onMouseEnter?.(event);\n },\n onFocus: (event: FocusEvent<HTMLElement>) => {\n triggerRef.current = event.currentTarget;\n trigger.props.onFocus?.(event);\n },\n } as TTooltipTriggerProps);\n\n const portalHost = getPortalHost();\n\n return (\n <>\n {triggerWithTracking}\n\n {portalHost !== null &&\n createPortal(\n <AnimatePresence>\n {resolvedOpen && (\n <motion.div\n key=\"tooltip-bubble\"\n ref={setFloating}\n variants={tooltipVariants}\n initial=\"hidden\"\n animate=\"visible\"\n exit=\"exit\"\n role=\"tooltip\"\n className={twMerge(\n 'fixed left-0 top-0 z-[999] max-w-xs rounded-lg bg-ink px-3 py-2 text-xs font-normal text-ink-fg shadow-lg',\n className,\n )}\n >\n {content}\n </motion.div>\n )}\n </AnimatePresence>,\n portalHost,\n )}\n </>\n );\n};\n\nexport default ControlledTooltip;\n"],"names":["trigger"],"mappings":"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"}
|
|
@@ -13,9 +13,9 @@ interface IDataTableBulkEditDialogProps<T extends IDataTableRow> {
|
|
|
13
13
|
onApply: (changes: Array<IDataTableFillChange<T>>) => void;
|
|
14
14
|
}
|
|
15
15
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
16
|
+
* Bulk edit in two modes. "Same value" applies one value to one field across the
|
|
17
|
+
* chosen scope; "Values per item" opens an item x fields matrix prefilled with
|
|
18
|
+
* the current values, and only the changed cells are written.
|
|
19
19
|
*
|
|
20
20
|
* O diálogo não persiste nada. Ele devolve as células escritas por `onApply`, que
|
|
21
21
|
* o host entrega ao `onFillCells` da fila de commit: um render, um flush e uma
|