@mattermost/compass-ui 0.1.0-alpha.28 → 0.1.0-alpha.29
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/compass-ui.css +4 -2
- package/dist/components/modal/ModalOverlay.cjs.map +1 -1
- package/dist/components/modal/ModalOverlay.d.ts +1 -1
- package/dist/components/modal/ModalOverlay.js.map +1 -1
- package/dist/hooks/use-anchored-popup-portal.cjs.map +1 -1
- package/dist/hooks/use-anchored-popup-portal.d.ts +1 -1
- package/dist/hooks/use-anchored-popup-portal.d.ts.map +1 -1
- package/dist/hooks/use-anchored-popup-portal.js.map +1 -1
- package/dist/utils/anchoredPopupPlacement.cjs +1 -1
- package/dist/utils/anchoredPopupPlacement.cjs.map +1 -1
- package/dist/utils/anchoredPopupPlacement.d.ts +8 -4
- package/dist/utils/anchoredPopupPlacement.d.ts.map +1 -1
- package/dist/utils/anchoredPopupPlacement.js +18 -18
- package/dist/utils/anchoredPopupPlacement.js.map +1 -1
- package/package.json +1 -1
package/dist/compass-ui.css
CHANGED
|
@@ -289,8 +289,10 @@
|
|
|
289
289
|
--attachment-red: var(--color-red-500);
|
|
290
290
|
--attachment-orange: var(--color-orange-500);
|
|
291
291
|
--z-sidebar: 100;
|
|
292
|
-
--z-modal:
|
|
293
|
-
--z-
|
|
292
|
+
--z-modal: 1050;
|
|
293
|
+
--z-menu: 1100;
|
|
294
|
+
--z-popover: 1200;
|
|
295
|
+
--z-tooltip: 1350;
|
|
294
296
|
--opacity-4: 0.04;
|
|
295
297
|
--opacity-8: 0.08;
|
|
296
298
|
--opacity-12: 0.12;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ModalOverlay.cjs","sources":["../../../src/components/modal/ModalOverlay.tsx"],"sourcesContent":["import {\n useEffect,\n useRef,\n type HTMLAttributes,\n type MouseEvent,\n type ReactNode,\n} from 'react';\nimport { createPortal } from 'react-dom';\nimport { usePopoverTransition } from '@/hooks/use-popover-transition';\nimport { mergeClassNames } from '@/utils/props';\nimport { lockBodyScroll } from '@/utils/scrollLock';\nimport { useOnExited } from '@/utils/useOnExited';\nimport styles from './ModalOverlay.module.scss';\n\nexport interface ModalOverlayProps extends HTMLAttributes<HTMLDivElement> {\n /** Whether the overlay is shown. Setting it to `false` plays the exit and then unmounts. */\n open: boolean;\n /** Called when the backdrop is pressed. Presses that start inside the dialog do not call it. */\n onClose?: () => void;\n /**\n * Called once the exit finishes and the overlay has unmounted. Not called if\n * the overlay itself is unmounted before the exit finishes, so do any\n * cleanup on unmount as well.\n */\n onExited?: () => void;\n /**\n * Portal node. Defaults to `document.body`; pass another element so a modal\n * inside another top layer can join it. `null` renders nothing until the node\n * exists (e.g. a ref held in state). Override `position` through `style`\n * when the container is a positioned stage. Scroll lock applies to that\n * node's document.\n */\n container?: HTMLElement | null;\n /**\n * Lock `document.body` scroll while the overlay is mounted. Turn off for an\n * overlay staged inside a frame (docs, Storybook) so it doesn't freeze the page.\n */\n lockScroll?: boolean;\n /**\n * Stacking order. Defaults to `--z-modal` (
|
|
1
|
+
{"version":3,"file":"ModalOverlay.cjs","sources":["../../../src/components/modal/ModalOverlay.tsx"],"sourcesContent":["import {\n useEffect,\n useRef,\n type HTMLAttributes,\n type MouseEvent,\n type ReactNode,\n} from 'react';\nimport { createPortal } from 'react-dom';\nimport { usePopoverTransition } from '@/hooks/use-popover-transition';\nimport { mergeClassNames } from '@/utils/props';\nimport { lockBodyScroll } from '@/utils/scrollLock';\nimport { useOnExited } from '@/utils/useOnExited';\nimport styles from './ModalOverlay.module.scss';\n\nexport interface ModalOverlayProps extends HTMLAttributes<HTMLDivElement> {\n /** Whether the overlay is shown. Setting it to `false` plays the exit and then unmounts. */\n open: boolean;\n /** Called when the backdrop is pressed. Presses that start inside the dialog do not call it. */\n onClose?: () => void;\n /**\n * Called once the exit finishes and the overlay has unmounted. Not called if\n * the overlay itself is unmounted before the exit finishes, so do any\n * cleanup on unmount as well.\n */\n onExited?: () => void;\n /**\n * Portal node. Defaults to `document.body`; pass another element so a modal\n * inside another top layer can join it. `null` renders nothing until the node\n * exists (e.g. a ref held in state). Override `position` through `style`\n * when the container is a positioned stage. Scroll lock applies to that\n * node's document.\n */\n container?: HTMLElement | null;\n /**\n * Lock `document.body` scroll while the overlay is mounted. Turn off for an\n * overlay staged inside a frame (docs, Storybook) so it doesn't freeze the page.\n */\n lockScroll?: boolean;\n /**\n * Stacking order. Defaults to `--z-modal` (1050, the webapp's modal layer); a host whose chrome sits\n * higher passes this or redefines `--z-modal`. Applied before `style`, so\n * `style.zIndex` wins.\n */\n zIndex?: number;\n /** Usually a `Modal`. */\n children?: ReactNode;\n}\n\n/**\n * Modal scrim: portals to `document.body` (or `container`), locks body scroll\n * while shown (opt out with `lockScroll={false}`), closes on backdrop press, and stays mounted for\n * `--duration-quick` after `open` becomes false so the fade-out can play.\n * Escape and focus stay on `Modal`. Hosts that already position their own\n * node can use `modalOverlayStyles` instead.\n */\nexport default function ModalOverlay({\n open,\n onClose,\n onExited,\n container,\n lockScroll = true,\n zIndex,\n className,\n style,\n children,\n onMouseDown,\n onClick,\n ...rest\n}: ModalOverlayProps) {\n const { mounted } = usePopoverTransition(open);\n useOnExited(mounted, onExited);\n const pressStartedInside = useRef(false);\n\n const target =\n container === undefined\n ? typeof document === 'undefined'\n ? null\n : document.body\n : container;\n\n useEffect(() => {\n if (!mounted || !target || !lockScroll) return undefined;\n return lockBodyScroll(target.ownerDocument);\n }, [mounted, target, lockScroll]);\n\n if (!mounted || !target) return null;\n\n const handleMouseDown = (e: MouseEvent<HTMLDivElement>) => {\n pressStartedInside.current = e.target !== e.currentTarget;\n onMouseDown?.(e);\n };\n\n const handleClick = (e: MouseEvent<HTMLDivElement>) => {\n const startedInside = pressStartedInside.current;\n pressStartedInside.current = false;\n onClick?.(e);\n if (open && !startedInside && e.target === e.currentTarget) onClose?.();\n };\n\n return createPortal(\n // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions -- backdrop click is a pointer shortcut; keyboard users close with Escape (handled by Modal)\n <div\n {...rest}\n className={mergeClassNames(\n styles['modal-overlay'],\n !open && styles['modal-overlay--exiting'],\n className,\n )}\n style={{ ...(zIndex !== undefined && { zIndex }), ...style }}\n onMouseDown={handleMouseDown}\n onClick={handleClick}\n >\n {children}\n </div>,\n target,\n );\n}\n"],"names":["ModalOverlay","open","onClose","onExited","container","lockScroll","zIndex","className","style","children","onMouseDown","onClick","rest","mounted","usePopoverTransition","useOnExited","pressStartedInside","useRef","target","useEffect","lockBodyScroll","handleMouseDown","handleClick","startedInside","createPortal","jsx","mergeClassNames","styles"],"mappings":"6YAuDA,SAAwBA,EAAa,CACnC,KAAAC,EACA,QAAAC,EACA,SAAAC,EACA,UAAAC,EACA,WAAAC,EAAa,GACb,OAAAC,EACA,UAAAC,EACA,MAAAC,EACA,SAAAC,EACA,YAAAC,EACA,QAAAC,EACA,GAAGC,CACL,EAAsB,CACpB,KAAM,CAAE,QAAAC,CAAA,EAAYC,EAAAA,qBAAqBb,CAAI,EAC7Cc,EAAAA,YAAYF,EAASV,CAAQ,EAC7B,MAAMa,EAAqBC,EAAAA,OAAO,EAAK,EAEjCC,EACJd,IAAc,OACV,OAAO,SAAa,IAClB,KACA,SAAS,KACXA,EAON,GALAe,EAAAA,UAAU,IAAM,CACd,GAAI,GAACN,GAAW,CAACK,GAAU,CAACb,GAC5B,OAAOe,EAAAA,eAAeF,EAAO,aAAa,CAC5C,EAAG,CAACL,EAASK,EAAQb,CAAU,CAAC,EAE5B,CAACQ,GAAW,CAACK,EAAQ,OAAO,KAEhC,MAAMG,EAAmB,GAAkC,CACzDL,EAAmB,QAAU,EAAE,SAAW,EAAE,cAC5CN,GAAA,MAAAA,EAAc,EAChB,EAEMY,EAAe,GAAkC,CACrD,MAAMC,EAAgBP,EAAmB,QACzCA,EAAmB,QAAU,GAC7BL,GAAA,MAAAA,EAAU,GACNV,GAAQ,CAACsB,GAAiB,EAAE,SAAW,EAAE,gBAAerB,GAAA,MAAAA,IAC9D,EAEA,OAAOsB,EAAAA,aAELC,EAAAA,IAAC,MAAA,CACE,GAAGb,EACJ,UAAWc,EAAAA,gBACTC,EAAAA,QAAO,eAAe,EACtB,CAAC1B,GAAQ0B,EAAAA,QAAO,wBAAwB,EACxCpB,CAAA,EAEF,MAAO,CAAE,GAAID,IAAW,QAAa,CAAE,OAAAA,CAAA,EAAW,GAAGE,CAAA,EACrD,YAAaa,EACb,QAASC,EAER,SAAAb,CAAA,CAAA,EAEHS,CAAA,CAEJ"}
|
|
@@ -24,7 +24,7 @@ export interface ModalOverlayProps extends HTMLAttributes<HTMLDivElement> {
|
|
|
24
24
|
*/
|
|
25
25
|
lockScroll?: boolean;
|
|
26
26
|
/**
|
|
27
|
-
* Stacking order. Defaults to `--z-modal` (
|
|
27
|
+
* Stacking order. Defaults to `--z-modal` (1050, the webapp's modal layer); a host whose chrome sits
|
|
28
28
|
* higher passes this or redefines `--z-modal`. Applied before `style`, so
|
|
29
29
|
* `style.zIndex` wins.
|
|
30
30
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ModalOverlay.js","sources":["../../../src/components/modal/ModalOverlay.tsx"],"sourcesContent":["import {\n useEffect,\n useRef,\n type HTMLAttributes,\n type MouseEvent,\n type ReactNode,\n} from 'react';\nimport { createPortal } from 'react-dom';\nimport { usePopoverTransition } from '@/hooks/use-popover-transition';\nimport { mergeClassNames } from '@/utils/props';\nimport { lockBodyScroll } from '@/utils/scrollLock';\nimport { useOnExited } from '@/utils/useOnExited';\nimport styles from './ModalOverlay.module.scss';\n\nexport interface ModalOverlayProps extends HTMLAttributes<HTMLDivElement> {\n /** Whether the overlay is shown. Setting it to `false` plays the exit and then unmounts. */\n open: boolean;\n /** Called when the backdrop is pressed. Presses that start inside the dialog do not call it. */\n onClose?: () => void;\n /**\n * Called once the exit finishes and the overlay has unmounted. Not called if\n * the overlay itself is unmounted before the exit finishes, so do any\n * cleanup on unmount as well.\n */\n onExited?: () => void;\n /**\n * Portal node. Defaults to `document.body`; pass another element so a modal\n * inside another top layer can join it. `null` renders nothing until the node\n * exists (e.g. a ref held in state). Override `position` through `style`\n * when the container is a positioned stage. Scroll lock applies to that\n * node's document.\n */\n container?: HTMLElement | null;\n /**\n * Lock `document.body` scroll while the overlay is mounted. Turn off for an\n * overlay staged inside a frame (docs, Storybook) so it doesn't freeze the page.\n */\n lockScroll?: boolean;\n /**\n * Stacking order. Defaults to `--z-modal` (
|
|
1
|
+
{"version":3,"file":"ModalOverlay.js","sources":["../../../src/components/modal/ModalOverlay.tsx"],"sourcesContent":["import {\n useEffect,\n useRef,\n type HTMLAttributes,\n type MouseEvent,\n type ReactNode,\n} from 'react';\nimport { createPortal } from 'react-dom';\nimport { usePopoverTransition } from '@/hooks/use-popover-transition';\nimport { mergeClassNames } from '@/utils/props';\nimport { lockBodyScroll } from '@/utils/scrollLock';\nimport { useOnExited } from '@/utils/useOnExited';\nimport styles from './ModalOverlay.module.scss';\n\nexport interface ModalOverlayProps extends HTMLAttributes<HTMLDivElement> {\n /** Whether the overlay is shown. Setting it to `false` plays the exit and then unmounts. */\n open: boolean;\n /** Called when the backdrop is pressed. Presses that start inside the dialog do not call it. */\n onClose?: () => void;\n /**\n * Called once the exit finishes and the overlay has unmounted. Not called if\n * the overlay itself is unmounted before the exit finishes, so do any\n * cleanup on unmount as well.\n */\n onExited?: () => void;\n /**\n * Portal node. Defaults to `document.body`; pass another element so a modal\n * inside another top layer can join it. `null` renders nothing until the node\n * exists (e.g. a ref held in state). Override `position` through `style`\n * when the container is a positioned stage. Scroll lock applies to that\n * node's document.\n */\n container?: HTMLElement | null;\n /**\n * Lock `document.body` scroll while the overlay is mounted. Turn off for an\n * overlay staged inside a frame (docs, Storybook) so it doesn't freeze the page.\n */\n lockScroll?: boolean;\n /**\n * Stacking order. Defaults to `--z-modal` (1050, the webapp's modal layer); a host whose chrome sits\n * higher passes this or redefines `--z-modal`. Applied before `style`, so\n * `style.zIndex` wins.\n */\n zIndex?: number;\n /** Usually a `Modal`. */\n children?: ReactNode;\n}\n\n/**\n * Modal scrim: portals to `document.body` (or `container`), locks body scroll\n * while shown (opt out with `lockScroll={false}`), closes on backdrop press, and stays mounted for\n * `--duration-quick` after `open` becomes false so the fade-out can play.\n * Escape and focus stay on `Modal`. Hosts that already position their own\n * node can use `modalOverlayStyles` instead.\n */\nexport default function ModalOverlay({\n open,\n onClose,\n onExited,\n container,\n lockScroll = true,\n zIndex,\n className,\n style,\n children,\n onMouseDown,\n onClick,\n ...rest\n}: ModalOverlayProps) {\n const { mounted } = usePopoverTransition(open);\n useOnExited(mounted, onExited);\n const pressStartedInside = useRef(false);\n\n const target =\n container === undefined\n ? typeof document === 'undefined'\n ? null\n : document.body\n : container;\n\n useEffect(() => {\n if (!mounted || !target || !lockScroll) return undefined;\n return lockBodyScroll(target.ownerDocument);\n }, [mounted, target, lockScroll]);\n\n if (!mounted || !target) return null;\n\n const handleMouseDown = (e: MouseEvent<HTMLDivElement>) => {\n pressStartedInside.current = e.target !== e.currentTarget;\n onMouseDown?.(e);\n };\n\n const handleClick = (e: MouseEvent<HTMLDivElement>) => {\n const startedInside = pressStartedInside.current;\n pressStartedInside.current = false;\n onClick?.(e);\n if (open && !startedInside && e.target === e.currentTarget) onClose?.();\n };\n\n return createPortal(\n // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions -- backdrop click is a pointer shortcut; keyboard users close with Escape (handled by Modal)\n <div\n {...rest}\n className={mergeClassNames(\n styles['modal-overlay'],\n !open && styles['modal-overlay--exiting'],\n className,\n )}\n style={{ ...(zIndex !== undefined && { zIndex }), ...style }}\n onMouseDown={handleMouseDown}\n onClick={handleClick}\n >\n {children}\n </div>,\n target,\n );\n}\n"],"names":["ModalOverlay","open","onClose","onExited","container","lockScroll","zIndex","className","style","children","onMouseDown","onClick","rest","mounted","usePopoverTransition","useOnExited","pressStartedInside","useRef","target","useEffect","lockBodyScroll","handleMouseDown","e","handleClick","startedInside","createPortal","jsx","mergeClassNames","styles"],"mappings":";;;;;;;;AAuDA,SAAwBA,EAAa;AAAA,EACnC,MAAAC;AAAA,EACA,SAAAC;AAAA,EACA,UAAAC;AAAA,EACA,WAAAC;AAAA,EACA,YAAAC,IAAa;AAAA,EACb,QAAAC;AAAA,EACA,WAAAC;AAAA,EACA,OAAAC;AAAA,EACA,UAAAC;AAAA,EACA,aAAAC;AAAA,EACA,SAAAC;AAAA,EACA,GAAGC;AACL,GAAsB;AACpB,QAAM,EAAE,SAAAC,EAAA,IAAYC,EAAqBb,CAAI;AAC7C,EAAAc,EAAYF,GAASV,CAAQ;AAC7B,QAAMa,IAAqBC,EAAO,EAAK,GAEjCC,IACJd,MAAc,SACV,OAAO,WAAa,MAClB,OACA,SAAS,OACXA;AAON,MALAe,EAAU,MAAM;AACd,QAAI,GAACN,KAAW,CAACK,KAAU,CAACb;AAC5B,aAAOe,EAAeF,EAAO,aAAa;AAAA,EAC5C,GAAG,CAACL,GAASK,GAAQb,CAAU,CAAC,GAE5B,CAACQ,KAAW,CAACK,EAAQ,QAAO;AAEhC,QAAMG,IAAkB,CAACC,MAAkC;AACzD,IAAAN,EAAmB,UAAUM,EAAE,WAAWA,EAAE,eAC5CZ,KAAA,QAAAA,EAAcY;AAAA,EAChB,GAEMC,IAAc,CAACD,MAAkC;AACrD,UAAME,IAAgBR,EAAmB;AACzC,IAAAA,EAAmB,UAAU,IAC7BL,KAAA,QAAAA,EAAUW,IACNrB,KAAQ,CAACuB,KAAiBF,EAAE,WAAWA,EAAE,kBAAepB,KAAA,QAAAA;AAAA,EAC9D;AAEA,SAAOuB;AAAA;AAAA,IAEL,gBAAAC;AAAA,MAAC;AAAA,MAAA;AAAA,QACE,GAAGd;AAAA,QACJ,WAAWe;AAAA,UACTC,EAAO,eAAe;AAAA,UACtB,CAAC3B,KAAQ2B,EAAO,wBAAwB;AAAA,UACxCrB;AAAA,QAAA;AAAA,QAEF,OAAO,EAAE,GAAID,MAAW,UAAa,EAAE,QAAAA,EAAA,GAAW,GAAGE,EAAA;AAAA,QACrD,aAAaa;AAAA,QACb,SAASE;AAAA,QAER,UAAAd;AAAA,MAAA;AAAA,IAAA;AAAA,IAEHS;AAAA,EAAA;AAEJ;"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-anchored-popup-portal.cjs","sources":["../../src/hooks/use-anchored-popup-portal.ts"],"sourcesContent":["import {\n useCallback,\n useLayoutEffect,\n useRef,\n useState,\n type CSSProperties,\n type ReactNode,\n type RefObject,\n} from 'react';\nimport { createPortal } from 'react-dom';\nimport {\n ANCHORED_POPUP_GAP,\n ANCHORED_POPUP_Z_INDEX,\n computeAnchoredPopupFixedStyle,\n computeAnchoredPopupPlacement,\n getAnchoredPopupContainerFrame,\n type PopupPlacement,\n} from '@/utils/anchoredPopupPlacement';\n\nexport interface UseAnchoredPopupPortalOptions {\n /** Height used for the flip decision when content isn’t measured yet. */\n preferredHeight: number;\n /** Cap for scrollable menus (Select/Combobox). Omit for intrinsic panels. */\n maxHeightCap?: number;\n /** Ref to the portaled panel for measuring intrinsic height (DateRangePicker). */\n contentRef?: RefObject<HTMLElement | null>;\n /**\n * Portal mount node; defaults to `document.body` with viewport-fixed coords.\n * Custom containers must establish a positioning context (`position` other\n * than `static`); coords are converted to that container’s space.\n */\n portalContainer?: HTMLElement | null;\n zIndex?: number;\n /**\n * When true (default), fixed width matches the trigger.\n * DateRangePicker sets false and keeps its CSS width.\n */\n matchWidth?: boolean;\n gap?: number;\n}\n\nexport interface UseAnchoredPopupPortalResult {\n placement: PopupPlacement;\n maxHeight: number;\n style: CSSProperties;\n /** Ref for the portaled surface — pass to `useOutsideClose` as a second root. */\n portalRef: RefObject<HTMLDivElement | null>;\n /** Wraps children in a portal to `portalContainer` or `document.body`. */\n renderPortal: (children: ReactNode) => ReactNode;\n}\n\nconst DEFAULT_PLACEMENT: PopupPlacement = 'below';\n\nfunction resolvePortalMount(\n portalContainer: HTMLElement | null | undefined,\n): HTMLElement | null {\n if (typeof document === 'undefined') return null;\n return portalContainer ?? document.body;\n}\n\nfunction usesViewportFixedCoords(mount: HTMLElement): boolean {\n return mount === document.body;\n}\n\n/**\n * Positions a form-widget popup in a portal with viewport-aware above/below flip.\n * Internal to compass-ui form widgets — not a general overlay positioning API.\n */\nexport function useAnchoredPopupPortal(\n anchorRef: RefObject<HTMLElement | null>,\n isOpen: boolean,\n options: UseAnchoredPopupPortalOptions,\n): UseAnchoredPopupPortalResult {\n const {\n preferredHeight,\n maxHeightCap,\n contentRef,\n portalContainer = null,\n zIndex = ANCHORED_POPUP_Z_INDEX,\n matchWidth = true,\n gap = ANCHORED_POPUP_GAP,\n } = options;\n\n const portalRef = useRef<HTMLDivElement | null>(null);\n const [placement, setPlacement] = useState<PopupPlacement>(DEFAULT_PLACEMENT);\n const [maxHeight, setMaxHeight] = useState(maxHeightCap ?? preferredHeight);\n const [style, setStyle] = useState<CSSProperties>({\n position: 'fixed',\n top: 0,\n left: 0,\n width: matchWidth ? 0 : undefined,\n zIndex,\n });\n\n const update = useCallback(() => {\n const anchor = anchorRef.current;\n if (!anchor) return;\n\n const mount = resolvePortalMount(portalContainer);\n if (!mount) return;\n\n const rect = anchor.getBoundingClientRect();\n const viewportHeight = window.innerHeight;\n const viewportFixed = usesViewportFixedCoords(mount);\n const containerFrame = viewportFixed\n ? undefined\n : getAnchoredPopupContainerFrame(mount);\n\n const measured =\n contentRef?.current?.offsetHeight ?? portalRef.current?.offsetHeight ?? 0;\n const heightForFlip = measured > 0 ? measured : preferredHeight;\n\n const { placement: nextPlacement, maxHeight: nextMaxHeight } =\n computeAnchoredPopupPlacement(rect, viewportHeight, {\n gap,\n preferredHeight: heightForFlip,\n maxHeightCap,\n bounds: containerFrame?.bounds,\n });\n\n const fixed = computeAnchoredPopupFixedStyle(rect, nextPlacement, {\n gap,\n zIndex,\n width: matchWidth ? rect.width : undefined,\n viewportHeight,\n containerFrame,\n });\n\n setPlacement(nextPlacement);\n setMaxHeight(nextMaxHeight);\n setStyle({\n position: fixed.position,\n left: fixed.left,\n zIndex: fixed.zIndex,\n ...(matchWidth ? { width: fixed.width } : null),\n ...(nextPlacement === 'below'\n ? { top: fixed.top, bottom: 'auto' }\n : { bottom: fixed.bottom, top: 'auto' }),\n });\n }, [\n anchorRef,\n contentRef,\n gap,\n matchWidth,\n maxHeightCap,\n portalContainer,\n preferredHeight,\n zIndex,\n ]);\n\n useLayoutEffect(() => {\n if (!isOpen) return;\n\n update();\n\n function handle() {\n update();\n }\n\n window.addEventListener('resize', handle);\n // Capture phase so nested scroll containers (modals, RHS) are covered.\n document.addEventListener('scroll', handle, true);\n return () => {\n window.removeEventListener('resize', handle);\n document.removeEventListener('scroll', handle, true);\n };\n }, [isOpen, update]);\n\n // Re-measure after content mounts / changes size (calendar, long lists).\n useLayoutEffect(() => {\n if (!isOpen) return;\n const el = contentRef?.current ?? portalRef.current;\n if (!el || typeof ResizeObserver === 'undefined') return;\n\n const ro = new ResizeObserver(() => update());\n ro.observe(el);\n return () => ro.disconnect();\n }, [isOpen, contentRef, update]);\n\n const renderPortal = useCallback(\n (children: ReactNode) => {\n const mount = resolvePortalMount(portalContainer);\n if (!mount) return null;\n return createPortal(children, mount);\n },\n [portalContainer],\n );\n\n return {\n placement,\n maxHeight,\n style,\n portalRef,\n renderPortal,\n };\n}\n"],"names":["DEFAULT_PLACEMENT","resolvePortalMount","portalContainer","usesViewportFixedCoords","mount","useAnchoredPopupPortal","anchorRef","isOpen","options","preferredHeight","maxHeightCap","contentRef","zIndex","ANCHORED_POPUP_Z_INDEX","matchWidth","gap","ANCHORED_POPUP_GAP","portalRef","useRef","placement","setPlacement","useState","maxHeight","setMaxHeight","style","setStyle","update","useCallback","anchor","rect","viewportHeight","containerFrame","getAnchoredPopupContainerFrame","measured","_a","_b","heightForFlip","nextPlacement","nextMaxHeight","computeAnchoredPopupPlacement","fixed","computeAnchoredPopupFixedStyle","useLayoutEffect","handle","el","ro","renderPortal","children","createPortal"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"use-anchored-popup-portal.cjs","sources":["../../src/hooks/use-anchored-popup-portal.ts"],"sourcesContent":["import {\n useCallback,\n useLayoutEffect,\n useRef,\n useState,\n type CSSProperties,\n type ReactNode,\n type RefObject,\n} from 'react';\nimport { createPortal } from 'react-dom';\nimport {\n ANCHORED_POPUP_GAP,\n ANCHORED_POPUP_Z_INDEX,\n computeAnchoredPopupFixedStyle,\n computeAnchoredPopupPlacement,\n getAnchoredPopupContainerFrame,\n type PopupPlacement,\n} from '@/utils/anchoredPopupPlacement';\n\nexport interface UseAnchoredPopupPortalOptions {\n /** Height used for the flip decision when content isn’t measured yet. */\n preferredHeight: number;\n /** Cap for scrollable menus (Select/Combobox). Omit for intrinsic panels. */\n maxHeightCap?: number;\n /** Ref to the portaled panel for measuring intrinsic height (DateRangePicker). */\n contentRef?: RefObject<HTMLElement | null>;\n /**\n * Portal mount node; defaults to `document.body` with viewport-fixed coords.\n * Custom containers must establish a positioning context (`position` other\n * than `static`); coords are converted to that container’s space.\n */\n portalContainer?: HTMLElement | null;\n zIndex?: number | string;\n /**\n * When true (default), fixed width matches the trigger.\n * DateRangePicker sets false and keeps its CSS width.\n */\n matchWidth?: boolean;\n gap?: number;\n}\n\nexport interface UseAnchoredPopupPortalResult {\n placement: PopupPlacement;\n maxHeight: number;\n style: CSSProperties;\n /** Ref for the portaled surface — pass to `useOutsideClose` as a second root. */\n portalRef: RefObject<HTMLDivElement | null>;\n /** Wraps children in a portal to `portalContainer` or `document.body`. */\n renderPortal: (children: ReactNode) => ReactNode;\n}\n\nconst DEFAULT_PLACEMENT: PopupPlacement = 'below';\n\nfunction resolvePortalMount(\n portalContainer: HTMLElement | null | undefined,\n): HTMLElement | null {\n if (typeof document === 'undefined') return null;\n return portalContainer ?? document.body;\n}\n\nfunction usesViewportFixedCoords(mount: HTMLElement): boolean {\n return mount === document.body;\n}\n\n/**\n * Positions a form-widget popup in a portal with viewport-aware above/below flip.\n * Internal to compass-ui form widgets — not a general overlay positioning API.\n */\nexport function useAnchoredPopupPortal(\n anchorRef: RefObject<HTMLElement | null>,\n isOpen: boolean,\n options: UseAnchoredPopupPortalOptions,\n): UseAnchoredPopupPortalResult {\n const {\n preferredHeight,\n maxHeightCap,\n contentRef,\n portalContainer = null,\n zIndex = ANCHORED_POPUP_Z_INDEX,\n matchWidth = true,\n gap = ANCHORED_POPUP_GAP,\n } = options;\n\n const portalRef = useRef<HTMLDivElement | null>(null);\n const [placement, setPlacement] = useState<PopupPlacement>(DEFAULT_PLACEMENT);\n const [maxHeight, setMaxHeight] = useState(maxHeightCap ?? preferredHeight);\n const [style, setStyle] = useState<CSSProperties>({\n position: 'fixed',\n top: 0,\n left: 0,\n width: matchWidth ? 0 : undefined,\n zIndex,\n });\n\n const update = useCallback(() => {\n const anchor = anchorRef.current;\n if (!anchor) return;\n\n const mount = resolvePortalMount(portalContainer);\n if (!mount) return;\n\n const rect = anchor.getBoundingClientRect();\n const viewportHeight = window.innerHeight;\n const viewportFixed = usesViewportFixedCoords(mount);\n const containerFrame = viewportFixed\n ? undefined\n : getAnchoredPopupContainerFrame(mount);\n\n const measured =\n contentRef?.current?.offsetHeight ?? portalRef.current?.offsetHeight ?? 0;\n const heightForFlip = measured > 0 ? measured : preferredHeight;\n\n const { placement: nextPlacement, maxHeight: nextMaxHeight } =\n computeAnchoredPopupPlacement(rect, viewportHeight, {\n gap,\n preferredHeight: heightForFlip,\n maxHeightCap,\n bounds: containerFrame?.bounds,\n });\n\n const fixed = computeAnchoredPopupFixedStyle(rect, nextPlacement, {\n gap,\n zIndex,\n width: matchWidth ? rect.width : undefined,\n viewportHeight,\n containerFrame,\n });\n\n setPlacement(nextPlacement);\n setMaxHeight(nextMaxHeight);\n setStyle({\n position: fixed.position,\n left: fixed.left,\n zIndex: fixed.zIndex,\n ...(matchWidth ? { width: fixed.width } : null),\n ...(nextPlacement === 'below'\n ? { top: fixed.top, bottom: 'auto' }\n : { bottom: fixed.bottom, top: 'auto' }),\n });\n }, [\n anchorRef,\n contentRef,\n gap,\n matchWidth,\n maxHeightCap,\n portalContainer,\n preferredHeight,\n zIndex,\n ]);\n\n useLayoutEffect(() => {\n if (!isOpen) return;\n\n update();\n\n function handle() {\n update();\n }\n\n window.addEventListener('resize', handle);\n // Capture phase so nested scroll containers (modals, RHS) are covered.\n document.addEventListener('scroll', handle, true);\n return () => {\n window.removeEventListener('resize', handle);\n document.removeEventListener('scroll', handle, true);\n };\n }, [isOpen, update]);\n\n // Re-measure after content mounts / changes size (calendar, long lists).\n useLayoutEffect(() => {\n if (!isOpen) return;\n const el = contentRef?.current ?? portalRef.current;\n if (!el || typeof ResizeObserver === 'undefined') return;\n\n const ro = new ResizeObserver(() => update());\n ro.observe(el);\n return () => ro.disconnect();\n }, [isOpen, contentRef, update]);\n\n const renderPortal = useCallback(\n (children: ReactNode) => {\n const mount = resolvePortalMount(portalContainer);\n if (!mount) return null;\n return createPortal(children, mount);\n },\n [portalContainer],\n );\n\n return {\n placement,\n maxHeight,\n style,\n portalRef,\n renderPortal,\n };\n}\n"],"names":["DEFAULT_PLACEMENT","resolvePortalMount","portalContainer","usesViewportFixedCoords","mount","useAnchoredPopupPortal","anchorRef","isOpen","options","preferredHeight","maxHeightCap","contentRef","zIndex","ANCHORED_POPUP_Z_INDEX","matchWidth","gap","ANCHORED_POPUP_GAP","portalRef","useRef","placement","setPlacement","useState","maxHeight","setMaxHeight","style","setStyle","update","useCallback","anchor","rect","viewportHeight","containerFrame","getAnchoredPopupContainerFrame","measured","_a","_b","heightForFlip","nextPlacement","nextMaxHeight","computeAnchoredPopupPlacement","fixed","computeAnchoredPopupFixedStyle","useLayoutEffect","handle","el","ro","renderPortal","children","createPortal"],"mappings":"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"}
|
|
@@ -13,7 +13,7 @@ export interface UseAnchoredPopupPortalOptions {
|
|
|
13
13
|
* than `static`); coords are converted to that container’s space.
|
|
14
14
|
*/
|
|
15
15
|
portalContainer?: HTMLElement | null;
|
|
16
|
-
zIndex?: number;
|
|
16
|
+
zIndex?: number | string;
|
|
17
17
|
/**
|
|
18
18
|
* When true (default), fixed width matches the trigger.
|
|
19
19
|
* DateRangePicker sets false and keeps its CSS width.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-anchored-popup-portal.d.ts","sourceRoot":"","sources":["../../src/hooks/use-anchored-popup-portal.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,aAAa,EAClB,KAAK,SAAS,EACd,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAEf,OAAO,EAML,KAAK,cAAc,EACpB,MAAM,gCAAgC,CAAC;AAExC,MAAM,WAAW,6BAA6B;IAC5C,yEAAyE;IACzE,eAAe,EAAE,MAAM,CAAC;IACxB,6EAA6E;IAC7E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,kFAAkF;IAClF,UAAU,CAAC,EAAE,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAC3C;;;;OAIG;IACH,eAAe,CAAC,EAAE,WAAW,GAAG,IAAI,CAAC;IACrC,MAAM,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"use-anchored-popup-portal.d.ts","sourceRoot":"","sources":["../../src/hooks/use-anchored-popup-portal.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,aAAa,EAClB,KAAK,SAAS,EACd,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAEf,OAAO,EAML,KAAK,cAAc,EACpB,MAAM,gCAAgC,CAAC;AAExC,MAAM,WAAW,6BAA6B;IAC5C,yEAAyE;IACzE,eAAe,EAAE,MAAM,CAAC;IACxB,6EAA6E;IAC7E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,kFAAkF;IAClF,UAAU,CAAC,EAAE,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAC3C;;;;OAIG;IACH,eAAe,CAAC,EAAE,WAAW,GAAG,IAAI,CAAC;IACrC,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACzB;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,4BAA4B;IAC3C,SAAS,EAAE,cAAc,CAAC;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,aAAa,CAAC;IACrB,iFAAiF;IACjF,SAAS,EAAE,SAAS,CAAC,cAAc,GAAG,IAAI,CAAC,CAAC;IAC5C,0EAA0E;IAC1E,YAAY,EAAE,CAAC,QAAQ,EAAE,SAAS,KAAK,SAAS,CAAC;CAClD;AAeD;;;GAGG;AACH,wBAAgB,sBAAsB,CACpC,SAAS,EAAE,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,EACxC,MAAM,EAAE,OAAO,EACf,OAAO,EAAE,6BAA6B,GACrC,4BAA4B,CA2H9B"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-anchored-popup-portal.js","sources":["../../src/hooks/use-anchored-popup-portal.ts"],"sourcesContent":["import {\n useCallback,\n useLayoutEffect,\n useRef,\n useState,\n type CSSProperties,\n type ReactNode,\n type RefObject,\n} from 'react';\nimport { createPortal } from 'react-dom';\nimport {\n ANCHORED_POPUP_GAP,\n ANCHORED_POPUP_Z_INDEX,\n computeAnchoredPopupFixedStyle,\n computeAnchoredPopupPlacement,\n getAnchoredPopupContainerFrame,\n type PopupPlacement,\n} from '@/utils/anchoredPopupPlacement';\n\nexport interface UseAnchoredPopupPortalOptions {\n /** Height used for the flip decision when content isn’t measured yet. */\n preferredHeight: number;\n /** Cap for scrollable menus (Select/Combobox). Omit for intrinsic panels. */\n maxHeightCap?: number;\n /** Ref to the portaled panel for measuring intrinsic height (DateRangePicker). */\n contentRef?: RefObject<HTMLElement | null>;\n /**\n * Portal mount node; defaults to `document.body` with viewport-fixed coords.\n * Custom containers must establish a positioning context (`position` other\n * than `static`); coords are converted to that container’s space.\n */\n portalContainer?: HTMLElement | null;\n zIndex?: number;\n /**\n * When true (default), fixed width matches the trigger.\n * DateRangePicker sets false and keeps its CSS width.\n */\n matchWidth?: boolean;\n gap?: number;\n}\n\nexport interface UseAnchoredPopupPortalResult {\n placement: PopupPlacement;\n maxHeight: number;\n style: CSSProperties;\n /** Ref for the portaled surface — pass to `useOutsideClose` as a second root. */\n portalRef: RefObject<HTMLDivElement | null>;\n /** Wraps children in a portal to `portalContainer` or `document.body`. */\n renderPortal: (children: ReactNode) => ReactNode;\n}\n\nconst DEFAULT_PLACEMENT: PopupPlacement = 'below';\n\nfunction resolvePortalMount(\n portalContainer: HTMLElement | null | undefined,\n): HTMLElement | null {\n if (typeof document === 'undefined') return null;\n return portalContainer ?? document.body;\n}\n\nfunction usesViewportFixedCoords(mount: HTMLElement): boolean {\n return mount === document.body;\n}\n\n/**\n * Positions a form-widget popup in a portal with viewport-aware above/below flip.\n * Internal to compass-ui form widgets — not a general overlay positioning API.\n */\nexport function useAnchoredPopupPortal(\n anchorRef: RefObject<HTMLElement | null>,\n isOpen: boolean,\n options: UseAnchoredPopupPortalOptions,\n): UseAnchoredPopupPortalResult {\n const {\n preferredHeight,\n maxHeightCap,\n contentRef,\n portalContainer = null,\n zIndex = ANCHORED_POPUP_Z_INDEX,\n matchWidth = true,\n gap = ANCHORED_POPUP_GAP,\n } = options;\n\n const portalRef = useRef<HTMLDivElement | null>(null);\n const [placement, setPlacement] = useState<PopupPlacement>(DEFAULT_PLACEMENT);\n const [maxHeight, setMaxHeight] = useState(maxHeightCap ?? preferredHeight);\n const [style, setStyle] = useState<CSSProperties>({\n position: 'fixed',\n top: 0,\n left: 0,\n width: matchWidth ? 0 : undefined,\n zIndex,\n });\n\n const update = useCallback(() => {\n const anchor = anchorRef.current;\n if (!anchor) return;\n\n const mount = resolvePortalMount(portalContainer);\n if (!mount) return;\n\n const rect = anchor.getBoundingClientRect();\n const viewportHeight = window.innerHeight;\n const viewportFixed = usesViewportFixedCoords(mount);\n const containerFrame = viewportFixed\n ? undefined\n : getAnchoredPopupContainerFrame(mount);\n\n const measured =\n contentRef?.current?.offsetHeight ?? portalRef.current?.offsetHeight ?? 0;\n const heightForFlip = measured > 0 ? measured : preferredHeight;\n\n const { placement: nextPlacement, maxHeight: nextMaxHeight } =\n computeAnchoredPopupPlacement(rect, viewportHeight, {\n gap,\n preferredHeight: heightForFlip,\n maxHeightCap,\n bounds: containerFrame?.bounds,\n });\n\n const fixed = computeAnchoredPopupFixedStyle(rect, nextPlacement, {\n gap,\n zIndex,\n width: matchWidth ? rect.width : undefined,\n viewportHeight,\n containerFrame,\n });\n\n setPlacement(nextPlacement);\n setMaxHeight(nextMaxHeight);\n setStyle({\n position: fixed.position,\n left: fixed.left,\n zIndex: fixed.zIndex,\n ...(matchWidth ? { width: fixed.width } : null),\n ...(nextPlacement === 'below'\n ? { top: fixed.top, bottom: 'auto' }\n : { bottom: fixed.bottom, top: 'auto' }),\n });\n }, [\n anchorRef,\n contentRef,\n gap,\n matchWidth,\n maxHeightCap,\n portalContainer,\n preferredHeight,\n zIndex,\n ]);\n\n useLayoutEffect(() => {\n if (!isOpen) return;\n\n update();\n\n function handle() {\n update();\n }\n\n window.addEventListener('resize', handle);\n // Capture phase so nested scroll containers (modals, RHS) are covered.\n document.addEventListener('scroll', handle, true);\n return () => {\n window.removeEventListener('resize', handle);\n document.removeEventListener('scroll', handle, true);\n };\n }, [isOpen, update]);\n\n // Re-measure after content mounts / changes size (calendar, long lists).\n useLayoutEffect(() => {\n if (!isOpen) return;\n const el = contentRef?.current ?? portalRef.current;\n if (!el || typeof ResizeObserver === 'undefined') return;\n\n const ro = new ResizeObserver(() => update());\n ro.observe(el);\n return () => ro.disconnect();\n }, [isOpen, contentRef, update]);\n\n const renderPortal = useCallback(\n (children: ReactNode) => {\n const mount = resolvePortalMount(portalContainer);\n if (!mount) return null;\n return createPortal(children, mount);\n },\n [portalContainer],\n );\n\n return {\n placement,\n maxHeight,\n style,\n portalRef,\n renderPortal,\n };\n}\n"],"names":["DEFAULT_PLACEMENT","resolvePortalMount","portalContainer","usesViewportFixedCoords","mount","useAnchoredPopupPortal","anchorRef","isOpen","options","preferredHeight","maxHeightCap","contentRef","zIndex","ANCHORED_POPUP_Z_INDEX","matchWidth","gap","ANCHORED_POPUP_GAP","portalRef","useRef","placement","setPlacement","useState","maxHeight","setMaxHeight","style","setStyle","update","useCallback","anchor","rect","viewportHeight","containerFrame","getAnchoredPopupContainerFrame","measured","_a","_b","heightForFlip","nextPlacement","nextMaxHeight","computeAnchoredPopupPlacement","fixed","computeAnchoredPopupFixedStyle","useLayoutEffect","handle","el","ro","renderPortal","children","createPortal"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"use-anchored-popup-portal.js","sources":["../../src/hooks/use-anchored-popup-portal.ts"],"sourcesContent":["import {\n useCallback,\n useLayoutEffect,\n useRef,\n useState,\n type CSSProperties,\n type ReactNode,\n type RefObject,\n} from 'react';\nimport { createPortal } from 'react-dom';\nimport {\n ANCHORED_POPUP_GAP,\n ANCHORED_POPUP_Z_INDEX,\n computeAnchoredPopupFixedStyle,\n computeAnchoredPopupPlacement,\n getAnchoredPopupContainerFrame,\n type PopupPlacement,\n} from '@/utils/anchoredPopupPlacement';\n\nexport interface UseAnchoredPopupPortalOptions {\n /** Height used for the flip decision when content isn’t measured yet. */\n preferredHeight: number;\n /** Cap for scrollable menus (Select/Combobox). Omit for intrinsic panels. */\n maxHeightCap?: number;\n /** Ref to the portaled panel for measuring intrinsic height (DateRangePicker). */\n contentRef?: RefObject<HTMLElement | null>;\n /**\n * Portal mount node; defaults to `document.body` with viewport-fixed coords.\n * Custom containers must establish a positioning context (`position` other\n * than `static`); coords are converted to that container’s space.\n */\n portalContainer?: HTMLElement | null;\n zIndex?: number | string;\n /**\n * When true (default), fixed width matches the trigger.\n * DateRangePicker sets false and keeps its CSS width.\n */\n matchWidth?: boolean;\n gap?: number;\n}\n\nexport interface UseAnchoredPopupPortalResult {\n placement: PopupPlacement;\n maxHeight: number;\n style: CSSProperties;\n /** Ref for the portaled surface — pass to `useOutsideClose` as a second root. */\n portalRef: RefObject<HTMLDivElement | null>;\n /** Wraps children in a portal to `portalContainer` or `document.body`. */\n renderPortal: (children: ReactNode) => ReactNode;\n}\n\nconst DEFAULT_PLACEMENT: PopupPlacement = 'below';\n\nfunction resolvePortalMount(\n portalContainer: HTMLElement | null | undefined,\n): HTMLElement | null {\n if (typeof document === 'undefined') return null;\n return portalContainer ?? document.body;\n}\n\nfunction usesViewportFixedCoords(mount: HTMLElement): boolean {\n return mount === document.body;\n}\n\n/**\n * Positions a form-widget popup in a portal with viewport-aware above/below flip.\n * Internal to compass-ui form widgets — not a general overlay positioning API.\n */\nexport function useAnchoredPopupPortal(\n anchorRef: RefObject<HTMLElement | null>,\n isOpen: boolean,\n options: UseAnchoredPopupPortalOptions,\n): UseAnchoredPopupPortalResult {\n const {\n preferredHeight,\n maxHeightCap,\n contentRef,\n portalContainer = null,\n zIndex = ANCHORED_POPUP_Z_INDEX,\n matchWidth = true,\n gap = ANCHORED_POPUP_GAP,\n } = options;\n\n const portalRef = useRef<HTMLDivElement | null>(null);\n const [placement, setPlacement] = useState<PopupPlacement>(DEFAULT_PLACEMENT);\n const [maxHeight, setMaxHeight] = useState(maxHeightCap ?? preferredHeight);\n const [style, setStyle] = useState<CSSProperties>({\n position: 'fixed',\n top: 0,\n left: 0,\n width: matchWidth ? 0 : undefined,\n zIndex,\n });\n\n const update = useCallback(() => {\n const anchor = anchorRef.current;\n if (!anchor) return;\n\n const mount = resolvePortalMount(portalContainer);\n if (!mount) return;\n\n const rect = anchor.getBoundingClientRect();\n const viewportHeight = window.innerHeight;\n const viewportFixed = usesViewportFixedCoords(mount);\n const containerFrame = viewportFixed\n ? undefined\n : getAnchoredPopupContainerFrame(mount);\n\n const measured =\n contentRef?.current?.offsetHeight ?? portalRef.current?.offsetHeight ?? 0;\n const heightForFlip = measured > 0 ? measured : preferredHeight;\n\n const { placement: nextPlacement, maxHeight: nextMaxHeight } =\n computeAnchoredPopupPlacement(rect, viewportHeight, {\n gap,\n preferredHeight: heightForFlip,\n maxHeightCap,\n bounds: containerFrame?.bounds,\n });\n\n const fixed = computeAnchoredPopupFixedStyle(rect, nextPlacement, {\n gap,\n zIndex,\n width: matchWidth ? rect.width : undefined,\n viewportHeight,\n containerFrame,\n });\n\n setPlacement(nextPlacement);\n setMaxHeight(nextMaxHeight);\n setStyle({\n position: fixed.position,\n left: fixed.left,\n zIndex: fixed.zIndex,\n ...(matchWidth ? { width: fixed.width } : null),\n ...(nextPlacement === 'below'\n ? { top: fixed.top, bottom: 'auto' }\n : { bottom: fixed.bottom, top: 'auto' }),\n });\n }, [\n anchorRef,\n contentRef,\n gap,\n matchWidth,\n maxHeightCap,\n portalContainer,\n preferredHeight,\n zIndex,\n ]);\n\n useLayoutEffect(() => {\n if (!isOpen) return;\n\n update();\n\n function handle() {\n update();\n }\n\n window.addEventListener('resize', handle);\n // Capture phase so nested scroll containers (modals, RHS) are covered.\n document.addEventListener('scroll', handle, true);\n return () => {\n window.removeEventListener('resize', handle);\n document.removeEventListener('scroll', handle, true);\n };\n }, [isOpen, update]);\n\n // Re-measure after content mounts / changes size (calendar, long lists).\n useLayoutEffect(() => {\n if (!isOpen) return;\n const el = contentRef?.current ?? portalRef.current;\n if (!el || typeof ResizeObserver === 'undefined') return;\n\n const ro = new ResizeObserver(() => update());\n ro.observe(el);\n return () => ro.disconnect();\n }, [isOpen, contentRef, update]);\n\n const renderPortal = useCallback(\n (children: ReactNode) => {\n const mount = resolvePortalMount(portalContainer);\n if (!mount) return null;\n return createPortal(children, mount);\n },\n [portalContainer],\n );\n\n return {\n placement,\n maxHeight,\n style,\n portalRef,\n renderPortal,\n };\n}\n"],"names":["DEFAULT_PLACEMENT","resolvePortalMount","portalContainer","usesViewportFixedCoords","mount","useAnchoredPopupPortal","anchorRef","isOpen","options","preferredHeight","maxHeightCap","contentRef","zIndex","ANCHORED_POPUP_Z_INDEX","matchWidth","gap","ANCHORED_POPUP_GAP","portalRef","useRef","placement","setPlacement","useState","maxHeight","setMaxHeight","style","setStyle","update","useCallback","anchor","rect","viewportHeight","containerFrame","getAnchoredPopupContainerFrame","measured","_a","_b","heightForFlip","nextPlacement","nextMaxHeight","computeAnchoredPopupPlacement","fixed","computeAnchoredPopupFixedStyle","useLayoutEffect","handle","el","ro","renderPortal","children","createPortal"],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const s=4,f=48,h=120,u="var(--z-menu, 1100)";function _(t,n,e){var g,H;const i=e.gap??s,o=e.viewportMargin??f,l=e.minHeight??h,p=Math.max(e.preferredHeight,0),a=((g=e.bounds)==null?void 0:g.top)??0,c=((H=e.bounds)==null?void 0:H.bottom)??n,r=Math.max(0,c-t.bottom-i-o),P=Math.max(0,t.top-a-i-o),d=r<p&&P>r?"above":"below",b=d==="below"?r:P,m=e.maxHeightCap!=null?Math.min(e.maxHeightCap,b):b;return{placement:d,maxHeight:Math.max(l,m)}}function A(t){const n=t.getBoundingClientRect(),e=n.top+t.clientTop,i=e+t.clientHeight;return{bounds:{top:e,bottom:i},anchorInContainer:o=>({left:o.left-n.left-t.clientLeft+t.scrollLeft,top:o.top-n.top-t.clientTop+t.scrollTop,bottom:o.bottom-n.top-t.clientTop+t.scrollTop,width:o.width}),clientHeight:t.clientHeight,scrollTop:t.scrollTop}}function O(t,n,e){const i=e.gap??s,o=e.width??t.width,l=e.zIndex??u,p=e.containerFrame;if(p){const c=p.anchorInContainer(t);if(n==="below")return{position:"absolute",top:c.bottom+i,left:c.left,width:o,zIndex:l};const r=c.top-p.scrollTop;return{position:"absolute",bottom:p.clientHeight-r+i,left:c.left,width:o,zIndex:l}}const a=e.viewportHeight??window.innerHeight;return n==="below"?{position:"fixed",top:t.bottom+i,left:t.left,width:o,zIndex:l}:{position:"fixed",bottom:a-t.top+i,left:t.left,width:o,zIndex:l}}exports.ANCHORED_POPUP_GAP=s;exports.ANCHORED_POPUP_MIN_HEIGHT=h;exports.ANCHORED_POPUP_VIEWPORT_MARGIN=f;exports.ANCHORED_POPUP_Z_INDEX=u;exports.computeAnchoredPopupFixedStyle=O;exports.computeAnchoredPopupPlacement=_;exports.getAnchoredPopupContainerFrame=A;
|
|
2
2
|
//# sourceMappingURL=anchoredPopupPlacement.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"anchoredPopupPlacement.cjs","sources":["../../src/utils/anchoredPopupPlacement.ts"],"sourcesContent":["export type PopupPlacement = 'above' | 'below';\n\nexport const ANCHORED_POPUP_GAP = 4;\nexport const ANCHORED_POPUP_VIEWPORT_MARGIN = 48;\nexport const ANCHORED_POPUP_MIN_HEIGHT = 120;\n
|
|
1
|
+
{"version":3,"file":"anchoredPopupPlacement.cjs","sources":["../../src/utils/anchoredPopupPlacement.ts"],"sourcesContent":["export type PopupPlacement = 'above' | 'below';\n\nexport const ANCHORED_POPUP_GAP = 4;\nexport const ANCHORED_POPUP_VIEWPORT_MARGIN = 48;\nexport const ANCHORED_POPUP_MIN_HEIGHT = 120;\n/**\n * Default stacking for portaled menus: the `--z-menu` layer, which sits above\n * modals (`--z-modal`). The fallback keeps it working when tokens aren't loaded.\n * Hosts can override per use via `zIndex` or by redefining the token.\n */\nexport const ANCHORED_POPUP_Z_INDEX = 'var(--z-menu, 1100)';\n\nexport interface AnchoredPopupPlacementOptions {\n /** Gap between trigger and popup. Default: 4. */\n gap?: number;\n /** Reserved space at the viewport edge. Default: 48. */\n viewportMargin?: number;\n /** Height used to decide whether the popup fits below. */\n preferredHeight: number;\n /** Cap for scrollable menus (e.g. Select/Combobox 280). */\n maxHeightCap?: number;\n /** Floor so short viewports still show a scrollable list. Default: 120. */\n minHeight?: number;\n}\n\nexport interface AnchoredPopupPlacementResult {\n placement: PopupPlacement;\n maxHeight: number;\n}\n\n/**\n * Prefers opening below the anchor; flips above when there isn’t enough room\n * below and there is more space above. Max-height is the available space in the\n * chosen direction, optionally capped.\n */\nexport function computeAnchoredPopupPlacement(\n anchorRect: Pick<DOMRect, 'top' | 'bottom'>,\n viewportHeight: number,\n options: AnchoredPopupPlacementOptions & {\n /** When set, flip/max-height use this box instead of the viewport. */\n bounds?: Pick<DOMRect, 'top' | 'bottom'>;\n },\n): AnchoredPopupPlacementResult {\n const gap = options.gap ?? ANCHORED_POPUP_GAP;\n const viewportMargin =\n options.viewportMargin ?? ANCHORED_POPUP_VIEWPORT_MARGIN;\n const minHeight = options.minHeight ?? ANCHORED_POPUP_MIN_HEIGHT;\n const preferredHeight = Math.max(options.preferredHeight, 0);\n\n const boundsTop = options.bounds?.top ?? 0;\n const boundsBottom = options.bounds?.bottom ?? viewportHeight;\n\n const spaceBelow = Math.max(\n 0,\n boundsBottom - anchorRect.bottom - gap - viewportMargin,\n );\n const spaceAbove = Math.max(\n 0,\n anchorRect.top - boundsTop - gap - viewportMargin,\n );\n\n const placement: PopupPlacement =\n spaceBelow < preferredHeight && spaceAbove > spaceBelow ? 'above' : 'below';\n\n const available = placement === 'below' ? spaceBelow : spaceAbove;\n const capped =\n options.maxHeightCap != null\n ? Math.min(options.maxHeightCap, available)\n : available;\n\n return {\n placement,\n maxHeight: Math.max(minHeight, capped),\n };\n}\n\nexport interface AnchoredPopupFixedStyle {\n position: 'fixed' | 'absolute';\n top?: number;\n bottom?: number;\n left: number;\n width: number;\n zIndex: number | string;\n}\n\nexport type AnchoredPopupBounds = Pick<\n DOMRect,\n 'top' | 'bottom' | 'left' | 'width' | 'height'\n>;\n\n/** Mount element metrics for portaled absolute positioning. */\nexport interface AnchoredPopupContainerFrame {\n /** Visible viewport bounds used for flip/max-height. */\n bounds: Pick<DOMRect, 'top' | 'bottom'>;\n /** Maps a viewport anchor rect into mount padding-box coordinates. */\n anchorInContainer: (\n anchorRect: Pick<DOMRect, 'top' | 'bottom' | 'left' | 'width'>,\n ) => {\n left: number;\n top: number;\n bottom: number;\n width: number;\n };\n /** Visible padding-box height used for above `bottom` anchoring. */\n clientHeight: number;\n /** Current vertical scroll offset of the mount. */\n scrollTop: number;\n}\n\nexport function getAnchoredPopupContainerFrame(\n mount: HTMLElement,\n): AnchoredPopupContainerFrame {\n const borderRect = mount.getBoundingClientRect();\n const boundsTop = borderRect.top + mount.clientTop;\n const boundsBottom = boundsTop + mount.clientHeight;\n\n return {\n bounds: { top: boundsTop, bottom: boundsBottom },\n anchorInContainer: (\n anchorRect: Pick<DOMRect, 'top' | 'bottom' | 'left' | 'width'>,\n ) => ({\n left:\n anchorRect.left - borderRect.left - mount.clientLeft + mount.scrollLeft,\n top: anchorRect.top - borderRect.top - mount.clientTop + mount.scrollTop,\n bottom:\n anchorRect.bottom - borderRect.top - mount.clientTop + mount.scrollTop,\n width: anchorRect.width,\n }),\n clientHeight: mount.clientHeight,\n scrollTop: mount.scrollTop,\n };\n}\n\n/**\n * Fixed box coords for a portaled popup anchored to a trigger rect.\n * Below: top edge sits `gap` px under the trigger bottom.\n * Above: bottom edge sits `gap` px above the trigger top (uses `bottom` so\n * height does not affect the gap).\n *\n * When `containerFrame` is provided, returns `absolute` coords in the mount\n * padding box (accounts for scroll and client border offsets). Otherwise uses\n * viewport `fixed` coords (document.body default).\n */\nexport function computeAnchoredPopupFixedStyle(\n anchorRect: Pick<DOMRect, 'top' | 'bottom' | 'left' | 'width'>,\n placement: PopupPlacement,\n options: {\n gap?: number;\n zIndex?: number | string;\n /** Override width (defaults to trigger width). */\n width?: number;\n viewportHeight?: number;\n containerFrame?: AnchoredPopupContainerFrame;\n },\n): AnchoredPopupFixedStyle {\n const gap = options.gap ?? ANCHORED_POPUP_GAP;\n const width = options.width ?? anchorRect.width;\n const zIndex = options.zIndex ?? ANCHORED_POPUP_Z_INDEX;\n const containerFrame = options.containerFrame;\n\n if (containerFrame) {\n const anchor = containerFrame.anchorInContainer(anchorRect);\n if (placement === 'below') {\n return {\n position: 'absolute',\n top: anchor.bottom + gap,\n left: anchor.left,\n width,\n zIndex,\n };\n }\n const visibleTop = anchor.top - containerFrame.scrollTop;\n return {\n position: 'absolute',\n bottom: containerFrame.clientHeight - visibleTop + gap,\n left: anchor.left,\n width,\n zIndex,\n };\n }\n\n const viewportHeight = options.viewportHeight ?? window.innerHeight;\n\n if (placement === 'below') {\n return {\n position: 'fixed',\n top: anchorRect.bottom + gap,\n left: anchorRect.left,\n width,\n zIndex,\n };\n }\n\n return {\n position: 'fixed',\n bottom: viewportHeight - anchorRect.top + gap,\n left: anchorRect.left,\n width,\n zIndex,\n };\n}\n"],"names":["ANCHORED_POPUP_GAP","ANCHORED_POPUP_VIEWPORT_MARGIN","ANCHORED_POPUP_MIN_HEIGHT","ANCHORED_POPUP_Z_INDEX","computeAnchoredPopupPlacement","anchorRect","viewportHeight","options","gap","viewportMargin","minHeight","preferredHeight","boundsTop","_a","boundsBottom","_b","spaceBelow","spaceAbove","placement","available","capped","getAnchoredPopupContainerFrame","mount","borderRect","computeAnchoredPopupFixedStyle","width","zIndex","containerFrame","anchor","visibleTop"],"mappings":"gFAEO,MAAMA,EAAqB,EACrBC,EAAiC,GACjCC,EAA4B,IAM5BC,EAAyB,sBAyB/B,SAASC,EACdC,EACAC,EACAC,EAI8B,SAC9B,MAAMC,EAAMD,EAAQ,KAAOP,EACrBS,EACJF,EAAQ,gBAAkBN,EACtBS,EAAYH,EAAQ,WAAaL,EACjCS,EAAkB,KAAK,IAAIJ,EAAQ,gBAAiB,CAAC,EAErDK,IAAYC,EAAAN,EAAQ,SAAR,YAAAM,EAAgB,MAAO,EACnCC,IAAeC,EAAAR,EAAQ,SAAR,YAAAQ,EAAgB,SAAUT,EAEzCU,EAAa,KAAK,IACtB,EACAF,EAAeT,EAAW,OAASG,EAAMC,CAAA,EAErCQ,EAAa,KAAK,IACtB,EACAZ,EAAW,IAAMO,EAAYJ,EAAMC,CAAA,EAG/BS,EACJF,EAAaL,GAAmBM,EAAaD,EAAa,QAAU,QAEhEG,EAAYD,IAAc,QAAUF,EAAaC,EACjDG,EACJb,EAAQ,cAAgB,KACpB,KAAK,IAAIA,EAAQ,aAAcY,CAAS,EACxCA,EAEN,MAAO,CACL,UAAAD,EACA,UAAW,KAAK,IAAIR,EAAWU,CAAM,CAAA,CAEzC,CAmCO,SAASC,EACdC,EAC6B,CAC7B,MAAMC,EAAaD,EAAM,sBAAA,EACnBV,EAAYW,EAAW,IAAMD,EAAM,UACnCR,EAAeF,EAAYU,EAAM,aAEvC,MAAO,CACL,OAAQ,CAAE,IAAKV,EAAW,OAAQE,CAAA,EAClC,kBACET,IACI,CACJ,KACEA,EAAW,KAAOkB,EAAW,KAAOD,EAAM,WAAaA,EAAM,WAC/D,IAAKjB,EAAW,IAAMkB,EAAW,IAAMD,EAAM,UAAYA,EAAM,UAC/D,OACEjB,EAAW,OAASkB,EAAW,IAAMD,EAAM,UAAYA,EAAM,UAC/D,MAAOjB,EAAW,KAAA,GAEpB,aAAciB,EAAM,aACpB,UAAWA,EAAM,SAAA,CAErB,CAYO,SAASE,EACdnB,EACAa,EACAX,EAQyB,CACzB,MAAMC,EAAMD,EAAQ,KAAOP,EACrByB,EAAQlB,EAAQ,OAASF,EAAW,MACpCqB,EAASnB,EAAQ,QAAUJ,EAC3BwB,EAAiBpB,EAAQ,eAE/B,GAAIoB,EAAgB,CAClB,MAAMC,EAASD,EAAe,kBAAkBtB,CAAU,EAC1D,GAAIa,IAAc,QAChB,MAAO,CACL,SAAU,WACV,IAAKU,EAAO,OAASpB,EACrB,KAAMoB,EAAO,KACb,MAAAH,EACA,OAAAC,CAAA,EAGJ,MAAMG,EAAaD,EAAO,IAAMD,EAAe,UAC/C,MAAO,CACL,SAAU,WACV,OAAQA,EAAe,aAAeE,EAAarB,EACnD,KAAMoB,EAAO,KACb,MAAAH,EACA,OAAAC,CAAA,CAEJ,CAEA,MAAMpB,EAAiBC,EAAQ,gBAAkB,OAAO,YAExD,OAAIW,IAAc,QACT,CACL,SAAU,QACV,IAAKb,EAAW,OAASG,EACzB,KAAMH,EAAW,KACjB,MAAAoB,EACA,OAAAC,CAAA,EAIG,CACL,SAAU,QACV,OAAQpB,EAAiBD,EAAW,IAAMG,EAC1C,KAAMH,EAAW,KACjB,MAAAoB,EACA,OAAAC,CAAA,CAEJ"}
|
|
@@ -2,8 +2,12 @@ export type PopupPlacement = 'above' | 'below';
|
|
|
2
2
|
export declare const ANCHORED_POPUP_GAP = 4;
|
|
3
3
|
export declare const ANCHORED_POPUP_VIEWPORT_MARGIN = 48;
|
|
4
4
|
export declare const ANCHORED_POPUP_MIN_HEIGHT = 120;
|
|
5
|
-
/**
|
|
6
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Default stacking for portaled menus: the `--z-menu` layer, which sits above
|
|
7
|
+
* modals (`--z-modal`). The fallback keeps it working when tokens aren't loaded.
|
|
8
|
+
* Hosts can override per use via `zIndex` or by redefining the token.
|
|
9
|
+
*/
|
|
10
|
+
export declare const ANCHORED_POPUP_Z_INDEX = "var(--z-menu, 1100)";
|
|
7
11
|
export interface AnchoredPopupPlacementOptions {
|
|
8
12
|
/** Gap between trigger and popup. Default: 4. */
|
|
9
13
|
gap?: number;
|
|
@@ -35,7 +39,7 @@ export interface AnchoredPopupFixedStyle {
|
|
|
35
39
|
bottom?: number;
|
|
36
40
|
left: number;
|
|
37
41
|
width: number;
|
|
38
|
-
zIndex: number;
|
|
42
|
+
zIndex: number | string;
|
|
39
43
|
}
|
|
40
44
|
export type AnchoredPopupBounds = Pick<DOMRect, 'top' | 'bottom' | 'left' | 'width' | 'height'>;
|
|
41
45
|
/** Mount element metrics for portaled absolute positioning. */
|
|
@@ -67,7 +71,7 @@ export declare function getAnchoredPopupContainerFrame(mount: HTMLElement): Anch
|
|
|
67
71
|
*/
|
|
68
72
|
export declare function computeAnchoredPopupFixedStyle(anchorRect: Pick<DOMRect, 'top' | 'bottom' | 'left' | 'width'>, placement: PopupPlacement, options: {
|
|
69
73
|
gap?: number;
|
|
70
|
-
zIndex?: number;
|
|
74
|
+
zIndex?: number | string;
|
|
71
75
|
/** Override width (defaults to trigger width). */
|
|
72
76
|
width?: number;
|
|
73
77
|
viewportHeight?: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"anchoredPopupPlacement.d.ts","sourceRoot":"","sources":["../../src/utils/anchoredPopupPlacement.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,cAAc,GAAG,OAAO,GAAG,OAAO,CAAC;AAE/C,eAAO,MAAM,kBAAkB,IAAI,CAAC;AACpC,eAAO,MAAM,8BAA8B,KAAK,CAAC;AACjD,eAAO,MAAM,yBAAyB,MAAM,CAAC;AAC7C
|
|
1
|
+
{"version":3,"file":"anchoredPopupPlacement.d.ts","sourceRoot":"","sources":["../../src/utils/anchoredPopupPlacement.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,cAAc,GAAG,OAAO,GAAG,OAAO,CAAC;AAE/C,eAAO,MAAM,kBAAkB,IAAI,CAAC;AACpC,eAAO,MAAM,8BAA8B,KAAK,CAAC;AACjD,eAAO,MAAM,yBAAyB,MAAM,CAAC;AAC7C;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,wBAAwB,CAAC;AAE5D,MAAM,WAAW,6BAA6B;IAC5C,iDAAiD;IACjD,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,wDAAwD;IACxD,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,0DAA0D;IAC1D,eAAe,EAAE,MAAM,CAAC;IACxB,2DAA2D;IAC3D,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,2EAA2E;IAC3E,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,4BAA4B;IAC3C,SAAS,EAAE,cAAc,CAAC;IAC1B,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;GAIG;AACH,wBAAgB,6BAA6B,CAC3C,UAAU,EAAE,IAAI,CAAC,OAAO,EAAE,KAAK,GAAG,QAAQ,CAAC,EAC3C,cAAc,EAAE,MAAM,EACtB,OAAO,EAAE,6BAA6B,GAAG;IACvC,sEAAsE;IACtE,MAAM,CAAC,EAAE,IAAI,CAAC,OAAO,EAAE,KAAK,GAAG,QAAQ,CAAC,CAAC;CAC1C,GACA,4BAA4B,CAgC9B;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE,OAAO,GAAG,UAAU,CAAC;IAC/B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC;CACzB;AAED,MAAM,MAAM,mBAAmB,GAAG,IAAI,CACpC,OAAO,EACP,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,CAC/C,CAAC;AAEF,+DAA+D;AAC/D,MAAM,WAAW,2BAA2B;IAC1C,wDAAwD;IACxD,MAAM,EAAE,IAAI,CAAC,OAAO,EAAE,KAAK,GAAG,QAAQ,CAAC,CAAC;IACxC,sEAAsE;IACtE,iBAAiB,EAAE,CACjB,UAAU,EAAE,IAAI,CAAC,OAAO,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,CAAC,KAC3D;QACH,IAAI,EAAE,MAAM,CAAC;QACb,GAAG,EAAE,MAAM,CAAC;QACZ,MAAM,EAAE,MAAM,CAAC;QACf,KAAK,EAAE,MAAM,CAAC;KACf,CAAC;IACF,oEAAoE;IACpE,YAAY,EAAE,MAAM,CAAC;IACrB,mDAAmD;IACnD,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,8BAA8B,CAC5C,KAAK,EAAE,WAAW,GACjB,2BAA2B,CAoB7B;AAED;;;;;;;;;GASG;AACH,wBAAgB,8BAA8B,CAC5C,UAAU,EAAE,IAAI,CAAC,OAAO,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,CAAC,EAC9D,SAAS,EAAE,cAAc,EACzB,OAAO,EAAE;IACP,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACzB,kDAAkD;IAClD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,2BAA2B,CAAC;CAC9C,GACA,uBAAuB,CA8CzB"}
|
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
const
|
|
2
|
-
function
|
|
3
|
-
var
|
|
4
|
-
const i = e.gap ?? 4, o = e.viewportMargin ?? 48, l = e.minHeight ?? 120, p = Math.max(e.preferredHeight, 0), s = ((
|
|
1
|
+
const h = 4, _ = 48, m = 120, g = "var(--z-menu, 1100)";
|
|
2
|
+
function O(t, n, e) {
|
|
3
|
+
var d, H;
|
|
4
|
+
const i = e.gap ?? 4, o = e.viewportMargin ?? 48, l = e.minHeight ?? 120, p = Math.max(e.preferredHeight, 0), s = ((d = e.bounds) == null ? void 0 : d.top) ?? 0, c = ((H = e.bounds) == null ? void 0 : H.bottom) ?? n, r = Math.max(
|
|
5
5
|
0,
|
|
6
6
|
c - t.bottom - i - o
|
|
7
|
-
),
|
|
7
|
+
), a = Math.max(
|
|
8
8
|
0,
|
|
9
9
|
t.top - s - i - o
|
|
10
|
-
),
|
|
10
|
+
), P = r < p && a > r ? "above" : "below", b = P === "below" ? r : a, f = e.maxHeightCap != null ? Math.min(e.maxHeightCap, b) : b;
|
|
11
11
|
return {
|
|
12
|
-
placement:
|
|
13
|
-
maxHeight: Math.max(l,
|
|
12
|
+
placement: P,
|
|
13
|
+
maxHeight: Math.max(l, f)
|
|
14
14
|
};
|
|
15
15
|
}
|
|
16
|
-
function
|
|
16
|
+
function A(t) {
|
|
17
17
|
const n = t.getBoundingClientRect(), e = n.top + t.clientTop, i = e + t.clientHeight;
|
|
18
18
|
return {
|
|
19
19
|
bounds: { top: e, bottom: i },
|
|
@@ -27,8 +27,8 @@ function m(t) {
|
|
|
27
27
|
scrollTop: t.scrollTop
|
|
28
28
|
};
|
|
29
29
|
}
|
|
30
|
-
function
|
|
31
|
-
const i = e.gap ?? 4, o = e.width ?? t.width, l = e.zIndex ??
|
|
30
|
+
function u(t, n, e) {
|
|
31
|
+
const i = e.gap ?? 4, o = e.width ?? t.width, l = e.zIndex ?? g, p = e.containerFrame;
|
|
32
32
|
if (p) {
|
|
33
33
|
const c = p.anchorInContainer(t);
|
|
34
34
|
if (n === "below")
|
|
@@ -64,12 +64,12 @@ function w(t, n, e) {
|
|
|
64
64
|
};
|
|
65
65
|
}
|
|
66
66
|
export {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
67
|
+
h as ANCHORED_POPUP_GAP,
|
|
68
|
+
m as ANCHORED_POPUP_MIN_HEIGHT,
|
|
69
|
+
_ as ANCHORED_POPUP_VIEWPORT_MARGIN,
|
|
70
|
+
g as ANCHORED_POPUP_Z_INDEX,
|
|
71
|
+
u as computeAnchoredPopupFixedStyle,
|
|
72
|
+
O as computeAnchoredPopupPlacement,
|
|
73
|
+
A as getAnchoredPopupContainerFrame
|
|
74
74
|
};
|
|
75
75
|
//# sourceMappingURL=anchoredPopupPlacement.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"anchoredPopupPlacement.js","sources":["../../src/utils/anchoredPopupPlacement.ts"],"sourcesContent":["export type PopupPlacement = 'above' | 'below';\n\nexport const ANCHORED_POPUP_GAP = 4;\nexport const ANCHORED_POPUP_VIEWPORT_MARGIN = 48;\nexport const ANCHORED_POPUP_MIN_HEIGHT = 120;\n
|
|
1
|
+
{"version":3,"file":"anchoredPopupPlacement.js","sources":["../../src/utils/anchoredPopupPlacement.ts"],"sourcesContent":["export type PopupPlacement = 'above' | 'below';\n\nexport const ANCHORED_POPUP_GAP = 4;\nexport const ANCHORED_POPUP_VIEWPORT_MARGIN = 48;\nexport const ANCHORED_POPUP_MIN_HEIGHT = 120;\n/**\n * Default stacking for portaled menus: the `--z-menu` layer, which sits above\n * modals (`--z-modal`). The fallback keeps it working when tokens aren't loaded.\n * Hosts can override per use via `zIndex` or by redefining the token.\n */\nexport const ANCHORED_POPUP_Z_INDEX = 'var(--z-menu, 1100)';\n\nexport interface AnchoredPopupPlacementOptions {\n /** Gap between trigger and popup. Default: 4. */\n gap?: number;\n /** Reserved space at the viewport edge. Default: 48. */\n viewportMargin?: number;\n /** Height used to decide whether the popup fits below. */\n preferredHeight: number;\n /** Cap for scrollable menus (e.g. Select/Combobox 280). */\n maxHeightCap?: number;\n /** Floor so short viewports still show a scrollable list. Default: 120. */\n minHeight?: number;\n}\n\nexport interface AnchoredPopupPlacementResult {\n placement: PopupPlacement;\n maxHeight: number;\n}\n\n/**\n * Prefers opening below the anchor; flips above when there isn’t enough room\n * below and there is more space above. Max-height is the available space in the\n * chosen direction, optionally capped.\n */\nexport function computeAnchoredPopupPlacement(\n anchorRect: Pick<DOMRect, 'top' | 'bottom'>,\n viewportHeight: number,\n options: AnchoredPopupPlacementOptions & {\n /** When set, flip/max-height use this box instead of the viewport. */\n bounds?: Pick<DOMRect, 'top' | 'bottom'>;\n },\n): AnchoredPopupPlacementResult {\n const gap = options.gap ?? ANCHORED_POPUP_GAP;\n const viewportMargin =\n options.viewportMargin ?? ANCHORED_POPUP_VIEWPORT_MARGIN;\n const minHeight = options.minHeight ?? ANCHORED_POPUP_MIN_HEIGHT;\n const preferredHeight = Math.max(options.preferredHeight, 0);\n\n const boundsTop = options.bounds?.top ?? 0;\n const boundsBottom = options.bounds?.bottom ?? viewportHeight;\n\n const spaceBelow = Math.max(\n 0,\n boundsBottom - anchorRect.bottom - gap - viewportMargin,\n );\n const spaceAbove = Math.max(\n 0,\n anchorRect.top - boundsTop - gap - viewportMargin,\n );\n\n const placement: PopupPlacement =\n spaceBelow < preferredHeight && spaceAbove > spaceBelow ? 'above' : 'below';\n\n const available = placement === 'below' ? spaceBelow : spaceAbove;\n const capped =\n options.maxHeightCap != null\n ? Math.min(options.maxHeightCap, available)\n : available;\n\n return {\n placement,\n maxHeight: Math.max(minHeight, capped),\n };\n}\n\nexport interface AnchoredPopupFixedStyle {\n position: 'fixed' | 'absolute';\n top?: number;\n bottom?: number;\n left: number;\n width: number;\n zIndex: number | string;\n}\n\nexport type AnchoredPopupBounds = Pick<\n DOMRect,\n 'top' | 'bottom' | 'left' | 'width' | 'height'\n>;\n\n/** Mount element metrics for portaled absolute positioning. */\nexport interface AnchoredPopupContainerFrame {\n /** Visible viewport bounds used for flip/max-height. */\n bounds: Pick<DOMRect, 'top' | 'bottom'>;\n /** Maps a viewport anchor rect into mount padding-box coordinates. */\n anchorInContainer: (\n anchorRect: Pick<DOMRect, 'top' | 'bottom' | 'left' | 'width'>,\n ) => {\n left: number;\n top: number;\n bottom: number;\n width: number;\n };\n /** Visible padding-box height used for above `bottom` anchoring. */\n clientHeight: number;\n /** Current vertical scroll offset of the mount. */\n scrollTop: number;\n}\n\nexport function getAnchoredPopupContainerFrame(\n mount: HTMLElement,\n): AnchoredPopupContainerFrame {\n const borderRect = mount.getBoundingClientRect();\n const boundsTop = borderRect.top + mount.clientTop;\n const boundsBottom = boundsTop + mount.clientHeight;\n\n return {\n bounds: { top: boundsTop, bottom: boundsBottom },\n anchorInContainer: (\n anchorRect: Pick<DOMRect, 'top' | 'bottom' | 'left' | 'width'>,\n ) => ({\n left:\n anchorRect.left - borderRect.left - mount.clientLeft + mount.scrollLeft,\n top: anchorRect.top - borderRect.top - mount.clientTop + mount.scrollTop,\n bottom:\n anchorRect.bottom - borderRect.top - mount.clientTop + mount.scrollTop,\n width: anchorRect.width,\n }),\n clientHeight: mount.clientHeight,\n scrollTop: mount.scrollTop,\n };\n}\n\n/**\n * Fixed box coords for a portaled popup anchored to a trigger rect.\n * Below: top edge sits `gap` px under the trigger bottom.\n * Above: bottom edge sits `gap` px above the trigger top (uses `bottom` so\n * height does not affect the gap).\n *\n * When `containerFrame` is provided, returns `absolute` coords in the mount\n * padding box (accounts for scroll and client border offsets). Otherwise uses\n * viewport `fixed` coords (document.body default).\n */\nexport function computeAnchoredPopupFixedStyle(\n anchorRect: Pick<DOMRect, 'top' | 'bottom' | 'left' | 'width'>,\n placement: PopupPlacement,\n options: {\n gap?: number;\n zIndex?: number | string;\n /** Override width (defaults to trigger width). */\n width?: number;\n viewportHeight?: number;\n containerFrame?: AnchoredPopupContainerFrame;\n },\n): AnchoredPopupFixedStyle {\n const gap = options.gap ?? ANCHORED_POPUP_GAP;\n const width = options.width ?? anchorRect.width;\n const zIndex = options.zIndex ?? ANCHORED_POPUP_Z_INDEX;\n const containerFrame = options.containerFrame;\n\n if (containerFrame) {\n const anchor = containerFrame.anchorInContainer(anchorRect);\n if (placement === 'below') {\n return {\n position: 'absolute',\n top: anchor.bottom + gap,\n left: anchor.left,\n width,\n zIndex,\n };\n }\n const visibleTop = anchor.top - containerFrame.scrollTop;\n return {\n position: 'absolute',\n bottom: containerFrame.clientHeight - visibleTop + gap,\n left: anchor.left,\n width,\n zIndex,\n };\n }\n\n const viewportHeight = options.viewportHeight ?? window.innerHeight;\n\n if (placement === 'below') {\n return {\n position: 'fixed',\n top: anchorRect.bottom + gap,\n left: anchorRect.left,\n width,\n zIndex,\n };\n }\n\n return {\n position: 'fixed',\n bottom: viewportHeight - anchorRect.top + gap,\n left: anchorRect.left,\n width,\n zIndex,\n };\n}\n"],"names":["ANCHORED_POPUP_GAP","ANCHORED_POPUP_VIEWPORT_MARGIN","ANCHORED_POPUP_MIN_HEIGHT","ANCHORED_POPUP_Z_INDEX","computeAnchoredPopupPlacement","anchorRect","viewportHeight","options","_a","_b","gap","viewportMargin","minHeight","preferredHeight","boundsTop","boundsBottom","spaceBelow","spaceAbove","placement","available","capped","getAnchoredPopupContainerFrame","mount","borderRect","computeAnchoredPopupFixedStyle","width","zIndex","containerFrame","anchor","visibleTop"],"mappings":"AAEO,MAAMA,IAAqB,GACrBC,IAAiC,IACjCC,IAA4B,KAM5BC,IAAyB;AAyB/B,SAASC,EACdC,GACAC,GACAC,GAI8B;AAxCzB,MAAAC,GAAAC;AAyCL,QAAMC,IAAMH,EAAQ,OAAO,GACrBI,IACJJ,EAAQ,kBAAkB,IACtBK,IAAYL,EAAQ,aAAa,KACjCM,IAAkB,KAAK,IAAIN,EAAQ,iBAAiB,CAAC,GAErDO,MAAYN,IAAAD,EAAQ,WAAR,gBAAAC,EAAgB,QAAO,GACnCO,MAAeN,IAAAF,EAAQ,WAAR,gBAAAE,EAAgB,WAAUH,GAEzCU,IAAa,KAAK;AAAA,IACtB;AAAA,IACAD,IAAeV,EAAW,SAASK,IAAMC;AAAA,EAAA,GAErCM,IAAa,KAAK;AAAA,IACtB;AAAA,IACAZ,EAAW,MAAMS,IAAYJ,IAAMC;AAAA,EAAA,GAG/BO,IACJF,IAAaH,KAAmBI,IAAaD,IAAa,UAAU,SAEhEG,IAAYD,MAAc,UAAUF,IAAaC,GACjDG,IACJb,EAAQ,gBAAgB,OACpB,KAAK,IAAIA,EAAQ,cAAcY,CAAS,IACxCA;AAEN,SAAO;AAAA,IACL,WAAAD;AAAA,IACA,WAAW,KAAK,IAAIN,GAAWQ,CAAM;AAAA,EAAA;AAEzC;AAmCO,SAASC,EACdC,GAC6B;AAC7B,QAAMC,IAAaD,EAAM,sBAAA,GACnBR,IAAYS,EAAW,MAAMD,EAAM,WACnCP,IAAeD,IAAYQ,EAAM;AAEvC,SAAO;AAAA,IACL,QAAQ,EAAE,KAAKR,GAAW,QAAQC,EAAA;AAAA,IAClC,mBAAmB,CACjBV,OACI;AAAA,MACJ,MACEA,EAAW,OAAOkB,EAAW,OAAOD,EAAM,aAAaA,EAAM;AAAA,MAC/D,KAAKjB,EAAW,MAAMkB,EAAW,MAAMD,EAAM,YAAYA,EAAM;AAAA,MAC/D,QACEjB,EAAW,SAASkB,EAAW,MAAMD,EAAM,YAAYA,EAAM;AAAA,MAC/D,OAAOjB,EAAW;AAAA,IAAA;AAAA,IAEpB,cAAciB,EAAM;AAAA,IACpB,WAAWA,EAAM;AAAA,EAAA;AAErB;AAYO,SAASE,EACdnB,GACAa,GACAX,GAQyB;AACzB,QAAMG,IAAMH,EAAQ,OAAO,GACrBkB,IAAQlB,EAAQ,SAASF,EAAW,OACpCqB,IAASnB,EAAQ,UAAUJ,GAC3BwB,IAAiBpB,EAAQ;AAE/B,MAAIoB,GAAgB;AAClB,UAAMC,IAASD,EAAe,kBAAkBtB,CAAU;AAC1D,QAAIa,MAAc;AAChB,aAAO;AAAA,QACL,UAAU;AAAA,QACV,KAAKU,EAAO,SAASlB;AAAA,QACrB,MAAMkB,EAAO;AAAA,QACb,OAAAH;AAAA,QACA,QAAAC;AAAA,MAAA;AAGJ,UAAMG,IAAaD,EAAO,MAAMD,EAAe;AAC/C,WAAO;AAAA,MACL,UAAU;AAAA,MACV,QAAQA,EAAe,eAAeE,IAAanB;AAAA,MACnD,MAAMkB,EAAO;AAAA,MACb,OAAAH;AAAA,MACA,QAAAC;AAAA,IAAA;AAAA,EAEJ;AAEA,QAAMpB,IAAiBC,EAAQ,kBAAkB,OAAO;AAExD,SAAIW,MAAc,UACT;AAAA,IACL,UAAU;AAAA,IACV,KAAKb,EAAW,SAASK;AAAA,IACzB,MAAML,EAAW;AAAA,IACjB,OAAAoB;AAAA,IACA,QAAAC;AAAA,EAAA,IAIG;AAAA,IACL,UAAU;AAAA,IACV,QAAQpB,IAAiBD,EAAW,MAAMK;AAAA,IAC1C,MAAML,EAAW;AAAA,IACjB,OAAAoB;AAAA,IACA,QAAAC;AAAA,EAAA;AAEJ;"}
|