@ahrowe/ui 0.41.1 → 0.41.2

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.
@@ -9,10 +9,11 @@ export interface UseAnchorTrackingArgs {
9
9
  */
10
10
  elementRefs: RefObject<HTMLElement | null>[];
11
11
  /**
12
- * Positions the popup. Called once on setup and then on every scroll, resize and tracked size
13
- * change. Reads the caller's own refs for everything beyond the two arguments.
12
+ * Positions the popup. Called once on setup and then on every scroll, resize, tracked size
13
+ * change and end of a CSS animation that moved the trigger, plus whenever it calls `rerun` for
14
+ * an end none of those report. Reads the caller's own refs for everything else.
14
15
  */
15
- update: (trigger: HTMLElement, ancestors: HTMLElement[]) => void;
16
+ update: (trigger: HTMLElement, ancestors: HTMLElement[], rerun: () => void) => void;
16
17
  /**
17
18
  * Elements whose own size changes must re-run `update`: a popup whose content changes size
18
19
  * while open (a filtered list shrinking, an async list loading in) otherwise keeps coordinates
@@ -32,6 +32,11 @@ export interface UsePopoverPositionArgs {
32
32
  * panel that overhangs it.
33
33
  */
34
34
  shrinkToFit?: boolean;
35
+ /**
36
+ * Called when a newly opened panel first becomes visible. Focus has to wait for it: a panel
37
+ * still hidden, waiting out an ancestor's animation, takes none.
38
+ */
39
+ onShown?: () => void;
35
40
  }
36
41
  /**
37
42
  * Positions a portaled, `position: fixed` popover on any of the four sides of its trigger with
@@ -42,4 +47,4 @@ export interface UsePopoverPositionArgs {
42
47
  * `useAnchorTracking` supplies the lifecycle around this: mount wait, ancestor resolution, and
43
48
  * re-running on scroll, viewport resize and panel size changes.
44
49
  */
45
- export declare function usePopoverPosition({ isOpen, placement, align, offset, triggerRef, floatingRef, panelRef, arrowRef, matchTriggerWidth, shrinkToFit, }: UsePopoverPositionArgs): void;
50
+ export declare function usePopoverPosition({ isOpen, placement, align, offset, triggerRef, floatingRef, panelRef, arrowRef, matchTriggerWidth, shrinkToFit, onShown, }: UsePopoverPositionArgs): void;
@@ -1 +1 @@
1
- {"version":3,"file":"fab.module.mjs","names":[],"sources":["../../../../package/common/fab/fab.module.pcss"],"sourcesContent":[".fab {\n animation: appear 0.3s forwards;\n opacity: 0;\n transform: scale(0);\n box-shadow:\n 0 3px 5px -1px rgb(0 0 0 / 20%),\n 0 6px 10px 0 rgb(0 0 0 / 14%),\n 0 1px 18px 0 rgb(0 0 0 / 12%);\n border-radius: 50%;\n\n &-container {\n position: fixed;\n bottom: 16px;\n right: 16px;\n\n @media (--breakpoint-tablet) {\n z-index: 2;\n }\n }\n\n &-menu {\n position: absolute;\n top: 0;\n transform: translateY(calc(-100% - 16px));\n width: 100%;\n display: flex;\n flex-flow: column-reverse;\n gap: 16px;\n align-items: center;\n pointer-events: none;\n user-select: none;\n\n &-backdrop {\n width: 0%;\n height: 0%;\n position: fixed;\n top: 0;\n left: 0;\n background: black;\n opacity: 0;\n transition: opacity 0.4s;\n\n &-open {\n width: 100%;\n height: 100%;\n opacity: 0.5;\n }\n }\n\n &-entry {\n position: relative;\n cursor: pointer;\n -webkit-tap-highlight-color: transparent;\n opacity: 0;\n transform: scale(0);\n transition: all 0.2s;\n pointer-events: all;\n\n &:nth-child(2) {\n transition-delay: 0.05s;\n }\n\n &:nth-child(3) {\n transition-delay: 0.1s;\n }\n\n &:nth-child(4) {\n transition-delay: 0.15s;\n }\n\n &:nth-child(5) {\n transition-delay: 0.2s;\n }\n\n &:nth-child(6) {\n transition-delay: 0.25s;\n }\n\n &:nth-child(7) {\n transition-delay: 0.3s;\n }\n\n &-shown {\n opacity: 1;\n transform: scale(1);\n }\n\n &-label {\n background: var(--background-accent-light);\n border-radius: var(--default-border-radius);\n padding: 4px 8px;\n\n &-container {\n position: absolute;\n width: max-content;\n transform: translateX(-100%);\n height: 100%;\n margin-left: -8px;\n align-items: center;\n display: flex;\n }\n }\n }\n }\n\n &-icon {\n transition: all 0.4s;\n display: flex;\n justify-content: center;\n align-items: center;\n position: relative;\n width: 100%;\n height: 100%;\n\n &-menuOpen {\n transform: rotate(225deg);\n }\n\n &-content {\n position: absolute;\n width: var(--custom-fab-icon-size);\n height: var(--custom-fab-icon-size);\n display: flex;\n align-items: center;\n justify-content: center;\n opacity: 0;\n transition: opacity 0.3s;\n\n &-shown {\n opacity: 1;\n }\n }\n }\n\n @keyframes appear {\n to {\n opacity: 1;\n transform: scale(1);\n }\n }\n}\n\n/* `.fab` is opacity 0 and scale 0 until `appear` runs, so removing the animation has to put it\n at the end state by hand or the button never shows up at all. */\n@media (prefers-reduced-motion: reduce) {\n .fab {\n animation: none;\n opacity: 1;\n transform: none;\n }\n\n .fab-menu-entry {\n transition: none;\n }\n}\n"],"mappings":""}
1
+ {"version":3,"file":"fab.module.mjs","names":[],"sources":["../../../../package/common/fab/fab.module.pcss"],"sourcesContent":[".fab {\n animation: appear 0.3s forwards;\n opacity: 0;\n transform: scale(0);\n box-shadow:\n 0 3px 5px -1px rgb(0 0 0 / 20%),\n 0 6px 10px 0 rgb(0 0 0 / 14%),\n 0 1px 18px 0 rgb(0 0 0 / 12%);\n border-radius: 50%;\n\n &-container {\n position: fixed;\n bottom: 16px;\n right: 16px;\n }\n\n &-menu {\n position: absolute;\n top: 0;\n transform: translateY(calc(-100% - 16px));\n width: 100%;\n display: flex;\n flex-flow: column-reverse;\n gap: 16px;\n align-items: center;\n pointer-events: none;\n user-select: none;\n\n &-backdrop {\n width: 0%;\n height: 0%;\n position: fixed;\n top: 0;\n left: 0;\n background: black;\n opacity: 0;\n transition: opacity 0.4s;\n\n &-open {\n width: 100%;\n height: 100%;\n opacity: 0.5;\n }\n }\n\n &-entry {\n position: relative;\n cursor: pointer;\n -webkit-tap-highlight-color: transparent;\n opacity: 0;\n transform: scale(0);\n transition: all 0.2s;\n pointer-events: all;\n\n &:nth-child(2) {\n transition-delay: 0.05s;\n }\n\n &:nth-child(3) {\n transition-delay: 0.1s;\n }\n\n &:nth-child(4) {\n transition-delay: 0.15s;\n }\n\n &:nth-child(5) {\n transition-delay: 0.2s;\n }\n\n &:nth-child(6) {\n transition-delay: 0.25s;\n }\n\n &:nth-child(7) {\n transition-delay: 0.3s;\n }\n\n &-shown {\n opacity: 1;\n transform: scale(1);\n }\n\n &-label {\n background: var(--background-accent-light);\n border-radius: var(--default-border-radius);\n padding: 4px 8px;\n\n &-container {\n position: absolute;\n width: max-content;\n transform: translateX(-100%);\n height: 100%;\n margin-left: -8px;\n align-items: center;\n display: flex;\n }\n }\n }\n }\n\n &-icon {\n transition: all 0.4s;\n display: flex;\n justify-content: center;\n align-items: center;\n position: relative;\n width: 100%;\n height: 100%;\n\n &-menuOpen {\n transform: rotate(225deg);\n }\n\n &-content {\n position: absolute;\n width: var(--custom-fab-icon-size);\n height: var(--custom-fab-icon-size);\n display: flex;\n align-items: center;\n justify-content: center;\n opacity: 0;\n transition: opacity 0.3s;\n\n &-shown {\n opacity: 1;\n }\n }\n }\n\n @keyframes appear {\n to {\n opacity: 1;\n transform: scale(1);\n }\n }\n}\n\n/* `.fab` is opacity 0 and scale 0 until `appear` runs, so removing the animation has to put it\n at the end state by hand or the button never shows up at all. */\n@media (prefers-reduced-motion: reduce) {\n .fab {\n animation: none;\n opacity: 1;\n transform: none;\n }\n\n .fab-menu-entry {\n transition: none;\n }\n}\n"],"mappings":""}
@@ -1,2 +1,2 @@
1
- "use client";import{getScrollableAncestors as e}from"../utils/scrollAncestors.mjs";import{useLayoutEffect as t,useRef as n}from"react";function r({isOpen:r,triggerRef:i,elementRefs:a,update:o,resizeRefs:s,teardown:c,deps:l=[]}){let u=n({elementRefs:a,update:o,resizeRefs:s,teardown:c});u.current={elementRefs:a,update:o,resizeRefs:s,teardown:c},t(()=>{if(!r)return;let t,n;function a(){let r=i.current,o=u.current.elementRefs;if(!r||o.some(e=>!e.current)){t=requestAnimationFrame(a);return}let s=e(r),c=new ResizeObserver(()=>d()),l=new Set;function d(){u.current.update(r,s),u.current.resizeRefs?.forEach(({current:e})=>{e&&!l.has(e)&&(l.add(e),c.observe(e))})}d();let f=[window,...s];f.forEach(e=>e.addEventListener(`scroll`,d,{passive:!0})),window.addEventListener(`resize`,d),window.visualViewport?.addEventListener(`resize`,d),window.visualViewport?.addEventListener(`scroll`,d),n=()=>{f.forEach(e=>e.removeEventListener(`scroll`,d)),window.removeEventListener(`resize`,d),window.visualViewport?.removeEventListener(`resize`,d),window.visualViewport?.removeEventListener(`scroll`,d),c.disconnect(),u.current.teardown?.()}}return a(),()=>{t!==void 0&&cancelAnimationFrame(t),n?.()}},[r,i,...l])}export{r as useAnchorTracking};
1
+ "use client";import{getScrollableAncestors as e}from"../utils/scrollAncestors.mjs";import{useLayoutEffect as t,useRef as n}from"react";function r({isOpen:r,triggerRef:i,elementRefs:a,update:o,resizeRefs:s,teardown:c,deps:l=[]}){let u=n({elementRefs:a,update:o,resizeRefs:s,teardown:c});u.current={elementRefs:a,update:o,resizeRefs:s,teardown:c},t(()=>{if(!r)return;let t,n;function a(){let r=i.current,o=u.current.elementRefs;if(!r||o.some(e=>!e.current)){t=requestAnimationFrame(a);return}let s=e(r),c=new ResizeObserver(()=>f()),l=new Set,d=!0;function f(){d&&(u.current.update(r,s,f),u.current.resizeRefs?.forEach(({current:e})=>{e&&!l.has(e)&&(l.add(e),c.observe(e))}))}f();let p=[window,...s];p.forEach(e=>e.addEventListener(`scroll`,f,{passive:!0})),window.addEventListener(`resize`,f),window.visualViewport?.addEventListener(`resize`,f),window.visualViewport?.addEventListener(`scroll`,f);function m(e){e.target.contains(r)&&f()}document.addEventListener(`transitionend`,m,!0),document.addEventListener(`animationend`,m,!0),n=()=>{d=!1,p.forEach(e=>e.removeEventListener(`scroll`,f)),window.removeEventListener(`resize`,f),window.visualViewport?.removeEventListener(`resize`,f),window.visualViewport?.removeEventListener(`scroll`,f),document.removeEventListener(`transitionend`,m,!0),document.removeEventListener(`animationend`,m,!0),c.disconnect(),u.current.teardown?.()}}return a(),()=>{t!==void 0&&cancelAnimationFrame(t),n?.()}},[r,i,...l])}export{r as useAnchorTracking};
2
2
  //# sourceMappingURL=useAnchorTracking.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"useAnchorTracking.mjs","names":[],"sources":["../../../../package/common/hooks/useAnchorTracking.ts"],"sourcesContent":["import { useLayoutEffect, useRef, type RefObject } from 'react';\nimport { getScrollableAncestors } from 'components/common/utils/scrollAncestors';\n\nexport interface UseAnchorTrackingArgs {\n isOpen: boolean;\n /** The element the popup is anchored to. Its scrollable ancestors are what get tracked. */\n triggerRef: RefObject<HTMLElement | null>;\n /**\n * Every element the positioning maths needs. Tracking starts only once all of them, plus the\n * trigger, are mounted. Read fresh on each setup, so an inline array literal is fine.\n */\n elementRefs: RefObject<HTMLElement | null>[];\n /**\n * Positions the popup. Called once on setup and then on every scroll, resize and tracked size\n * change. Reads the caller's own refs for everything beyond the two arguments.\n */\n update: (trigger: HTMLElement, ancestors: HTMLElement[]) => void;\n /**\n * Elements whose own size changes must re-run `update`: a popup whose content changes size\n * while open (a filtered list shrinking, an async list loading in) otherwise keeps coordinates\n * computed for its old height and ends up detached from the trigger. Re-read on every update,\n * since a panel can mount a commit later than the element it hangs off.\n */\n resizeRefs?: RefObject<HTMLElement | null>[];\n /** Hands the popup's elements back as they were found. Runs when tracking stops. */\n teardown?: () => void;\n /** Values that must restart tracking, and so re-run `update`, when they change. */\n deps?: readonly unknown[];\n}\n\n/**\n * The lifecycle half of anchoring a portaled, `position: fixed` popup to a trigger: waits for the\n * elements to mount, resolves the trigger's scrolling ancestors once, and re-runs the caller's\n * positioning on every scroll, viewport resize and tracked size change until it closes.\n *\n * Callers supply only the geometry (`update`) and the inline styles to clear again (`teardown`);\n * `usePopoverPosition` is built on this.\n *\n * Position is deliberately written straight to the DOM by the caller rather than through React\n * state: going through setState -> re-render -> commit adds a round-trip that lags behind the\n * browser's own scroll painting by at least a frame.\n */\nexport function useAnchorTracking({\n isOpen,\n triggerRef,\n elementRefs,\n update,\n resizeRefs,\n teardown,\n deps = [],\n}: UseAnchorTrackingArgs) {\n // Latest-value refs, so the effect restarts only for `deps` the caller names, never merely\n // because these were passed as fresh array/function literals this render.\n const latest = useRef({ elementRefs, update, resizeRefs, teardown });\n latest.current = { elementRefs, update, resizeRefs, teardown };\n\n useLayoutEffect(() => {\n if (!isOpen) return;\n\n let rafId: number | undefined;\n let stop: (() => void) | undefined;\n\n // The elements may not be mounted yet on the very first invocation of this effect: a portal\n // target can still be missing (a `#bodyEnd` the app renders, or any while hydrating), and then\n // the portaled content and its ref commits one render later. Since refs never change\n // identity, that later commit doesn't re-trigger this effect on its own, so retry each frame.\n function trySetup() {\n const trigger = triggerRef.current;\n const elements = latest.current.elementRefs;\n if (!trigger || elements.some((ref) => !ref.current)) {\n rafId = requestAnimationFrame(trySetup);\n return;\n }\n const ancestors = getScrollableAncestors(trigger);\n\n const resizeObserver = new ResizeObserver(() => runUpdate());\n const observed = new Set<Element>();\n\n function runUpdate() {\n latest.current.update(trigger!, ancestors);\n // After the update, so an element that only mounts during it is picked up too. Once\n // observed this is a handful of Set lookups, so it costs nothing on the scroll path.\n latest.current.resizeRefs?.forEach(({ current }) => {\n if (!current || observed.has(current)) return;\n observed.add(current);\n resizeObserver.observe(current);\n });\n }\n\n runUpdate();\n\n const scrollTargets: (Window | HTMLElement)[] = [window, ...ancestors];\n scrollTargets.forEach((t) => t.addEventListener('scroll', runUpdate, { passive: true }));\n window.addEventListener('resize', runUpdate);\n // `window.resize` doesn't reliably fire when a mobile keyboard opens/closes — visualViewport\n // does, and is what the callers' flip/clamp maths is measured against.\n window.visualViewport?.addEventListener('resize', runUpdate);\n window.visualViewport?.addEventListener('scroll', runUpdate);\n\n stop = () => {\n scrollTargets.forEach((t) => t.removeEventListener('scroll', runUpdate));\n window.removeEventListener('resize', runUpdate);\n window.visualViewport?.removeEventListener('resize', runUpdate);\n window.visualViewport?.removeEventListener('scroll', runUpdate);\n resizeObserver.disconnect();\n latest.current.teardown?.();\n };\n }\n\n trySetup();\n\n return () => {\n if (rafId !== undefined) cancelAnimationFrame(rafId);\n stop?.();\n };\n // Spread rather than listed: `deps` is the caller's own set of geometry inputs, and changing\n // any of them has to restart tracking so the new values get applied immediately.\n // oxlint-disable-next-line react-hooks/exhaustive-deps\n }, [isOpen, triggerRef, ...deps]);\n}\n"],"mappings":"uIA0CA,SAAgB,EAAkB,CAChC,SACA,aACA,cACA,SACA,aACA,WACA,OAAO,CAAC,GACgB,CAGxB,IAAM,EAAS,EAAO,CAAE,cAAa,SAAQ,aAAY,UAAS,CAAC,EACnE,EAAO,QAAU,CAAE,cAAa,SAAQ,aAAY,UAAS,EAE7D,MAAsB,CACpB,GAAI,CAAC,EAAQ,OAEb,IAAI,EACA,EAMJ,SAAS,GAAW,CAClB,IAAM,EAAU,EAAW,QACrB,EAAW,EAAO,QAAQ,YAChC,GAAI,CAAC,GAAW,EAAS,KAAM,GAAQ,CAAC,EAAI,OAAO,EAAG,CACpD,EAAQ,sBAAsB,CAAQ,EACtC,MACF,CACA,IAAM,EAAY,EAAuB,CAAO,EAE1C,EAAiB,IAAI,mBAAqB,EAAU,CAAC,EACrD,EAAW,IAAI,IAErB,SAAS,GAAY,CACnB,EAAO,QAAQ,OAAO,EAAU,CAAS,EAGzC,EAAO,QAAQ,YAAY,SAAS,CAAE,aAAc,CAC7C,GAAW,GAAS,IAAI,CAAO,IACpC,EAAS,IAAI,CAAO,EACpB,EAAe,QAAQ,CAAO,EAChC,CAAC,CACH,CAEA,EAAU,EAEV,IAAM,EAA0C,CAAC,OAAQ,GAAG,CAAS,EACrE,EAAc,QAAS,GAAM,EAAE,iBAAiB,SAAU,EAAW,CAAE,QAAS,EAAK,CAAC,CAAC,EACvF,OAAO,iBAAiB,SAAU,CAAS,EAG3C,OAAO,gBAAgB,iBAAiB,SAAU,CAAS,EAC3D,OAAO,gBAAgB,iBAAiB,SAAU,CAAS,EAE3D,MAAa,CACX,EAAc,QAAS,GAAM,EAAE,oBAAoB,SAAU,CAAS,CAAC,EACvE,OAAO,oBAAoB,SAAU,CAAS,EAC9C,OAAO,gBAAgB,oBAAoB,SAAU,CAAS,EAC9D,OAAO,gBAAgB,oBAAoB,SAAU,CAAS,EAC9D,EAAe,WAAW,EAC1B,EAAO,QAAQ,WAAW,CAC5B,CACF,CAIA,OAFA,EAAS,MAEI,CACP,IAAU,IAAA,IAAW,qBAAqB,CAAK,EACnD,IAAO,CACT,CAIF,EAAG,CAAC,EAAQ,EAAY,GAAG,CAAI,CAAC,CAClC"}
1
+ {"version":3,"file":"useAnchorTracking.mjs","names":[],"sources":["../../../../package/common/hooks/useAnchorTracking.ts"],"sourcesContent":["import { useLayoutEffect, useRef, type RefObject } from 'react';\nimport { getScrollableAncestors } from 'components/common/utils/scrollAncestors';\n\nexport interface UseAnchorTrackingArgs {\n isOpen: boolean;\n /** The element the popup is anchored to. Its scrollable ancestors are what get tracked. */\n triggerRef: RefObject<HTMLElement | null>;\n /**\n * Every element the positioning maths needs. Tracking starts only once all of them, plus the\n * trigger, are mounted. Read fresh on each setup, so an inline array literal is fine.\n */\n elementRefs: RefObject<HTMLElement | null>[];\n /**\n * Positions the popup. Called once on setup and then on every scroll, resize, tracked size\n * change and end of a CSS animation that moved the trigger, plus whenever it calls `rerun` for\n * an end none of those report. Reads the caller's own refs for everything else.\n */\n update: (trigger: HTMLElement, ancestors: HTMLElement[], rerun: () => void) => void;\n /**\n * Elements whose own size changes must re-run `update`: a popup whose content changes size\n * while open (a filtered list shrinking, an async list loading in) otherwise keeps coordinates\n * computed for its old height and ends up detached from the trigger. Re-read on every update,\n * since a panel can mount a commit later than the element it hangs off.\n */\n resizeRefs?: RefObject<HTMLElement | null>[];\n /** Hands the popup's elements back as they were found. Runs when tracking stops. */\n teardown?: () => void;\n /** Values that must restart tracking, and so re-run `update`, when they change. */\n deps?: readonly unknown[];\n}\n\n/**\n * The lifecycle half of anchoring a portaled, `position: fixed` popup to a trigger: waits for the\n * elements to mount, resolves the trigger's scrolling ancestors once, and re-runs the caller's\n * positioning on every scroll, viewport resize and tracked size change until it closes.\n *\n * Callers supply only the geometry (`update`) and the inline styles to clear again (`teardown`);\n * `usePopoverPosition` is built on this.\n *\n * Position is deliberately written straight to the DOM by the caller rather than through React\n * state: going through setState -> re-render -> commit adds a round-trip that lags behind the\n * browser's own scroll painting by at least a frame.\n */\nexport function useAnchorTracking({\n isOpen,\n triggerRef,\n elementRefs,\n update,\n resizeRefs,\n teardown,\n deps = [],\n}: UseAnchorTrackingArgs) {\n // Latest-value refs, so the effect restarts only for `deps` the caller names, never merely\n // because these were passed as fresh array/function literals this render.\n const latest = useRef({ elementRefs, update, resizeRefs, teardown });\n latest.current = { elementRefs, update, resizeRefs, teardown };\n\n useLayoutEffect(() => {\n if (!isOpen) return;\n\n let rafId: number | undefined;\n let stop: (() => void) | undefined;\n\n // The elements may not be mounted yet on the very first invocation of this effect: a portal\n // target can still be missing (a `#bodyEnd` the app renders, or any while hydrating), and then\n // the portaled content and its ref commits one render later. Since refs never change\n // identity, that later commit doesn't re-trigger this effect on its own, so retry each frame.\n function trySetup() {\n const trigger = triggerRef.current;\n const elements = latest.current.elementRefs;\n if (!trigger || elements.some((ref) => !ref.current)) {\n rafId = requestAnimationFrame(trySetup);\n return;\n }\n const ancestors = getScrollableAncestors(trigger);\n\n const resizeObserver = new ResizeObserver(() => runUpdate());\n const observed = new Set<Element>();\n\n let active = true;\n function runUpdate() {\n // `rerun` may be called after tracking stopped, from a promise that settled late.\n if (!active) return;\n latest.current.update(trigger!, ancestors, runUpdate);\n // After the update, so an element that only mounts during it is picked up too. Once\n // observed this is a handful of Set lookups, so it costs nothing on the scroll path.\n latest.current.resizeRefs?.forEach(({ current }) => {\n if (!current || observed.has(current)) return;\n observed.add(current);\n resizeObserver.observe(current);\n });\n }\n\n runUpdate();\n\n const scrollTargets: (Window | HTMLElement)[] = [window, ...ancestors];\n scrollTargets.forEach((t) => t.addEventListener('scroll', runUpdate, { passive: true }));\n window.addEventListener('resize', runUpdate);\n // `window.resize` doesn't reliably fire when a mobile keyboard opens/closes — visualViewport\n // does, and is what the callers' flip/clamp maths is measured against.\n window.visualViewport?.addEventListener('resize', runUpdate);\n window.visualViewport?.addEventListener('scroll', runUpdate);\n // A transform on an ancestor moves the trigger without any of the events above: a popup\n // opened as a Modal scales in was placed, and hidden, against rects collapsed at scale(0).\n function onAncestorMotionEnd(event: Event) {\n if ((event.target as Node).contains(trigger)) runUpdate();\n }\n document.addEventListener('transitionend', onAncestorMotionEnd, true);\n document.addEventListener('animationend', onAncestorMotionEnd, true);\n\n stop = () => {\n active = false;\n scrollTargets.forEach((t) => t.removeEventListener('scroll', runUpdate));\n window.removeEventListener('resize', runUpdate);\n window.visualViewport?.removeEventListener('resize', runUpdate);\n window.visualViewport?.removeEventListener('scroll', runUpdate);\n document.removeEventListener('transitionend', onAncestorMotionEnd, true);\n document.removeEventListener('animationend', onAncestorMotionEnd, true);\n resizeObserver.disconnect();\n latest.current.teardown?.();\n };\n }\n\n trySetup();\n\n return () => {\n if (rafId !== undefined) cancelAnimationFrame(rafId);\n stop?.();\n };\n // Spread rather than listed: `deps` is the caller's own set of geometry inputs, and changing\n // any of them has to restart tracking so the new values get applied immediately.\n // oxlint-disable-next-line react-hooks/exhaustive-deps\n }, [isOpen, triggerRef, ...deps]);\n}\n"],"mappings":"uIA2CA,SAAgB,EAAkB,CAChC,SACA,aACA,cACA,SACA,aACA,WACA,OAAO,CAAC,GACgB,CAGxB,IAAM,EAAS,EAAO,CAAE,cAAa,SAAQ,aAAY,UAAS,CAAC,EACnE,EAAO,QAAU,CAAE,cAAa,SAAQ,aAAY,UAAS,EAE7D,MAAsB,CACpB,GAAI,CAAC,EAAQ,OAEb,IAAI,EACA,EAMJ,SAAS,GAAW,CAClB,IAAM,EAAU,EAAW,QACrB,EAAW,EAAO,QAAQ,YAChC,GAAI,CAAC,GAAW,EAAS,KAAM,GAAQ,CAAC,EAAI,OAAO,EAAG,CACpD,EAAQ,sBAAsB,CAAQ,EACtC,MACF,CACA,IAAM,EAAY,EAAuB,CAAO,EAE1C,EAAiB,IAAI,mBAAqB,EAAU,CAAC,EACrD,EAAW,IAAI,IAEjB,EAAS,GACb,SAAS,GAAY,CAEd,IACL,EAAO,QAAQ,OAAO,EAAU,EAAW,CAAS,EAGpD,EAAO,QAAQ,YAAY,SAAS,CAAE,aAAc,CAC7C,GAAW,GAAS,IAAI,CAAO,IACpC,EAAS,IAAI,CAAO,EACpB,EAAe,QAAQ,CAAO,EAChC,CAAC,EACH,CAEA,EAAU,EAEV,IAAM,EAA0C,CAAC,OAAQ,GAAG,CAAS,EACrE,EAAc,QAAS,GAAM,EAAE,iBAAiB,SAAU,EAAW,CAAE,QAAS,EAAK,CAAC,CAAC,EACvF,OAAO,iBAAiB,SAAU,CAAS,EAG3C,OAAO,gBAAgB,iBAAiB,SAAU,CAAS,EAC3D,OAAO,gBAAgB,iBAAiB,SAAU,CAAS,EAG3D,SAAS,EAAoB,EAAc,CACpC,EAAM,OAAgB,SAAS,CAAO,GAAG,EAAU,CAC1D,CACA,SAAS,iBAAiB,gBAAiB,EAAqB,EAAI,EACpE,SAAS,iBAAiB,eAAgB,EAAqB,EAAI,EAEnE,MAAa,CACX,EAAS,GACT,EAAc,QAAS,GAAM,EAAE,oBAAoB,SAAU,CAAS,CAAC,EACvE,OAAO,oBAAoB,SAAU,CAAS,EAC9C,OAAO,gBAAgB,oBAAoB,SAAU,CAAS,EAC9D,OAAO,gBAAgB,oBAAoB,SAAU,CAAS,EAC9D,SAAS,oBAAoB,gBAAiB,EAAqB,EAAI,EACvE,SAAS,oBAAoB,eAAgB,EAAqB,EAAI,EACtE,EAAe,WAAW,EAC1B,EAAO,QAAQ,WAAW,CAC5B,CACF,CAIA,OAFA,EAAS,MAEI,CACP,IAAU,IAAA,IAAW,qBAAqB,CAAK,EACnD,IAAO,CACT,CAIF,EAAG,CAAC,EAAQ,EAAY,GAAG,CAAI,CAAC,CAClC"}
@@ -1,2 +1,2 @@
1
- "use client";var e=[`a[href]`,`area[href]`,`button:not([disabled])`,`input:not([disabled])`,`select:not([disabled])`,`textarea:not([disabled])`,`[tabindex]:not([tabindex="-1"])`,`[contenteditable="true"]`].join(`,`);function t(t){return t?Array.from(t.querySelectorAll(e)).filter(e=>e.getAttribute(`tabindex`)!==`-1`):[]}function n(t){return t.matches(e)&&t.getAttribute(`tabindex`)!==`-1`}function r({triggerRef:e,contentRef:r}){function i(){let r=e.current;return r&&n(r)?r:t(r)[0]??r}function a(){i()?.focus()}function o(){let e=t(document.body).filter(e=>!r.current?.contains(e)),n=i(),a=n?e.indexOf(n):-1;a>=0&&e[a+1]?.focus()}return{firstTriggerFocusable:i,restoreFocusToTrigger:a,focusAfterTrigger:o}}export{t as tabbablesIn,r as useFocusBoundary};
1
+ "use client";var e=[`a[href]`,`area[href]`,`button:not([disabled])`,`input:not([disabled])`,`select:not([disabled])`,`textarea:not([disabled])`,`[tabindex]:not([tabindex="-1"])`,`[contenteditable="true"]`].join(`,`);function t(t){return t?Array.from(t.querySelectorAll(e)).filter(e=>e.getAttribute(`tabindex`)!==`-1`):[]}function n(t){return t.matches(e)&&t.getAttribute(`tabindex`)!==`-1`}function r({triggerRef:e,contentRef:r}){function i(){let r=e.current;return r&&n(r)?r:t(r)[0]??r}function a(){i()?.focus()}function o(){let e=i(),n=e?.closest(`[aria-modal="true"]`),a=t(n??document.body).filter(e=>!r.current?.contains(e)),o=e?a.indexOf(e):-1;o<0||(a[o+1]??(n?a[0]:void 0))?.focus()}return{firstTriggerFocusable:i,restoreFocusToTrigger:a,focusAfterTrigger:o}}export{t as tabbablesIn,r as useFocusBoundary};
2
2
  //# sourceMappingURL=useFocusBoundary.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"useFocusBoundary.mjs","names":[],"sources":["../../../../package/common/hooks/useFocusBoundary.ts"],"sourcesContent":["import type { RefObject } from 'react';\n\nconst FOCUSABLE_SELECTOR = [\n 'a[href]',\n 'area[href]',\n 'button:not([disabled])',\n 'input:not([disabled])',\n 'select:not([disabled])',\n 'textarea:not([disabled])',\n '[tabindex]:not([tabindex=\"-1\"])',\n '[contenteditable=\"true\"]',\n].join(',');\n\n/** Tabbable descendants of `root`, in DOM order. */\nexport function tabbablesIn(root: ParentNode | null): HTMLElement[] {\n if (!root) return [];\n return (\n Array.from(root.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR))\n // A button or a link can be taken out of the tab order too, which the\n // selector above only catches for elements whose focusability comes from\n // the attribute. A roving tabindex, where one item of a list is the tab\n // stop and the rest are reachable by arrow key, otherwise reads as a list\n // of tab stops, and the Tab boundary lands on the wrong entry.\n .filter((el) => el.getAttribute('tabindex') !== '-1')\n );\n}\n\nfunction isTabbable(element: HTMLElement): boolean {\n return element.matches(FOCUSABLE_SELECTOR) && element.getAttribute('tabindex') !== '-1';\n}\n\nexport interface UseFocusBoundaryArgs {\n /** The trigger element focus returns to on Escape / Shift+Tab out of the content's top. */\n triggerRef: RefObject<HTMLElement | null>;\n /** The portaled content root — tabbables inside it define the Tab boundary. */\n contentRef: RefObject<HTMLElement | null>;\n}\n\n/**\n * Focus helpers for content portaled to the end of the DOM (e.g. via BodyEnd), where Tab can't\n * reach it in visual document order on its own. Used by Popover and by Menu, which both need to\n * hand focus back to the trigger on Escape / Shift+Tab out of the top, and continue focus\n * naturally past the trigger when Tab exits the bottom.\n */\nexport function useFocusBoundary({ triggerRef, contentRef }: UseFocusBoundaryArgs) {\n function firstTriggerFocusable(): HTMLElement | null {\n const trigger = triggerRef.current;\n // The trigger itself where it takes focus, as an element handed to Popover's `anchor` can;\n // else the first element inside it that does, as inside Popover's and Menu's own wrappers.\n if (trigger && isTabbable(trigger)) return trigger;\n return tabbablesIn(trigger)[0] ?? trigger;\n }\n\n function restoreFocusToTrigger() {\n firstTriggerFocusable()?.focus();\n }\n\n // Move focus to the tabbable element that follows the trigger in the document, skipping the\n // portaled content — the \"natural next\" stop when Tab exits the bottom of the content.\n function focusAfterTrigger() {\n const all = tabbablesIn(document.body).filter((el) => !contentRef.current?.contains(el));\n const trig = firstTriggerFocusable();\n const idx = trig ? all.indexOf(trig) : -1;\n if (idx >= 0) all[idx + 1]?.focus();\n }\n\n return { firstTriggerFocusable, restoreFocusToTrigger, focusAfterTrigger };\n}\n"],"mappings":"aAEA,IAAM,EAAqB,CACzB,UACA,aACA,yBACA,wBACA,yBACA,2BACA,kCACA,0BACF,CAAC,CAAC,KAAK,GAAG,EAGV,SAAgB,EAAY,EAAwC,CAElE,OADK,EAEH,MAAM,KAAK,EAAK,iBAA8B,CAAkB,CAAC,CAAC,CAM/D,OAAQ,GAAO,EAAG,aAAa,UAAU,IAAM,IAAI,EARtC,CAAC,CAUrB,CAEA,SAAS,EAAW,EAA+B,CACjD,OAAO,EAAQ,QAAQ,CAAkB,GAAK,EAAQ,aAAa,UAAU,IAAM,IACrF,CAeA,SAAgB,EAAiB,CAAE,aAAY,cAAoC,CACjF,SAAS,GAA4C,CACnD,IAAM,EAAU,EAAW,QAI3B,OADI,GAAW,EAAW,CAAO,EAAU,EACpC,EAAY,CAAO,CAAC,CAAC,IAAM,CACpC,CAEA,SAAS,GAAwB,CAC/B,EAAsB,CAAC,EAAE,MAAM,CACjC,CAIA,SAAS,GAAoB,CAC3B,IAAM,EAAM,EAAY,SAAS,IAAI,CAAC,CAAC,OAAQ,GAAO,CAAC,EAAW,SAAS,SAAS,CAAE,CAAC,EACjF,EAAO,EAAsB,EAC7B,EAAM,EAAO,EAAI,QAAQ,CAAI,EAAI,GACnC,GAAO,GAAG,EAAI,EAAM,EAAE,EAAE,MAAM,CACpC,CAEA,MAAO,CAAE,wBAAuB,wBAAuB,mBAAkB,CAC3E"}
1
+ {"version":3,"file":"useFocusBoundary.mjs","names":[],"sources":["../../../../package/common/hooks/useFocusBoundary.ts"],"sourcesContent":["import type { RefObject } from 'react';\n\nconst FOCUSABLE_SELECTOR = [\n 'a[href]',\n 'area[href]',\n 'button:not([disabled])',\n 'input:not([disabled])',\n 'select:not([disabled])',\n 'textarea:not([disabled])',\n '[tabindex]:not([tabindex=\"-1\"])',\n '[contenteditable=\"true\"]',\n].join(',');\n\n/** Tabbable descendants of `root`, in DOM order. */\nexport function tabbablesIn(root: ParentNode | null): HTMLElement[] {\n if (!root) return [];\n return (\n Array.from(root.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR))\n // A button or a link can be taken out of the tab order too, which the\n // selector above only catches for elements whose focusability comes from\n // the attribute. A roving tabindex, where one item of a list is the tab\n // stop and the rest are reachable by arrow key, otherwise reads as a list\n // of tab stops, and the Tab boundary lands on the wrong entry.\n .filter((el) => el.getAttribute('tabindex') !== '-1')\n );\n}\n\nfunction isTabbable(element: HTMLElement): boolean {\n return element.matches(FOCUSABLE_SELECTOR) && element.getAttribute('tabindex') !== '-1';\n}\n\nexport interface UseFocusBoundaryArgs {\n /** The trigger element focus returns to on Escape / Shift+Tab out of the content's top. */\n triggerRef: RefObject<HTMLElement | null>;\n /** The portaled content root — tabbables inside it define the Tab boundary. */\n contentRef: RefObject<HTMLElement | null>;\n}\n\n/**\n * Focus helpers for content portaled to the end of the DOM (e.g. via BodyEnd), where Tab can't\n * reach it in visual document order on its own. Used by Popover and by Menu, which both need to\n * hand focus back to the trigger on Escape / Shift+Tab out of the top, and continue focus\n * naturally past the trigger when Tab exits the bottom.\n */\nexport function useFocusBoundary({ triggerRef, contentRef }: UseFocusBoundaryArgs) {\n function firstTriggerFocusable(): HTMLElement | null {\n const trigger = triggerRef.current;\n // The trigger itself where it takes focus, as an element handed to Popover's `anchor` can;\n // else the first element inside it that does, as inside Popover's and Menu's own wrappers.\n if (trigger && isTabbable(trigger)) return trigger;\n return tabbablesIn(trigger)[0] ?? trigger;\n }\n\n function restoreFocusToTrigger() {\n firstTriggerFocusable()?.focus();\n }\n\n // Move focus to the tabbable element that follows the trigger in the document, skipping the\n // portaled content — the \"natural next\" stop when Tab exits the bottom of the content.\n function focusAfterTrigger() {\n const trig = firstTriggerFocusable();\n // Inside a Modal the next stop is the Modal's, wrapping as its trap does: past its last\n // element lies the page behind it, and the focus stayed on the panel being removed.\n const modal = trig?.closest<HTMLElement>('[aria-modal=\"true\"]');\n const all = tabbablesIn(modal ?? document.body).filter((el) => !contentRef.current?.contains(el));\n const idx = trig ? all.indexOf(trig) : -1;\n if (idx < 0) return;\n (all[idx + 1] ?? (modal ? all[0] : undefined))?.focus();\n }\n\n return { firstTriggerFocusable, restoreFocusToTrigger, focusAfterTrigger };\n}\n"],"mappings":"aAEA,IAAM,EAAqB,CACzB,UACA,aACA,yBACA,wBACA,yBACA,2BACA,kCACA,0BACF,CAAC,CAAC,KAAK,GAAG,EAGV,SAAgB,EAAY,EAAwC,CAElE,OADK,EAEH,MAAM,KAAK,EAAK,iBAA8B,CAAkB,CAAC,CAAC,CAM/D,OAAQ,GAAO,EAAG,aAAa,UAAU,IAAM,IAAI,EARtC,CAAC,CAUrB,CAEA,SAAS,EAAW,EAA+B,CACjD,OAAO,EAAQ,QAAQ,CAAkB,GAAK,EAAQ,aAAa,UAAU,IAAM,IACrF,CAeA,SAAgB,EAAiB,CAAE,aAAY,cAAoC,CACjF,SAAS,GAA4C,CACnD,IAAM,EAAU,EAAW,QAI3B,OADI,GAAW,EAAW,CAAO,EAAU,EACpC,EAAY,CAAO,CAAC,CAAC,IAAM,CACpC,CAEA,SAAS,GAAwB,CAC/B,EAAsB,CAAC,EAAE,MAAM,CACjC,CAIA,SAAS,GAAoB,CAC3B,IAAM,EAAO,EAAsB,EAG7B,EAAQ,GAAM,QAAqB,qBAAqB,EACxD,EAAM,EAAY,GAAS,SAAS,IAAI,CAAC,CAAC,OAAQ,GAAO,CAAC,EAAW,SAAS,SAAS,CAAE,CAAC,EAC1F,EAAM,EAAO,EAAI,QAAQ,CAAI,EAAI,GACnC,EAAM,IACT,EAAI,EAAM,KAAO,EAAQ,EAAI,GAAK,IAAA,IAAA,EAAa,MAAM,CACxD,CAEA,MAAO,CAAE,wBAAuB,wBAAuB,mBAAkB,CAC3E"}
@@ -1,2 +1,2 @@
1
- "use client";import{tabbablesIn as e}from"./useFocusBoundary.mjs";import{useEffect as t,useRef as n}from"react";function r({isActive:r,containerRef:o,onEscape:s,trapFocus:c=!0,lockScroll:d=!0}){let f=n(s);t(()=>{f.current=s});let p=n(0);r&&p.current===0&&(p.current=i++),!r&&p.current!==0&&(p.current=0),t(()=>{if(!r)return;let t=p.current;a.add(t);let n=()=>t===Math.max(...a),i=document.activeElement;if(c){let t=o.current;(e(t)[0]??t)?.focus()}function s(t){if(!n())return;if(t.key===`Escape`&&f.current){t.preventDefault(),f.current();return}if(t.key!==`Tab`||!c)return;let r=e(o.current);if(r.length===0){t.preventDefault();return}let i=r[0],a=r[r.length-1],s=document.activeElement;o.current?.contains(s)?!t.shiftKey&&s===a?(t.preventDefault(),i.focus()):t.shiftKey&&s===i&&(t.preventDefault(),a.focus()):(t.preventDefault(),i.focus())}let m=new Set,h=e=>e.code||e.key;function g(e){e.repeat?!m.has(h(e))&&o.current?.contains(e.target)&&(e.preventDefault(),e.stopPropagation()):m.add(h(e))}function _(e){m.delete(h(e))}return window.addEventListener(`keydown`,s),window.addEventListener(`keydown`,g,!0),window.addEventListener(`keyup`,_,!0),d&&l(),()=>{window.removeEventListener(`keydown`,s),window.removeEventListener(`keydown`,g,!0),window.removeEventListener(`keyup`,_,!0),d&&u(),a.delete(t);let e=[...a].some(e=>e>t);c&&!e&&i?.focus()}},[r,c,d,o])}var i=1,a=new Set,o=0,s=``,c=``;function l(){let{body:e}=document;if(o===0){s=e.style.overflow,c=e.style.paddingRight;let t=window.innerWidth-document.documentElement.clientWidth;if(t>0){let n=parseFloat(window.getComputedStyle(e).paddingRight)||0;e.style.paddingRight=`${n+t}px`}e.style.overflow=`hidden`}o+=1}function u(){--o,!(o>0)&&(o=0,document.body.style.overflow=s,document.body.style.paddingRight=c)}export{r as useOverlay};
1
+ "use client";import{tabbablesIn as e}from"./useFocusBoundary.mjs";import{useEffect as t,useRef as n}from"react";function r({isActive:r,containerRef:o,onEscape:s,trapFocus:c=!0,lockScroll:d=!0}){let f=n(s);t(()=>{f.current=s});let p=n(0);r&&p.current===0&&(p.current=i++),!r&&p.current!==0&&(p.current=0),t(()=>{if(!r)return;let t=p.current;a.set(t,{trapFocus:c,containerRef:o});let n=()=>t===Math.max(...a.keys()),i=()=>[...a].filter(([e])=>e>t).map(([,e])=>e),s=document.activeElement;if(c){let t=o.current;(e(t)[0]??t)?.focus()}function m(t){if(t.key===`Escape`&&f.current&&n()){t.preventDefault(),f.current();return}if(t.key!==`Tab`||!c||t.defaultPrevented)return;let r=i();if(r.some(e=>e.trapFocus)||r.some(e=>e.containerRef.current?.contains(document.activeElement)))return;let a=e(o.current);if(a.length===0){t.preventDefault();return}let s=a[0],l=a[a.length-1],u=document.activeElement;o.current?.contains(u)?!t.shiftKey&&u===l?(t.preventDefault(),s.focus()):t.shiftKey&&u===s&&(t.preventDefault(),l.focus()):(t.preventDefault(),s.focus())}let h=new Set,g=e=>e.code||e.key;function _(e){e.repeat?!h.has(g(e))&&o.current?.contains(e.target)&&(e.preventDefault(),e.stopPropagation()):h.add(g(e))}function v(e){h.delete(g(e))}return window.addEventListener(`keydown`,m),window.addEventListener(`keydown`,_,!0),window.addEventListener(`keyup`,v,!0),d&&l(),()=>{window.removeEventListener(`keydown`,m),window.removeEventListener(`keydown`,_,!0),window.removeEventListener(`keyup`,v,!0),d&&u();let e=i().length>0;a.delete(t),c&&!e&&s?.focus()}},[r,c,d,o])}var i=1,a=new Map,o=0,s=``,c=``;function l(){let{body:e}=document;if(o===0){s=e.style.overflow,c=e.style.paddingRight;let t=window.innerWidth-document.documentElement.clientWidth;if(t>0){let n=parseFloat(window.getComputedStyle(e).paddingRight)||0;e.style.paddingRight=`${n+t}px`}e.style.overflow=`hidden`}o+=1}function u(){--o,!(o>0)&&(o=0,document.body.style.overflow=s,document.body.style.paddingRight=c)}export{r as useOverlay};
2
2
  //# sourceMappingURL=useOverlay.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"useOverlay.mjs","names":[],"sources":["../../../../package/common/hooks/useOverlay.ts"],"sourcesContent":["import { useEffect, useRef, type RefObject } from 'react';\nimport { tabbablesIn } from './useFocusBoundary';\n\nexport interface UseOverlayArgs {\n /** Whether the overlay is currently visible. Everything below is set up while this is true. */\n isActive: boolean;\n /** The overlay panel. Focus is trapped inside it and Escape is scoped to it. */\n containerRef: RefObject<HTMLElement | null>;\n /** Called when Escape is pressed while this is the topmost overlay. Omit to not handle Escape. */\n onEscape?: () => void;\n /**\n * Keep Tab inside the panel and restore focus to the previously focused element on close, unless\n * an overlay opened after this one is still open.\n */\n trapFocus?: boolean;\n /** Prevent the page behind the overlay from scrolling. */\n lockScroll?: boolean;\n}\n\n/**\n * The three behaviours every blocking overlay needs: Escape to dismiss, a focus trap, and a\n * scroll lock on the page behind it.\n *\n * They share one module-level stack because they share one question: overlays nest (a\n * ConfirmModal opened from inside a Modal), every instance listens on `document`, and only the\n * innermost one may react. Without the stack, Escape would close every open layer at once and\n * the outer trap would yank focus out of the inner panel on the next Tab.\n *\n * The scroll lock is reference-counted for the same reason: the page stays locked until the last\n * overlay closes, and the body's original inline styles are restored rather than cleared.\n */\nexport function useOverlay({ isActive, containerRef, onEscape, trapFocus = true, lockScroll = true }: UseOverlayArgs) {\n const onEscapeRef = useRef(onEscape);\n useEffect(() => {\n onEscapeRef.current = onEscape;\n });\n\n // The layer's position in the stack is claimed during render, not in the effect below, because\n // effects run child-first: an overlay opened from *inside* another one (a menu in a modal) has\n // its effect run before its own parent's and would come out as the lower layer. Render runs\n // parent-first, which is the order the layers are actually nested in. DOM order is no help\n // either — React commits the inner portal ahead of the outer one.\n const orderRef = useRef(0);\n if (isActive && orderRef.current === 0) orderRef.current = nextLayerOrder++;\n if (!isActive && orderRef.current !== 0) orderRef.current = 0;\n\n useEffect(() => {\n if (!isActive) return;\n\n const order = orderRef.current;\n activeLayers.add(order);\n const isTopmost = () => order === Math.max(...activeLayers);\n\n const previouslyFocused = document.activeElement as HTMLElement | null;\n if (trapFocus) {\n const container = containerRef.current;\n (tabbablesIn(container)[0] ?? container)?.focus();\n }\n\n function onKeyDown(event: KeyboardEvent) {\n if (!isTopmost()) return;\n\n if (event.key === 'Escape' && onEscapeRef.current) {\n event.preventDefault();\n onEscapeRef.current();\n return;\n }\n\n if (event.key !== 'Tab' || !trapFocus) return;\n const focusables = tabbablesIn(containerRef.current);\n // Nothing to move to inside the panel, but Tab must still not escape it.\n if (focusables.length === 0) {\n event.preventDefault();\n return;\n }\n const first = focusables[0];\n const last = focusables[focusables.length - 1];\n const active = document.activeElement;\n if (!containerRef.current?.contains(active)) {\n event.preventDefault();\n first.focus();\n } else if (!event.shiftKey && active === last) {\n event.preventDefault();\n first.focus();\n } else if (event.shiftKey && active === first) {\n event.preventDefault();\n last.focus();\n }\n }\n\n // A key held as this opened would repeat into what it focused: a held Enter pressed Close.\n // Outside the panel it still repeats, since suggestions open while the user types.\n const pressedSinceOpen = new Set<string>();\n const keyId = (event: KeyboardEvent) => event.code || event.key;\n function onKeyDownCapture(event: KeyboardEvent) {\n if (!event.repeat) pressedSinceOpen.add(keyId(event));\n else if (!pressedSinceOpen.has(keyId(event)) && containerRef.current?.contains(event.target as Node)) {\n event.preventDefault();\n event.stopPropagation();\n }\n }\n function onKeyUp(event: KeyboardEvent) {\n pressedSinceOpen.delete(keyId(event));\n }\n\n // `window`, not `document`: a key event on a focused element reaches both, but a synthetic\n // one dispatched straight at `window` (as tests and some consumers do) never reaches\n // `document`, since it sits above it in the propagation path.\n window.addEventListener('keydown', onKeyDown);\n window.addEventListener('keydown', onKeyDownCapture, true);\n window.addEventListener('keyup', onKeyUp, true);\n if (lockScroll) acquireScrollLock();\n\n return () => {\n window.removeEventListener('keydown', onKeyDown);\n window.removeEventListener('keydown', onKeyDownCapture, true);\n window.removeEventListener('keyup', onKeyUp, true);\n if (lockScroll) releaseScrollLock();\n activeLayers.delete(order);\n // Modal closes a commit late, so a Popover opened by the same click already holds the focus.\n const coveredByLaterLayer = [...activeLayers].some((other) => other > order);\n if (trapFocus && !coveredByLaterLayer) previouslyFocused?.focus();\n };\n }, [isActive, trapFocus, lockScroll, containerRef]);\n}\n\n/** Monotonic, so a layer opened later always outranks one opened earlier. */\nlet nextLayerOrder = 1;\nconst activeLayers = new Set<number>();\n\nlet scrollLockCount = 0;\nlet previousOverflow = '';\nlet previousPaddingRight = '';\n\nfunction acquireScrollLock() {\n const { body } = document;\n if (scrollLockCount === 0) {\n previousOverflow = body.style.overflow;\n previousPaddingRight = body.style.paddingRight;\n // Removing the page's scrollbar reclaims its width, so everything behind the overlay would\n // shift right by that amount without the compensating padding.\n const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;\n if (scrollbarWidth > 0) {\n const current = parseFloat(window.getComputedStyle(body).paddingRight) || 0;\n body.style.paddingRight = `${current + scrollbarWidth}px`;\n }\n body.style.overflow = 'hidden';\n }\n scrollLockCount += 1;\n}\n\nfunction releaseScrollLock() {\n scrollLockCount -= 1;\n if (scrollLockCount > 0) return;\n scrollLockCount = 0;\n document.body.style.overflow = previousOverflow;\n document.body.style.paddingRight = previousPaddingRight;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"useOverlay.mjs","names":[],"sources":["../../../../package/common/hooks/useOverlay.ts"],"sourcesContent":["import { useEffect, useRef, type RefObject } from 'react';\nimport { tabbablesIn } from './useFocusBoundary';\n\nexport interface UseOverlayArgs {\n /** Whether the overlay is currently visible. Everything below is set up while this is true. */\n isActive: boolean;\n /** The overlay panel. Focus is trapped inside it and Escape is scoped to it. */\n containerRef: RefObject<HTMLElement | null>;\n /** Called when Escape is pressed while this is the topmost overlay. Omit to not handle Escape. */\n onEscape?: () => void;\n /**\n * Keep Tab inside the panel and restore focus to the previously focused element on close, unless\n * an overlay opened after this one is still open.\n */\n trapFocus?: boolean;\n /** Prevent the page behind the overlay from scrolling. */\n lockScroll?: boolean;\n}\n\n/**\n * The three behaviours every blocking overlay needs: Escape to dismiss, a focus trap, and a\n * scroll lock on the page behind it.\n *\n * They share one module-level stack because they share one question: overlays nest (a\n * ConfirmModal opened from inside a Modal), every instance listens on `document`, and only the\n * innermost one may react. Without the stack, Escape would close every open layer at once and\n * the outer trap would yank focus out of the inner panel on the next Tab.\n *\n * The scroll lock is reference-counted for the same reason: the page stays locked until the last\n * overlay closes, and the body's original inline styles are restored rather than cleared.\n */\nexport function useOverlay({ isActive, containerRef, onEscape, trapFocus = true, lockScroll = true }: UseOverlayArgs) {\n const onEscapeRef = useRef(onEscape);\n useEffect(() => {\n onEscapeRef.current = onEscape;\n });\n\n // The layer's position in the stack is claimed during render, not in the effect below, because\n // effects run child-first: an overlay opened from *inside* another one (a menu in a modal) has\n // its effect run before its own parent's and would come out as the lower layer. Render runs\n // parent-first, which is the order the layers are actually nested in. DOM order is no help\n // either — React commits the inner portal ahead of the outer one.\n const orderRef = useRef(0);\n if (isActive && orderRef.current === 0) orderRef.current = nextLayerOrder++;\n if (!isActive && orderRef.current !== 0) orderRef.current = 0;\n\n useEffect(() => {\n if (!isActive) return;\n\n const order = orderRef.current;\n activeLayers.set(order, { trapFocus, containerRef });\n const isTopmost = () => order === Math.max(...activeLayers.keys());\n const layersAbove = () => [...activeLayers].filter(([other]) => other > order).map(([, layer]) => layer);\n\n const previouslyFocused = document.activeElement as HTMLElement | null;\n if (trapFocus) {\n const container = containerRef.current;\n (tabbablesIn(container)[0] ?? container)?.focus();\n }\n\n function onKeyDown(event: KeyboardEvent) {\n if (event.key === 'Escape' && onEscapeRef.current && isTopmost()) {\n event.preventDefault();\n onEscapeRef.current();\n return;\n }\n\n if (event.key !== 'Tab' || !trapFocus || event.defaultPrevented) return;\n // A layer that does not trap, a Popover, leaves Tab to the trap below it: with nothing to\n // focus in its panel, Tab otherwise left the Modal for the browser's own controls. Tab\n // inside it stays its own, and so does the move it already made out of either end.\n const above = layersAbove();\n if (above.some((layer) => layer.trapFocus)) return;\n if (above.some((layer) => layer.containerRef.current?.contains(document.activeElement))) return;\n const focusables = tabbablesIn(containerRef.current);\n // Nothing to move to inside the panel, but Tab must still not escape it.\n if (focusables.length === 0) {\n event.preventDefault();\n return;\n }\n const first = focusables[0];\n const last = focusables[focusables.length - 1];\n const active = document.activeElement;\n if (!containerRef.current?.contains(active)) {\n event.preventDefault();\n first.focus();\n } else if (!event.shiftKey && active === last) {\n event.preventDefault();\n first.focus();\n } else if (event.shiftKey && active === first) {\n event.preventDefault();\n last.focus();\n }\n }\n\n // A key held as this opened would repeat into what it focused: a held Enter pressed Close.\n // Outside the panel it still repeats, since suggestions open while the user types.\n const pressedSinceOpen = new Set<string>();\n const keyId = (event: KeyboardEvent) => event.code || event.key;\n function onKeyDownCapture(event: KeyboardEvent) {\n if (!event.repeat) pressedSinceOpen.add(keyId(event));\n else if (!pressedSinceOpen.has(keyId(event)) && containerRef.current?.contains(event.target as Node)) {\n event.preventDefault();\n event.stopPropagation();\n }\n }\n function onKeyUp(event: KeyboardEvent) {\n pressedSinceOpen.delete(keyId(event));\n }\n\n // `window`, not `document`: a key event on a focused element reaches both, but a synthetic\n // one dispatched straight at `window` (as tests and some consumers do) never reaches\n // `document`, since it sits above it in the propagation path.\n window.addEventListener('keydown', onKeyDown);\n window.addEventListener('keydown', onKeyDownCapture, true);\n window.addEventListener('keyup', onKeyUp, true);\n if (lockScroll) acquireScrollLock();\n\n return () => {\n window.removeEventListener('keydown', onKeyDown);\n window.removeEventListener('keydown', onKeyDownCapture, true);\n window.removeEventListener('keyup', onKeyUp, true);\n if (lockScroll) releaseScrollLock();\n // Modal closes a commit late, so a Popover opened by the same click already holds the focus.\n const coveredByLaterLayer = layersAbove().length > 0;\n activeLayers.delete(order);\n if (trapFocus && !coveredByLaterLayer) previouslyFocused?.focus();\n };\n }, [isActive, trapFocus, lockScroll, containerRef]);\n}\n\n/** Monotonic, so a layer opened later always outranks one opened earlier. */\nlet nextLayerOrder = 1;\nconst activeLayers = new Map<number, { trapFocus: boolean; containerRef: RefObject<HTMLElement | null> }>();\n\nlet scrollLockCount = 0;\nlet previousOverflow = '';\nlet previousPaddingRight = '';\n\nfunction acquireScrollLock() {\n const { body } = document;\n if (scrollLockCount === 0) {\n previousOverflow = body.style.overflow;\n previousPaddingRight = body.style.paddingRight;\n // Removing the page's scrollbar reclaims its width, so everything behind the overlay would\n // shift right by that amount without the compensating padding.\n const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;\n if (scrollbarWidth > 0) {\n const current = parseFloat(window.getComputedStyle(body).paddingRight) || 0;\n body.style.paddingRight = `${current + scrollbarWidth}px`;\n }\n body.style.overflow = 'hidden';\n }\n scrollLockCount += 1;\n}\n\nfunction releaseScrollLock() {\n scrollLockCount -= 1;\n if (scrollLockCount > 0) return;\n scrollLockCount = 0;\n document.body.style.overflow = previousOverflow;\n document.body.style.paddingRight = previousPaddingRight;\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"modal.module.mjs","names":[],"sources":["../../../../package/common/modal/modal.module.pcss"],"sourcesContent":[".modal {\n /* Theme tokens resolved once, each with a built-in fallback. Set any of the public\n `--modal-*` variables via ThemeProvider (theme-wide) or `style` (per instance) to\n override; unset ones fall back to the values below. */\n --custom-modal-padding: var(--modal-padding, var(--spacing-l));\n --custom-modal-gap: var(--modal-gap, var(--spacing-l));\n\n /* Half the gap, so the scroll port's clipping edge — where the divider sits — lands midway\n between the chrome and where the content rests, with the same room either side. Flush\n against the chrome (bleed = gap) reads as a sliced line of text colliding with the title;\n an unrelated value (this was `0.6em`) puts it at a distance that drifts with the font size. */\n --custom-modal-body-bleed: var(--modal-body-bleed, calc(var(--custom-modal-gap) / 2));\n\n /* The panel never comes closer to the screen edge than its own padding, at every size. A\n percentage instead reads as a different inset on every screen, and pairing one with a\n breakpoint made the two axes disagree: 5% of a 390px width is 19.5px but 5% of a 780px\n height is 39px, so the same rule came out narrower and taller than the default on one phone. */\n --custom-modal-max-width: var(--modal-max-width, calc(100% - 2 * var(--custom-modal-padding)));\n --custom-modal-max-height: var(--modal-max-height, calc(100% - 2 * var(--custom-modal-padding)));\n\n padding: var(--custom-modal-padding);\n border-radius: var(--default-border-radius);\n background: var(--background-modal);\n color: var(--text-color);\n font-family: var(--default-font);\n box-sizing: border-box;\n position: relative;\n display: flex;\n flex-direction: column;\n transform-origin: bottom center;\n animation:\n slideIn 0.2s,\n fadeIn 0.3s forwards;\n max-width: var(--custom-modal-max-width);\n max-height: var(--custom-modal-max-height);\n\n /* The panel itself never scrolls, so the header stays put; `&-body` is what scrolls. */\n overflow: hidden;\n\n /* A sheet-presented modal carries forms and detail views, not a short menu, so it takes more\n of the screen than the shared default — and keeps the panel's own padding at the bottom edge. */\n &-sheet {\n --sheet-max-height: 90dvh;\n --sheet-safe-padding: var(--custom-modal-padding);\n }\n\n &-container {\n position: fixed;\n width: 100%;\n height: 100%;\n display: flex;\n justify-content: center;\n align-items: center;\n top: 0;\n left: 0;\n }\n\n &-header {\n display: flex;\n gap: var(--spacing-m);\n justify-content: space-between;\n align-items: center;\n margin-bottom: var(--custom-modal-gap);\n flex: 0 0 auto;\n\n div {\n overflow: hidden;\n text-overflow: ellipsis;\n }\n }\n\n &-body {\n /* `min-height: 0` lets this shrink below its content's height so it, not the panel, scrolls. */\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n\n /* A negative margin cancelled by an equal padding, so a focus ring or a floating label at\n the body's edge isn't clipped by the scroll container. Override the pair with\n `--modal-body-bleed`, never with a plain `padding`, which drops the negative half and\n brings the clipping back. See `docs/Modal.md` for what the two do together. */\n margin: calc(-1 * var(--custom-modal-body-bleed));\n padding: var(--custom-modal-body-bleed);\n\n /* The mask lives in `styles/scrollFade.module.pcss`; the component applies it along with the\n modifiers saying which end genuinely hides content. The run has to clear the port's own\n padding before any content fades at all, hence gap + bleed. Set here as the shared sheet's\n private length rather than as `--scroll-fade-length`, which would inherit down into a\n `ScrollFade` nested in the body. */\n --custom-scroll-fade-length: var(\n --modal-scroll-fade,\n calc(var(--custom-modal-gap) + var(--custom-modal-body-bleed))\n );\n }\n\n &-footer {\n margin-top: var(--custom-modal-gap);\n flex: 0 0 auto;\n }\n\n &-close-button {\n flex-shrink: 0;\n background: none;\n border: none;\n padding: 0;\n color: inherit;\n font-size: inherit;\n line-height: 1;\n cursor: pointer;\n -webkit-tap-highlight-color: transparent;\n }\n\n &-wrapper {\n position: fixed;\n width: 100%;\n height: 100%;\n background: #00000052;\n top: 0;\n left: 0;\n transition: all 0.3s ease-in-out;\n opacity: 0;\n\n &-shown {\n opacity: 1;\n }\n }\n}\n\n@keyframes slideIn {\n from {\n transform: scale(0);\n }\n\n to {\n transform: scale(0.99);\n }\n}\n\n@keyframes fadeIn {\n from {\n opacity: 0;\n }\n\n to {\n opacity: 1;\n }\n}\n\n/* Only the scale-in goes; the backdrop and the panel still fade, which is not motion.\n `slideIn` ends at scale(0.99) and the panel has no transform of its own, so dropping the\n animation lands it at scale(1) rather than leaving it collapsed. */\n@media (prefers-reduced-motion: reduce) {\n .modal {\n animation: fadeIn 0.3s forwards;\n }\n}\n"],"mappings":""}
1
+ {"version":3,"file":"modal.module.mjs","names":[],"sources":["../../../../package/common/modal/modal.module.pcss"],"sourcesContent":[".modal {\n /* Theme tokens resolved once, each with a built-in fallback. Set any of the public\n `--modal-*` variables via ThemeProvider (theme-wide) or `style` (per instance) to\n override; unset ones fall back to the values below. */\n --custom-modal-padding: var(--modal-padding, var(--spacing-l));\n --custom-modal-gap: var(--modal-gap, var(--spacing-l));\n\n /* Half the gap, so the scroll port's clipping edge — where the divider sits — lands midway\n between the chrome and where the content rests, with the same room either side. Flush\n against the chrome (bleed = gap) reads as a sliced line of text colliding with the title;\n an unrelated value (this was `0.6em`) puts it at a distance that drifts with the font size. */\n --custom-modal-body-bleed: var(--modal-body-bleed, calc(var(--custom-modal-gap) / 2));\n\n /* The panel never comes closer to the screen edge than its own padding, at every size. A\n percentage instead reads as a different inset on every screen, and pairing one with a\n breakpoint made the two axes disagree: 5% of a 390px width is 19.5px but 5% of a 780px\n height is 39px, so the same rule came out narrower and taller than the default on one phone. */\n --custom-modal-max-width: var(--modal-max-width, calc(100% - 2 * var(--custom-modal-padding)));\n --custom-modal-max-height: var(--modal-max-height, calc(100% - 2 * var(--custom-modal-padding)));\n\n padding: var(--custom-modal-padding);\n border-radius: var(--default-border-radius);\n background: var(--background-modal);\n color: var(--text-color);\n font-family: var(--default-font);\n box-sizing: border-box;\n position: relative;\n display: flex;\n flex-direction: column;\n transform-origin: bottom center;\n animation:\n slideIn 0.2s,\n fadeIn 0.3s forwards;\n max-width: var(--custom-modal-max-width);\n max-height: var(--custom-modal-max-height);\n\n /* The panel itself never scrolls, so the header stays put; `&-body` is what scrolls. */\n overflow: hidden;\n\n /* A sheet-presented modal carries forms and detail views, not a short menu, so it takes more\n of the screen than the shared default — and keeps the panel's own padding at the bottom edge. */\n &-sheet {\n --sheet-max-height: 90dvh;\n --sheet-safe-padding: var(--custom-modal-padding);\n }\n\n &-container {\n position: fixed;\n width: 100%;\n height: 100%;\n display: flex;\n justify-content: center;\n align-items: center;\n top: 0;\n left: 0;\n\n /* Shared by every overlay, so the order they are portaled in, which is the order they opened in, decides. */\n z-index: 1;\n }\n\n &-header {\n display: flex;\n gap: var(--spacing-m);\n justify-content: space-between;\n align-items: center;\n margin-bottom: var(--custom-modal-gap);\n flex: 0 0 auto;\n\n div {\n overflow: hidden;\n text-overflow: ellipsis;\n }\n }\n\n &-body {\n /* `min-height: 0` lets this shrink below its content's height so it, not the panel, scrolls. */\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n\n /* A negative margin cancelled by an equal padding, so a focus ring or a floating label at\n the body's edge isn't clipped by the scroll container. Override the pair with\n `--modal-body-bleed`, never with a plain `padding`, which drops the negative half and\n brings the clipping back. See `docs/Modal.md` for what the two do together. */\n margin: calc(-1 * var(--custom-modal-body-bleed));\n padding: var(--custom-modal-body-bleed);\n\n /* The mask lives in `styles/scrollFade.module.pcss`; the component applies it along with the\n modifiers saying which end genuinely hides content. The run has to clear the port's own\n padding before any content fades at all, hence gap + bleed. Set here as the shared sheet's\n private length rather than as `--scroll-fade-length`, which would inherit down into a\n `ScrollFade` nested in the body. */\n --custom-scroll-fade-length: var(\n --modal-scroll-fade,\n calc(var(--custom-modal-gap) + var(--custom-modal-body-bleed))\n );\n }\n\n &-footer {\n margin-top: var(--custom-modal-gap);\n flex: 0 0 auto;\n }\n\n &-close-button {\n flex-shrink: 0;\n background: none;\n border: none;\n padding: 0;\n color: inherit;\n font-size: inherit;\n line-height: 1;\n cursor: pointer;\n -webkit-tap-highlight-color: transparent;\n }\n\n &-wrapper {\n position: fixed;\n width: 100%;\n height: 100%;\n background: #00000052;\n top: 0;\n left: 0;\n transition: all 0.3s ease-in-out;\n opacity: 0;\n\n &-shown {\n opacity: 1;\n }\n }\n}\n\n@keyframes slideIn {\n from {\n transform: scale(0);\n }\n\n to {\n transform: scale(0.99);\n }\n}\n\n@keyframes fadeIn {\n from {\n opacity: 0;\n }\n\n to {\n opacity: 1;\n }\n}\n\n/* Only the scale-in goes; the backdrop and the panel still fade, which is not motion.\n `slideIn` ends at scale(0.99) and the panel has no transform of its own, so dropping the\n animation lands it at scale(1) rather than leaving it collapsed. */\n@media (prefers-reduced-motion: reduce) {\n .modal {\n animation: fadeIn 0.3s forwards;\n }\n}\n"],"mappings":""}
@@ -1,2 +1,2 @@
1
- "use client";import{useComponentDefaults as e}from"../configProvider/useComponentDefaults.mjs";import{usePresentationDefault as t}from"../configProvider/usePresentationDefault.mjs";import{Presentation as n}from"../configProvider/presentation.types.mjs";import r from"../bodyEnd/bodyEnd.mjs";import i from"./popover.module.mjs";import a from"../styles/sheet.module.mjs";import ee from"../sheetBackdrop/sheetBackdrop.mjs";import{PopoverAlign as te,PopoverPlacement as ne,PopoverTrigger as o}from"./popover.types.mjs";import{usePopoverPosition as re}from"./usePopoverPosition.mjs";import{tabbablesIn as s,useFocusBoundary as ie}from"../hooks/useFocusBoundary.mjs";import ae from"../hooks/useSheet.mjs";import{useOverlay as c}from"../hooks/useOverlay.mjs";import{useCallback as l,useEffect as u,useRef as d,useState as f}from"react";import p from"classnames";import{jsx as m,jsxs as h}from"react/jsx-runtime";var oe=1e3;function g(e,t){return e?{current:e}:t}function _(_){let{children:se=null,anchor:v,content:ce=null,placement:y=ne.Bottom,align:le=te.Center,trigger:b=o.Click,isOpen:x,defaultOpen:ue=!1,onOpenChange:S=()=>{},offset:C=10,withArrow:w=!0,closeOnOutsideClick:T=!0,closeOnEscape:de=!0,openDelay:E=100,closeDelay:fe=120,disabled:D=!1,focusOnOpen:O=`keyboard`,closeOnTriggerClick:pe=!0,width:me=`auto`,constrainWidth:he=!0,presentation:ge,className:_e,style:ve,classNames:k,styles:A,...ye}=e(`Popover`,_),j=d(null),M=d(null),N=d(null),P=d(null),F=d(void 0),I=d(!1),L=d(!1),R=d(!1),z=d(null);function B(){I.current=!0}function be(e){B(),z.current=e.target}let[V,xe]=f(()=>({anchor:v,ref:g(v,j)}));V.anchor!==v&&xe({anchor:v,ref:g(v,j)});let H=V.ref,U=d(null),W=v!==void 0;u(()=>{if(!W)return;function e(e){U.current={target:e.target,keyboard:e.detail===0,time:performance.now()}}return document.addEventListener(`click`,e,!0),()=>document.removeEventListener(`click`,e,!0)},[W]);let G=x!==void 0,[Se,Ce]=f(ue),K=G?x:Se,q=d(S);u(()=>{q.current=S});let J=l((e,t=!1)=>{D&&e||(e&&(R.current=t),G||Ce(e),q.current(e,{fromKeyboard:t}))},[D,G]),we=t(),Te=ge??we??n.Default,{isSheet:Ee,sheetStyle:De}=ae(Te),Y=Ee&&b===o.Click;re({isOpen:!!K&&!Y,placement:y,align:le,offset:C,triggerRef:H,floatingRef:M,panelRef:N,arrowRef:w?P:{current:null},matchTriggerWidth:me===`trigger`});let{restoreFocusToTrigger:X,focusAfterTrigger:Oe}=ie({triggerRef:H,contentRef:M}),Z=d(X);u(()=>{Z.current=X});let Q=d(!1),ke=l(e=>{if(M.current=e,Q.current=!1,e)return()=>{M.current=null;let t=document.activeElement;Q.current=!!t&&(e.contains(t)||t===z.current)}},[]);u(()=>{if(K||!Q.current)return;Q.current=!1;let e=document.activeElement;(!e||e===document.body)&&Z.current()},[K]),c({isActive:!!K,containerRef:N,onEscape:de?()=>{M.current?.contains(document.activeElement)&&X(),J(!1,!0)}:void 0,trapFocus:!1,lockScroll:Y}),u(()=>{if(!K){R.current=!1;return}let e=U.current;U.current=null;let t=!!v&&!!e?.keyboard&&v.contains(e.target)&&performance.now()-e.time<oe;if(!(O===!0||O===`keyboard`&&(R.current||t))||b!==o.Click)return;let n=s(N.current);n.length!==0&&(M.current?.setAttribute(`role`,`dialog`),n[0].focus())},[K,b,O,v]);function Ae(e){if(e.key!==`Tab`||b!==o.Click)return;let t=s(N.current);if(t.length===0)return;let n=document.activeElement;!e.shiftKey&&n===t[t.length-1]?(e.preventDefault(),Oe(),J(!1,!0)):e.shiftKey&&n===t[0]&&(e.preventDefault(),X(),J(!1,!0))}u(()=>{if(!K)return;I.current=!1,L.current=!1;function e(e){return!!(M.current?.contains(e)||j.current?.contains(e)||H.current?.contains(e))}function t(t){let n=I.current;return I.current=!1,n||e(t.target)}function n(e){L.current=!t(e)}function r(e){let n=L.current;L.current=!1,!t(e)&&n&&J(!1)}function i(n){if(t(n))return;let r=n.relatedTarget;r&&(r===z.current||e(r))&&J(!1,!L.current)}return T&&b!==o.Hover&&(window.addEventListener(`mousedown`,n),window.addEventListener(`mouseup`,r),document.addEventListener(`focusin`,i)),()=>{window.removeEventListener(`mousedown`,n),window.removeEventListener(`mouseup`,r),document.removeEventListener(`focusin`,i)}},[K,T,b,J,H]),u(()=>()=>clearTimeout(F.current),[]);function $(e){clearTimeout(F.current),F.current=setTimeout(()=>J(e),e?E:fe)}function je(e){K||e.key!==`ArrowDown`&&e.key!==`ArrowUp`||(e.preventDefault(),J(!0,!0))}let Me=b===o.Click?{onClick:e=>{(!K||pe)&&J(!K,e.detail===0)},onKeyDown:je}:b===o.Hover?{onMouseEnter:()=>$(!0),onMouseLeave:()=>$(!1)}:{onFocus:()=>J(!0),onBlur:()=>J(!1)},Ne=b===o.Hover?{onMouseEnter:()=>$(!0),onMouseLeave:()=>$(!1)}:{};return h(`div`,{...ye,className:p(i.popover,_e,k?.root),style:{...ve,...A?.root},children:[m(`div`,{ref:j,className:p(i.popoverTrigger,k?.trigger),style:A?.trigger,...Me,children:se}),K&&h(r,{children:[Y&&m(ee,{onClose:()=>{T&&J(!1)},className:k?.backdrop,style:A?.backdrop}),h(`div`,{ref:ke,className:p(Y?a.sheet:i.popoverFloating,k?.floating),style:{...Y?De:void 0,...A?.floating},"data-placement":y,onKeyDown:Ae,onMouseDownCapture:B,onMouseUpCapture:B,onFocusCapture:be,...Ne,children:[m(`div`,{ref:N,className:p(i.popoverPanel,he&&i.popoverPanelConstrained,Y&&a.sheetPanel,k?.panel),style:A?.panel,children:ce}),w&&!Y&&m(`div`,{ref:P,className:p(i.popoverArrow,k?.arrow),style:A?.arrow})]})]})]})}export{_ as default};
1
+ "use client";import{useComponentDefaults as e}from"../configProvider/useComponentDefaults.mjs";import{usePresentationDefault as t}from"../configProvider/usePresentationDefault.mjs";import{Presentation as n}from"../configProvider/presentation.types.mjs";import r from"../bodyEnd/bodyEnd.mjs";import i from"./popover.module.mjs";import a from"../styles/sheet.module.mjs";import ee from"../sheetBackdrop/sheetBackdrop.mjs";import{PopoverAlign as te,PopoverPlacement as ne,PopoverTrigger as o}from"./popover.types.mjs";import{usePopoverPosition as re}from"./usePopoverPosition.mjs";import{tabbablesIn as s,useFocusBoundary as ie}from"../hooks/useFocusBoundary.mjs";import ae from"../hooks/useSheet.mjs";import{useOverlay as oe}from"../hooks/useOverlay.mjs";import{useCallback as c,useEffect as l,useRef as u,useState as d}from"react";import f from"classnames";import{jsx as p,jsxs as m}from"react/jsx-runtime";var se=1e3;function h(e,t){return e?{current:e}:t}function g(g){let{children:ce=null,anchor:_,content:le=null,placement:v=ne.Bottom,align:ue=te.Center,trigger:y=o.Click,isOpen:b,defaultOpen:de=!1,onOpenChange:x=()=>{},offset:fe=10,withArrow:S=!0,closeOnOutsideClick:C=!0,closeOnEscape:pe=!0,openDelay:me=100,closeDelay:he=120,disabled:w=!1,focusOnOpen:T=`keyboard`,closeOnTriggerClick:ge=!0,width:_e=`auto`,constrainWidth:ve=!0,presentation:ye,className:be,style:xe,classNames:E,styles:D,...Se}=e(`Popover`,g),O=u(null),k=u(null),A=u(null),j=u(null),M=u(void 0),N=u(!1),P=u(!1),F=u(!1),I=u(null);function L(){N.current=!0}function Ce(e){L(),I.current=e.target}let[R,we]=d(()=>({anchor:_,ref:h(_,O)}));R.anchor!==_&&we({anchor:_,ref:h(_,O)});let z=R.ref,B=u(null),V=_!==void 0;l(()=>{if(!V)return;function e(e){B.current={target:e.target,keyboard:e.detail===0,time:performance.now()}}return document.addEventListener(`click`,e,!0),()=>document.removeEventListener(`click`,e,!0)},[V]);let H=b!==void 0,[Te,Ee]=d(de),U=H?b:Te,W=u(x);l(()=>{W.current=x});let G=c((e,t=!1)=>{w&&e||(e&&(F.current=t),H||Ee(e),W.current(e,{fromKeyboard:t}))},[w,H]),De=t(),Oe=ye??De??n.Default,{isSheet:ke,sheetStyle:Ae}=ae(Oe),K=ke&&y===o.Click,q=u(!1),J=u(!1);function Y(){J.current=!1;let e=s(A.current);e.length!==0&&(k.current?.setAttribute(`role`,`dialog`),e[0].focus())}re({isOpen:!!U&&!K,placement:v,align:ue,offset:fe,triggerRef:z,floatingRef:k,panelRef:A,arrowRef:S?j:{current:null},matchTriggerWidth:_e===`trigger`,onShown:()=>{q.current=!0,J.current&&Y()}});let{restoreFocusToTrigger:X,focusAfterTrigger:je}=ie({triggerRef:z,contentRef:k}),Z=u(X);l(()=>{Z.current=X});let Q=u(!1),Me=c(e=>{if(k.current=e,Q.current=!1,e)return()=>{k.current=null;let t=document.activeElement;Q.current=!!t&&(e.contains(t)||t===I.current)}},[]);l(()=>{if(U||!Q.current)return;Q.current=!1;let e=document.activeElement;(!e||e===document.body)&&Z.current()},[U]),oe({isActive:!!U,containerRef:A,onEscape:pe?()=>{k.current?.contains(document.activeElement)&&X(),G(!1,!0)}:void 0,trapFocus:!1,lockScroll:K}),l(()=>{if(!U){F.current=!1,q.current=!1,J.current=!1;return}let e=B.current;B.current=null;let t=!!_&&!!e?.keyboard&&_.contains(e.target)&&performance.now()-e.time<se;(T===!0||T===`keyboard`&&(F.current||t))&&y===o.Click&&(q.current?Y():J.current=!0)},[U,y,T,_]);function Ne(e){if(e.key!==`Tab`||y!==o.Click)return;let t=s(A.current);if(t.length===0)return;let n=document.activeElement;!e.shiftKey&&n===t[t.length-1]?(e.preventDefault(),je(),G(!1,!0)):e.shiftKey&&n===t[0]&&(e.preventDefault(),X(),G(!1,!0))}l(()=>{if(!U)return;N.current=!1,P.current=!1;function e(e){return!!(k.current?.contains(e)||O.current?.contains(e)||z.current?.contains(e))}function t(t){let n=N.current;return N.current=!1,n||e(t.target)}function n(e){P.current=!t(e)}function r(e){let n=P.current;P.current=!1,!t(e)&&n&&G(!1)}function i(n){if(t(n))return;let r=n.relatedTarget;r&&(r===I.current||e(r))&&G(!1,!P.current)}return C&&y!==o.Hover&&(window.addEventListener(`mousedown`,n),window.addEventListener(`mouseup`,r),document.addEventListener(`focusin`,i)),()=>{window.removeEventListener(`mousedown`,n),window.removeEventListener(`mouseup`,r),document.removeEventListener(`focusin`,i)}},[U,C,y,G,z]),l(()=>()=>clearTimeout(M.current),[]);function $(e){clearTimeout(M.current),M.current=setTimeout(()=>G(e),e?me:he)}function Pe(e){U||e.key!==`ArrowDown`&&e.key!==`ArrowUp`||(e.preventDefault(),G(!0,!0))}let Fe=y===o.Click?{onClick:e=>{(!U||ge)&&G(!U,e.detail===0)},onKeyDown:Pe}:y===o.Hover?{onMouseEnter:()=>$(!0),onMouseLeave:()=>$(!1)}:{onFocus:()=>G(!0),onBlur:()=>G(!1)},Ie=y===o.Hover?{onMouseEnter:()=>$(!0),onMouseLeave:()=>$(!1)}:{};return m(`div`,{...Se,className:f(i.popover,be,E?.root),style:{...xe,...D?.root},children:[p(`div`,{ref:O,className:f(i.popoverTrigger,E?.trigger),style:D?.trigger,...Fe,children:ce}),U&&m(r,{children:[K&&p(ee,{onClose:()=>{C&&G(!1)},className:E?.backdrop,style:D?.backdrop}),m(`div`,{ref:Me,className:f(K?a.sheet:i.popoverFloating,E?.floating),style:{...K?Ae:void 0,...D?.floating},"data-placement":v,onKeyDown:Ne,onMouseDownCapture:L,onMouseUpCapture:L,onFocusCapture:Ce,...Ie,children:[p(`div`,{ref:A,className:f(i.popoverPanel,ve&&i.popoverPanelConstrained,K&&a.sheetPanel,E?.panel),style:D?.panel,children:le}),S&&!K&&p(`div`,{ref:j,className:f(i.popoverArrow,E?.arrow),style:D?.arrow})]})]})]})}export{g as default};
2
2
  //# sourceMappingURL=popover.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"popover.mjs","names":[],"sources":["../../../../package/common/popover/popover.tsx"],"sourcesContent":["import { useRef, useEffect, useState, useCallback, type RefObject } from 'react';\nimport cx from 'classnames';\nimport BodyEnd from 'components/common/bodyEnd';\n\nimport styles from './popover.module.pcss';\nimport sheetStyles from '../styles/sheet.module.pcss';\nimport SheetBackdrop from '../sheetBackdrop';\nimport { usePopoverPosition } from './usePopoverPosition';\nimport { useFocusBoundary, tabbablesIn } from '../hooks/useFocusBoundary';\nimport { useSheet } from '../hooks/useSheet';\nimport { useOverlay } from '../hooks/useOverlay';\nimport { useComponentDefaults, usePresentationDefault } from '../configProvider';\nimport { Presentation } from '../configProvider/presentation.types';\nimport { PopoverPlacement, PopoverAlign, PopoverTrigger, type PopoverProps } from './popover.types';\n\n/**\n * How long after its click an anchored popover still counts as opened by it: long enough for the\n * render the click causes, short enough that an old keypress does not decide a later open.\n */\nconst ACTIVATION_WINDOW_MS = 1000;\n\nfunction toReference(\n anchor: HTMLElement | null | undefined,\n triggerRef: RefObject<HTMLElement | null>,\n): RefObject<HTMLElement | null> {\n return anchor ? { current: anchor } : triggerRef;\n}\n\nfunction Popover(props: PopoverProps) {\n const {\n children = null,\n anchor,\n content = null,\n placement = PopoverPlacement.Bottom,\n align = PopoverAlign.Center,\n trigger = PopoverTrigger.Click,\n isOpen,\n defaultOpen = false,\n onOpenChange = () => {},\n offset = 10,\n withArrow = true,\n closeOnOutsideClick = true,\n closeOnEscape = true,\n openDelay = 100,\n closeDelay = 120,\n disabled = false,\n focusOnOpen = 'keyboard',\n closeOnTriggerClick = true,\n width = 'auto',\n constrainWidth = true,\n presentation: presentationProp,\n className,\n style,\n classNames,\n styles: slotStyles,\n ...rest\n } = useComponentDefaults('Popover', props);\n const triggerRef = useRef<HTMLDivElement>(null);\n const floatingRef = useRef<HTMLDivElement>(null);\n const panelRef = useRef<HTMLDivElement>(null);\n const arrowRef = useRef<HTMLDivElement>(null);\n const hoverTimer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);\n // Set from a capture handler on the panel, which React fires for its whole\n // React subtree — including children that portal elsewhere in the DOM, such as\n // a Dropdown's list inside a DatePicker's calendar. A DOM `contains` check\n // cannot see those, and without this the panel closes when one is used.\n const panelInteractedRef = useRef(false);\n // An outside click is a press that starts and ends outside while the panel is open. One that\n // starts inside is a drag, such as selecting a field's text, and Chromium and WebKit send its\n // `mouseup` to wherever it ends. One that started before the panel opened is the gesture that\n // opened it, such as Calendar's drag to create, which opens an editor on `pointerup`.\n const pressStartedOutsideRef = useRef(false);\n // Whether the open that is running came from the keyboard, which is what the\n // default `focusOnOpen` follows and what `onOpenChange` reports. A click raised\n // by Enter or Space reports `detail === 0`, where a pointer press counts the\n // clicks; an open with no trigger event at all, such as a panel that appears\n // while the user types, leaves this false.\n const openedByKeyboardRef = useRef(false);\n // A nested portal sits outside the panel's DOM; this is how focus leaving one counts as leaving.\n const lastPanelFocusRef = useRef<EventTarget | null>(null);\n function markPanelInteraction() {\n panelInteractedRef.current = true;\n }\n function markPanelFocus(event: React.FocusEvent) {\n markPanelInteraction();\n lastPanelFocusRef.current = event.target;\n }\n\n // What the panel is placed against, and where the focus goes back to. A new object exactly when\n // `anchor` changes, which is what restarts the position tracking for the new element: state, not\n // a memo, because a memo React chose to drop would restart it for no reason.\n const [reference, setReference] = useState(() => ({ anchor, ref: toReference(anchor, triggerRef) }));\n if (reference.anchor !== anchor) setReference({ anchor, ref: toReference(anchor, triggerRef) });\n const referenceRef = reference.ref;\n\n // An anchored popover is opened by its consumer through `isOpen`, so none of the key handling\n // below sees how. The click that caused it does: Enter and Space raise one with `detail === 0`,\n // the same test the trigger uses. Not `:focus-visible`, which a text field matches after a\n // mouse click too, and which stays on a keyboard-focused button that is then clicked.\n const activationRef = useRef<{ target: Node; keyboard: boolean; time: number } | null>(null);\n const anchored = anchor !== undefined;\n useEffect(() => {\n if (!anchored) return;\n function onClick(event: MouseEvent) {\n activationRef.current = { target: event.target as Node, keyboard: event.detail === 0, time: performance.now() };\n }\n // Capturing, so it is recorded before the consumer's own handler opens the panel.\n document.addEventListener('click', onClick, true);\n return () => document.removeEventListener('click', onClick, true);\n }, [anchored]);\n\n const isControlled = isOpen !== undefined;\n const [internalOpen, setInternalOpen] = useState(defaultOpen);\n const open = isControlled ? isOpen : internalOpen;\n\n // Keep the outside-click/Escape listeners pointed at the latest setter without re-subscribing.\n const onOpenChangeRef = useRef(onOpenChange);\n useEffect(() => {\n onOpenChangeRef.current = onOpenChange;\n });\n\n const setOpen = useCallback(\n (next: boolean, fromKeyboard = false) => {\n if (disabled && next) return;\n if (next) openedByKeyboardRef.current = fromKeyboard;\n if (!isControlled) setInternalOpen(next);\n onOpenChangeRef.current(next, { fromKeyboard });\n },\n [disabled, isControlled],\n );\n\n // Explicit prop → this component's global default → the app-wide one → anchored.\n const appPresentation = usePresentationDefault();\n const presentation = presentationProp ?? appPresentation ?? Presentation.Default;\n const { isSheet: sheetPresentation, sheetStyle } = useSheet(presentation);\n // Hover and focus are pointer affordances that open something small next to what caused them;\n // a sheet is a deliberate, screen-owning open. Only a click asks for one.\n const isSheet = sheetPresentation && trigger === PopoverTrigger.Click;\n\n // A sheet isn't anchored to anything, so there's nothing to track, flip or clamp.\n usePopoverPosition({\n isOpen: !!open && !isSheet,\n placement,\n align,\n offset,\n triggerRef: referenceRef,\n floatingRef,\n panelRef,\n arrowRef: withArrow ? arrowRef : { current: null },\n matchTriggerWidth: width === 'trigger',\n });\n\n // Registered for as long as it's open, not just as a sheet: this is what puts the popover on\n // the overlay stack, so an enclosing Modal knows something sits above it and neither drags\n // focus out of this (portaled, so outside the modal's panel) panel nor closes itself on the\n // Escape meant for this. Focus isn't trapped: a Popover is deliberately non-modal, and it\n // already moves focus into the panel on open by itself.\n const { restoreFocusToTrigger, focusAfterTrigger } = useFocusBoundary({\n triggerRef: referenceRef,\n contentRef: floatingRef,\n });\n const restoreFocusRef = useRef(restoreFocusToTrigger);\n useEffect(() => {\n restoreFocusRef.current = restoreFocusToTrigger;\n });\n\n // Focus inside the panel returns to the trigger however it closes, as with `hidePopover()`. Read\n // in a ref cleanup, while the panel is still in the DOM; moved in an effect, because StrictMode\n // detaches a new panel's refs once more right after it took the focus.\n const focusLeftWithPanelRef = useRef(false);\n const floatingCallbackRef = useCallback((node: HTMLDivElement | null) => {\n floatingRef.current = node;\n focusLeftWithPanelRef.current = false;\n if (!node) return;\n return () => {\n floatingRef.current = null;\n const active = document.activeElement;\n focusLeftWithPanelRef.current = !!active && (node.contains(active) || active === lastPanelFocusRef.current);\n };\n }, []);\n useEffect(() => {\n if (open || !focusLeftWithPanelRef.current) return;\n focusLeftWithPanelRef.current = false;\n const active = document.activeElement;\n if (!active || active === document.body) restoreFocusRef.current();\n }, [open]);\n\n useOverlay({\n isActive: !!open,\n containerRef: panelRef,\n onEscape: closeOnEscape\n ? () => {\n // Hand focus back to the trigger when it currently sits inside the panel.\n if (floatingRef.current?.contains(document.activeElement)) restoreFocusToTrigger();\n setOpen(false, true);\n }\n : undefined,\n trapFocus: false,\n lockScroll: isSheet,\n });\n\n // The panel is portaled to the end of the DOM, so Tab never flows into it naturally. When we\n // manage focus (click trigger + focusable content), move focus in on open and, on Tab out of\n // either end, close and continue focus as if the panel sat right after the trigger.\n useEffect(() => {\n if (!open) {\n openedByKeyboardRef.current = false;\n return;\n }\n const activation = activationRef.current;\n activationRef.current = null;\n const anchorKeyed =\n !!anchor &&\n !!activation?.keyboard &&\n anchor.contains(activation.target) &&\n performance.now() - activation.time < ACTIVATION_WINDOW_MS;\n const takeFocus =\n focusOnOpen === true || (focusOnOpen === 'keyboard' && (openedByKeyboardRef.current || anchorKeyed));\n if (!takeFocus || trigger !== PopoverTrigger.Click) return;\n const focusables = tabbablesIn(panelRef.current);\n if (focusables.length === 0) return;\n // Only claim the dialog role when we're actually managing focus for real content.\n floatingRef.current?.setAttribute('role', 'dialog');\n focusables[0].focus();\n }, [open, trigger, focusOnOpen, anchor]);\n\n function handleFloatingKeyDown(event: React.KeyboardEvent) {\n if (event.key !== 'Tab' || trigger !== PopoverTrigger.Click) return;\n const focusables = tabbablesIn(panelRef.current);\n if (focusables.length === 0) return;\n const active = document.activeElement;\n if (!event.shiftKey && active === focusables[focusables.length - 1]) {\n event.preventDefault();\n focusAfterTrigger();\n setOpen(false, true);\n } else if (event.shiftKey && active === focusables[0]) {\n event.preventDefault();\n restoreFocusToTrigger();\n setOpen(false, true);\n }\n }\n\n // Outside click. Registered only while open and only for the relevant trigger mode; Escape is\n // handled by `useOverlay` above, which scopes it to the innermost open layer.\n useEffect(() => {\n if (!open) return;\n // Focus that landed in the panel before these listeners existed (ours on open, or content\n // that focuses itself on mount) set the flag with nothing to clear it, and the first\n // click outside was swallowed.\n panelInteractedRef.current = false;\n pressStartedOutsideRef.current = false;\n\n function containsNode(node: Node) {\n // Both trigger refs: with an anchor, `children` still toggle the panel, and a press on them is theirs.\n return Boolean(\n floatingRef.current?.contains(node) ||\n triggerRef.current?.contains(node) ||\n referenceRef.current?.contains(node),\n );\n }\n function isInside(event: Event) {\n // Read and clear: the flag describes this one event, and the panel's React\n // handler has already run by the time this does.\n const inPanelSubtree = panelInteractedRef.current;\n panelInteractedRef.current = false;\n return inPanelSubtree || containsNode(event.target as Node);\n }\n function onMouseDown(event: MouseEvent) {\n pressStartedOutsideRef.current = !isInside(event);\n }\n function onMouseUp(event: MouseEvent) {\n const pressStartedOutside = pressStartedOutsideRef.current;\n pressStartedOutsideRef.current = false;\n if (!isInside(event) && pressStartedOutside) setOpen(false);\n }\n // Keyboard counterpart to the outside click. Only focus that comes from inside closes it: code\n // moving focus elsewhere (a Modal restoring it) must not, nor `null`, focus back from another tab.\n function onFocusIn(event: FocusEvent) {\n if (isInside(event)) return;\n const from = event.relatedTarget as Node | null;\n if (!from || (from !== lastPanelFocusRef.current && !containsNode(from))) return;\n // A press on an outside field focuses it before its `mouseup`, and that is not the keyboard.\n setOpen(false, !pressStartedOutsideRef.current);\n }\n\n if (closeOnOutsideClick && trigger !== PopoverTrigger.Hover) {\n window.addEventListener('mousedown', onMouseDown);\n window.addEventListener('mouseup', onMouseUp);\n document.addEventListener('focusin', onFocusIn);\n }\n return () => {\n window.removeEventListener('mousedown', onMouseDown);\n window.removeEventListener('mouseup', onMouseUp);\n document.removeEventListener('focusin', onFocusIn);\n };\n }, [open, closeOnOutsideClick, trigger, setOpen, referenceRef]);\n\n useEffect(() => () => clearTimeout(hoverTimer.current), []);\n\n function scheduleHover(next: boolean) {\n clearTimeout(hoverTimer.current);\n hoverTimer.current = setTimeout(() => setOpen(next), next ? openDelay : closeDelay);\n }\n\n // Opening from the keyboard without the trigger having to be a button that\n // happens to toggle on Enter: the arrow keys are what the menu-button pattern\n // asks for, and what a consumer reaches for first.\n function handleTriggerKeyDown(event: React.KeyboardEvent) {\n if (open || (event.key !== 'ArrowDown' && event.key !== 'ArrowUp')) return;\n event.preventDefault();\n setOpen(true, true);\n }\n\n const triggerHandlers =\n trigger === PopoverTrigger.Click\n ? {\n onClick: (event: React.MouseEvent) => {\n // A field that opens a picker keeps it open when clicked again, so\n // that placing the caret in the field does not close the panel.\n if (open && !closeOnTriggerClick) return;\n setOpen(!open, event.detail === 0);\n },\n onKeyDown: handleTriggerKeyDown,\n }\n : trigger === PopoverTrigger.Hover\n ? { onMouseEnter: () => scheduleHover(true), onMouseLeave: () => scheduleHover(false) }\n : { onFocus: () => setOpen(true), onBlur: () => setOpen(false) };\n\n const floatingHoverHandlers =\n trigger === PopoverTrigger.Hover\n ? { onMouseEnter: () => scheduleHover(true), onMouseLeave: () => scheduleHover(false) }\n : {};\n\n return (\n <div\n {...rest}\n className={cx(styles.popover, className, classNames?.root)}\n style={{ ...style, ...slotStyles?.root }}\n >\n <div\n ref={triggerRef}\n className={cx(styles.popoverTrigger, classNames?.trigger)}\n style={slotStyles?.trigger}\n {...triggerHandlers}\n >\n {children}\n </div>\n {open && (\n <BodyEnd>\n {isSheet && (\n <SheetBackdrop\n onClose={() => {\n if (closeOnOutsideClick) setOpen(false);\n }}\n className={classNames?.backdrop}\n style={slotStyles?.backdrop}\n />\n )}\n <div\n ref={floatingCallbackRef}\n className={cx(isSheet ? sheetStyles.sheet : styles.popoverFloating, classNames?.floating)}\n style={{ ...(isSheet ? sheetStyle : undefined), ...slotStyles?.floating }}\n data-placement={placement}\n onKeyDown={handleFloatingKeyDown}\n onMouseDownCapture={markPanelInteraction}\n onMouseUpCapture={markPanelInteraction}\n onFocusCapture={markPanelFocus}\n {...floatingHoverHandlers}\n >\n <div\n ref={panelRef}\n className={cx(\n styles.popoverPanel,\n constrainWidth && styles.popoverPanelConstrained,\n isSheet && sheetStyles.sheetPanel,\n classNames?.panel,\n )}\n style={slotStyles?.panel}\n >\n {content}\n </div>\n {withArrow && !isSheet && (\n <div ref={arrowRef} className={cx(styles.popoverArrow, classNames?.arrow)} style={slotStyles?.arrow} />\n )}\n </div>\n </BodyEnd>\n )}\n </div>\n );\n}\n\nexport default Popover;\n"],"mappings":"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"}
1
+ {"version":3,"file":"popover.mjs","names":[],"sources":["../../../../package/common/popover/popover.tsx"],"sourcesContent":["import { useRef, useEffect, useState, useCallback, type RefObject } from 'react';\nimport cx from 'classnames';\nimport BodyEnd from 'components/common/bodyEnd';\n\nimport styles from './popover.module.pcss';\nimport sheetStyles from '../styles/sheet.module.pcss';\nimport SheetBackdrop from '../sheetBackdrop';\nimport { usePopoverPosition } from './usePopoverPosition';\nimport { useFocusBoundary, tabbablesIn } from '../hooks/useFocusBoundary';\nimport { useSheet } from '../hooks/useSheet';\nimport { useOverlay } from '../hooks/useOverlay';\nimport { useComponentDefaults, usePresentationDefault } from '../configProvider';\nimport { Presentation } from '../configProvider/presentation.types';\nimport { PopoverPlacement, PopoverAlign, PopoverTrigger, type PopoverProps } from './popover.types';\n\n/**\n * How long after its click an anchored popover still counts as opened by it: long enough for the\n * render the click causes, short enough that an old keypress does not decide a later open.\n */\nconst ACTIVATION_WINDOW_MS = 1000;\n\nfunction toReference(\n anchor: HTMLElement | null | undefined,\n triggerRef: RefObject<HTMLElement | null>,\n): RefObject<HTMLElement | null> {\n return anchor ? { current: anchor } : triggerRef;\n}\n\nfunction Popover(props: PopoverProps) {\n const {\n children = null,\n anchor,\n content = null,\n placement = PopoverPlacement.Bottom,\n align = PopoverAlign.Center,\n trigger = PopoverTrigger.Click,\n isOpen,\n defaultOpen = false,\n onOpenChange = () => {},\n offset = 10,\n withArrow = true,\n closeOnOutsideClick = true,\n closeOnEscape = true,\n openDelay = 100,\n closeDelay = 120,\n disabled = false,\n focusOnOpen = 'keyboard',\n closeOnTriggerClick = true,\n width = 'auto',\n constrainWidth = true,\n presentation: presentationProp,\n className,\n style,\n classNames,\n styles: slotStyles,\n ...rest\n } = useComponentDefaults('Popover', props);\n const triggerRef = useRef<HTMLDivElement>(null);\n const floatingRef = useRef<HTMLDivElement>(null);\n const panelRef = useRef<HTMLDivElement>(null);\n const arrowRef = useRef<HTMLDivElement>(null);\n const hoverTimer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);\n // Set from a capture handler on the panel, which React fires for its whole\n // React subtree — including children that portal elsewhere in the DOM, such as\n // a Dropdown's list inside a DatePicker's calendar. A DOM `contains` check\n // cannot see those, and without this the panel closes when one is used.\n const panelInteractedRef = useRef(false);\n // An outside click is a press that starts and ends outside while the panel is open. One that\n // starts inside is a drag, such as selecting a field's text, and Chromium and WebKit send its\n // `mouseup` to wherever it ends. One that started before the panel opened is the gesture that\n // opened it, such as Calendar's drag to create, which opens an editor on `pointerup`.\n const pressStartedOutsideRef = useRef(false);\n // Whether the open that is running came from the keyboard, which is what the\n // default `focusOnOpen` follows and what `onOpenChange` reports. A click raised\n // by Enter or Space reports `detail === 0`, where a pointer press counts the\n // clicks; an open with no trigger event at all, such as a panel that appears\n // while the user types, leaves this false.\n const openedByKeyboardRef = useRef(false);\n // A nested portal sits outside the panel's DOM; this is how focus leaving one counts as leaving.\n const lastPanelFocusRef = useRef<EventTarget | null>(null);\n function markPanelInteraction() {\n panelInteractedRef.current = true;\n }\n function markPanelFocus(event: React.FocusEvent) {\n markPanelInteraction();\n lastPanelFocusRef.current = event.target;\n }\n\n // What the panel is placed against, and where the focus goes back to. A new object exactly when\n // `anchor` changes, which is what restarts the position tracking for the new element: state, not\n // a memo, because a memo React chose to drop would restart it for no reason.\n const [reference, setReference] = useState(() => ({ anchor, ref: toReference(anchor, triggerRef) }));\n if (reference.anchor !== anchor) setReference({ anchor, ref: toReference(anchor, triggerRef) });\n const referenceRef = reference.ref;\n\n // An anchored popover is opened by its consumer through `isOpen`, so none of the key handling\n // below sees how. The click that caused it does: Enter and Space raise one with `detail === 0`,\n // the same test the trigger uses. Not `:focus-visible`, which a text field matches after a\n // mouse click too, and which stays on a keyboard-focused button that is then clicked.\n const activationRef = useRef<{ target: Node; keyboard: boolean; time: number } | null>(null);\n const anchored = anchor !== undefined;\n useEffect(() => {\n if (!anchored) return;\n function onClick(event: MouseEvent) {\n activationRef.current = { target: event.target as Node, keyboard: event.detail === 0, time: performance.now() };\n }\n // Capturing, so it is recorded before the consumer's own handler opens the panel.\n document.addEventListener('click', onClick, true);\n return () => document.removeEventListener('click', onClick, true);\n }, [anchored]);\n\n const isControlled = isOpen !== undefined;\n const [internalOpen, setInternalOpen] = useState(defaultOpen);\n const open = isControlled ? isOpen : internalOpen;\n\n // Keep the outside-click/Escape listeners pointed at the latest setter without re-subscribing.\n const onOpenChangeRef = useRef(onOpenChange);\n useEffect(() => {\n onOpenChangeRef.current = onOpenChange;\n });\n\n const setOpen = useCallback(\n (next: boolean, fromKeyboard = false) => {\n if (disabled && next) return;\n if (next) openedByKeyboardRef.current = fromKeyboard;\n if (!isControlled) setInternalOpen(next);\n onOpenChangeRef.current(next, { fromKeyboard });\n },\n [disabled, isControlled],\n );\n\n // Explicit prop → this component's global default → the app-wide one → anchored.\n const appPresentation = usePresentationDefault();\n const presentation = presentationProp ?? appPresentation ?? Presentation.Default;\n const { isSheet: sheetPresentation, sheetStyle } = useSheet(presentation);\n // Hover and focus are pointer affordances that open something small next to what caused them;\n // a sheet is a deliberate, screen-owning open. Only a click asks for one.\n const isSheet = sheetPresentation && trigger === PopoverTrigger.Click;\n\n // Focus into the panel waits until it shows, since one that waits out a moving ancestor hidden\n // takes none.\n const shownRef = useRef(false);\n const focusPendingRef = useRef(false);\n function focusPanel() {\n focusPendingRef.current = false;\n const focusables = tabbablesIn(panelRef.current);\n if (focusables.length === 0) return;\n // Only claim the dialog role when we're actually managing focus for real content.\n floatingRef.current?.setAttribute('role', 'dialog');\n focusables[0].focus();\n }\n\n // A sheet isn't anchored to anything, so there's nothing to track, flip or clamp.\n usePopoverPosition({\n isOpen: !!open && !isSheet,\n placement,\n align,\n offset,\n triggerRef: referenceRef,\n floatingRef,\n panelRef,\n arrowRef: withArrow ? arrowRef : { current: null },\n matchTriggerWidth: width === 'trigger',\n onShown: () => {\n shownRef.current = true;\n if (focusPendingRef.current) focusPanel();\n },\n });\n\n // Registered for as long as it's open, not just as a sheet: this is what puts the popover on\n // the overlay stack, so an enclosing Modal knows something sits above it and neither drags\n // focus out of this (portaled, so outside the modal's panel) panel nor closes itself on the\n // Escape meant for this. Focus isn't trapped: a Popover is deliberately non-modal, and it\n // already moves focus into the panel on open by itself.\n const { restoreFocusToTrigger, focusAfterTrigger } = useFocusBoundary({\n triggerRef: referenceRef,\n contentRef: floatingRef,\n });\n const restoreFocusRef = useRef(restoreFocusToTrigger);\n useEffect(() => {\n restoreFocusRef.current = restoreFocusToTrigger;\n });\n\n // Focus inside the panel returns to the trigger however it closes, as with `hidePopover()`. Read\n // in a ref cleanup, while the panel is still in the DOM; moved in an effect, because StrictMode\n // detaches a new panel's refs once more right after it took the focus.\n const focusLeftWithPanelRef = useRef(false);\n const floatingCallbackRef = useCallback((node: HTMLDivElement | null) => {\n floatingRef.current = node;\n focusLeftWithPanelRef.current = false;\n if (!node) return;\n return () => {\n floatingRef.current = null;\n const active = document.activeElement;\n focusLeftWithPanelRef.current = !!active && (node.contains(active) || active === lastPanelFocusRef.current);\n };\n }, []);\n useEffect(() => {\n if (open || !focusLeftWithPanelRef.current) return;\n focusLeftWithPanelRef.current = false;\n const active = document.activeElement;\n if (!active || active === document.body) restoreFocusRef.current();\n }, [open]);\n\n useOverlay({\n isActive: !!open,\n containerRef: panelRef,\n onEscape: closeOnEscape\n ? () => {\n // Hand focus back to the trigger when it currently sits inside the panel.\n if (floatingRef.current?.contains(document.activeElement)) restoreFocusToTrigger();\n setOpen(false, true);\n }\n : undefined,\n trapFocus: false,\n lockScroll: isSheet,\n });\n\n // The panel is portaled to the end of the DOM, so Tab never flows into it naturally. When we\n // manage focus (click trigger + focusable content), move focus in on open and, on Tab out of\n // either end, close and continue focus as if the panel sat right after the trigger.\n useEffect(() => {\n if (!open) {\n openedByKeyboardRef.current = false;\n shownRef.current = false;\n focusPendingRef.current = false;\n return;\n }\n const activation = activationRef.current;\n activationRef.current = null;\n const anchorKeyed =\n !!anchor &&\n !!activation?.keyboard &&\n anchor.contains(activation.target) &&\n performance.now() - activation.time < ACTIVATION_WINDOW_MS;\n const takeFocus =\n focusOnOpen === true || (focusOnOpen === 'keyboard' && (openedByKeyboardRef.current || anchorKeyed));\n if (!takeFocus || trigger !== PopoverTrigger.Click) return;\n if (shownRef.current) focusPanel();\n else focusPendingRef.current = true;\n }, [open, trigger, focusOnOpen, anchor]);\n\n function handleFloatingKeyDown(event: React.KeyboardEvent) {\n if (event.key !== 'Tab' || trigger !== PopoverTrigger.Click) return;\n const focusables = tabbablesIn(panelRef.current);\n if (focusables.length === 0) return;\n const active = document.activeElement;\n if (!event.shiftKey && active === focusables[focusables.length - 1]) {\n event.preventDefault();\n focusAfterTrigger();\n setOpen(false, true);\n } else if (event.shiftKey && active === focusables[0]) {\n event.preventDefault();\n restoreFocusToTrigger();\n setOpen(false, true);\n }\n }\n\n // Outside click. Registered only while open and only for the relevant trigger mode; Escape is\n // handled by `useOverlay` above, which scopes it to the innermost open layer.\n useEffect(() => {\n if (!open) return;\n // Focus that landed in the panel before these listeners existed (ours on open, or content\n // that focuses itself on mount) set the flag with nothing to clear it, and the first\n // click outside was swallowed.\n panelInteractedRef.current = false;\n pressStartedOutsideRef.current = false;\n\n function containsNode(node: Node) {\n // Both trigger refs: with an anchor, `children` still toggle the panel, and a press on them is theirs.\n return Boolean(\n floatingRef.current?.contains(node) ||\n triggerRef.current?.contains(node) ||\n referenceRef.current?.contains(node),\n );\n }\n function isInside(event: Event) {\n // Read and clear: the flag describes this one event, and the panel's React\n // handler has already run by the time this does.\n const inPanelSubtree = panelInteractedRef.current;\n panelInteractedRef.current = false;\n return inPanelSubtree || containsNode(event.target as Node);\n }\n function onMouseDown(event: MouseEvent) {\n pressStartedOutsideRef.current = !isInside(event);\n }\n function onMouseUp(event: MouseEvent) {\n const pressStartedOutside = pressStartedOutsideRef.current;\n pressStartedOutsideRef.current = false;\n if (!isInside(event) && pressStartedOutside) setOpen(false);\n }\n // Keyboard counterpart to the outside click. Only focus that comes from inside closes it: code\n // moving focus elsewhere (a Modal restoring it) must not, nor `null`, focus back from another tab.\n function onFocusIn(event: FocusEvent) {\n if (isInside(event)) return;\n const from = event.relatedTarget as Node | null;\n if (!from || (from !== lastPanelFocusRef.current && !containsNode(from))) return;\n // A press on an outside field focuses it before its `mouseup`, and that is not the keyboard.\n setOpen(false, !pressStartedOutsideRef.current);\n }\n\n if (closeOnOutsideClick && trigger !== PopoverTrigger.Hover) {\n window.addEventListener('mousedown', onMouseDown);\n window.addEventListener('mouseup', onMouseUp);\n document.addEventListener('focusin', onFocusIn);\n }\n return () => {\n window.removeEventListener('mousedown', onMouseDown);\n window.removeEventListener('mouseup', onMouseUp);\n document.removeEventListener('focusin', onFocusIn);\n };\n }, [open, closeOnOutsideClick, trigger, setOpen, referenceRef]);\n\n useEffect(() => () => clearTimeout(hoverTimer.current), []);\n\n function scheduleHover(next: boolean) {\n clearTimeout(hoverTimer.current);\n hoverTimer.current = setTimeout(() => setOpen(next), next ? openDelay : closeDelay);\n }\n\n // Opening from the keyboard without the trigger having to be a button that\n // happens to toggle on Enter: the arrow keys are what the menu-button pattern\n // asks for, and what a consumer reaches for first.\n function handleTriggerKeyDown(event: React.KeyboardEvent) {\n if (open || (event.key !== 'ArrowDown' && event.key !== 'ArrowUp')) return;\n event.preventDefault();\n setOpen(true, true);\n }\n\n const triggerHandlers =\n trigger === PopoverTrigger.Click\n ? {\n onClick: (event: React.MouseEvent) => {\n // A field that opens a picker keeps it open when clicked again, so\n // that placing the caret in the field does not close the panel.\n if (open && !closeOnTriggerClick) return;\n setOpen(!open, event.detail === 0);\n },\n onKeyDown: handleTriggerKeyDown,\n }\n : trigger === PopoverTrigger.Hover\n ? { onMouseEnter: () => scheduleHover(true), onMouseLeave: () => scheduleHover(false) }\n : { onFocus: () => setOpen(true), onBlur: () => setOpen(false) };\n\n const floatingHoverHandlers =\n trigger === PopoverTrigger.Hover\n ? { onMouseEnter: () => scheduleHover(true), onMouseLeave: () => scheduleHover(false) }\n : {};\n\n return (\n <div\n {...rest}\n className={cx(styles.popover, className, classNames?.root)}\n style={{ ...style, ...slotStyles?.root }}\n >\n <div\n ref={triggerRef}\n className={cx(styles.popoverTrigger, classNames?.trigger)}\n style={slotStyles?.trigger}\n {...triggerHandlers}\n >\n {children}\n </div>\n {open && (\n <BodyEnd>\n {isSheet && (\n <SheetBackdrop\n onClose={() => {\n if (closeOnOutsideClick) setOpen(false);\n }}\n className={classNames?.backdrop}\n style={slotStyles?.backdrop}\n />\n )}\n <div\n ref={floatingCallbackRef}\n className={cx(isSheet ? sheetStyles.sheet : styles.popoverFloating, classNames?.floating)}\n style={{ ...(isSheet ? sheetStyle : undefined), ...slotStyles?.floating }}\n data-placement={placement}\n onKeyDown={handleFloatingKeyDown}\n onMouseDownCapture={markPanelInteraction}\n onMouseUpCapture={markPanelInteraction}\n onFocusCapture={markPanelFocus}\n {...floatingHoverHandlers}\n >\n <div\n ref={panelRef}\n className={cx(\n styles.popoverPanel,\n constrainWidth && styles.popoverPanelConstrained,\n isSheet && sheetStyles.sheetPanel,\n classNames?.panel,\n )}\n style={slotStyles?.panel}\n >\n {content}\n </div>\n {withArrow && !isSheet && (\n <div ref={arrowRef} className={cx(styles.popoverArrow, classNames?.arrow)} style={slotStyles?.arrow} />\n )}\n </div>\n </BodyEnd>\n )}\n </div>\n );\n}\n\nexport default Popover;\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"popover.module.mjs","names":[],"sources":["../../../../package/common/popover/popover.module.pcss"],"sourcesContent":["/* Neither of these two generates a box, so the trigger lands in the consumer's\n layout exactly as if Popover were not around it: it is the flex item, the grid\n item, the block-level child. A field that fills its column still fills it, a\n short button is still only as wide as itself, and a row that stretches its\n children reaches the trigger instead of stopping at a wrapper around it. They\n stay in the event path, so the handlers on them still fire, and\n `usePopoverPosition` already looks past boxless wrappers for its anchor.\n\n Every box we tried here was wrong in one direction or the other. A wrapper that\n spans its container opens the panel from the empty space beside a short button.\n `width: fit-content` collapses a field whose own width is a percentage of its\n parent. A flex root stretches the trigger's height but sizes its width by\n content, which collapsed KlipyPicker's search field.\n\n The cost is that `className`, `style` and `classNames.root` cannot paint\n anything on these two, because there is nothing to paint on. */\n.popover {\n display: contents;\n\n &-trigger {\n display: contents;\n }\n\n &-floating {\n position: fixed;\n top: 0;\n left: 0;\n z-index: 1;\n\n /* Hidden until usePopoverPosition writes the first coordinates, to avoid a top-left flash. */\n visibility: hidden;\n }\n\n &-panel {\n position: relative;\n\n /* Above the arrow so the panel background hides the arrow's inner half — only the\n tip pokes out past the panel edge. */\n z-index: 1;\n color: var(--text-color);\n font-family: var(--default-font);\n font-size: var(--default-font-size);\n background: var(--background-modal);\n box-shadow: var(--card-shadow);\n border-radius: calc(var(--default-border-radius) + 4px);\n padding: 12px 14px;\n\n /* A readable measure for a popover of text. A panel that brings its own\n width — a calendar, a media grid — turns it off with `constrainWidth`.\n `.sheet-panel` overrides this by name for the sheet path; check there\n before touching the cap. */\n &-constrained {\n max-width: min(320px, calc(100vw - 24px));\n }\n\n animation: popoverIn 0.12s ease-out forwards;\n }\n\n &-arrow {\n position: absolute;\n width: 10px;\n height: 10px;\n background: var(--background-modal);\n box-shadow: var(--card-shadow);\n transform: rotate(45deg);\n }\n\n /* Arrow sits on the panel edge facing the trigger; --arrow-x / --arrow-y centre it on the trigger. */\n &-floating[data-placement='bottom'] &-arrow {\n top: -5px;\n left: var(--arrow-x, 50%);\n margin-left: -5px;\n }\n\n &-floating[data-placement='top'] &-arrow {\n bottom: -5px;\n left: var(--arrow-x, 50%);\n margin-left: -5px;\n }\n\n &-floating[data-placement='right'] &-arrow {\n left: -5px;\n top: var(--arrow-y, 50%);\n margin-top: -5px;\n }\n\n &-floating[data-placement='left'] &-arrow {\n right: -5px;\n top: var(--arrow-y, 50%);\n margin-top: -5px;\n }\n\n /* Grow from the edge nearest the trigger. */\n &-floating[data-placement='bottom'] &-panel {\n transform-origin: top center;\n }\n\n &-floating[data-placement='top'] &-panel {\n transform-origin: bottom center;\n }\n\n &-floating[data-placement='right'] &-panel {\n transform-origin: center left;\n }\n\n &-floating[data-placement='left'] &-panel {\n transform-origin: center right;\n }\n\n @keyframes popoverIn {\n from {\n opacity: 0.4;\n transform: scale(0.7);\n }\n\n to {\n opacity: 1;\n transform: scale(1);\n }\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .popover-panel {\n animation: none;\n }\n}\n"],"mappings":""}
1
+ {"version":3,"file":"popover.module.mjs","names":[],"sources":["../../../../package/common/popover/popover.module.pcss"],"sourcesContent":["/* Neither of these two generates a box, so the trigger lands in the consumer's\n layout exactly as if Popover were not around it: it is the flex item, the grid\n item, the block-level child. A field that fills its column still fills it, a\n short button is still only as wide as itself, and a row that stretches its\n children reaches the trigger instead of stopping at a wrapper around it. They\n stay in the event path, so the handlers on them still fire, and\n `usePopoverPosition` already looks past boxless wrappers for its anchor.\n\n Every box we tried here was wrong in one direction or the other. A wrapper that\n spans its container opens the panel from the empty space beside a short button.\n `width: fit-content` collapses a field whose own width is a percentage of its\n parent. A flex root stretches the trigger's height but sizes its width by\n content, which collapsed KlipyPicker's search field.\n\n The cost is that `className`, `style` and `classNames.root` cannot paint\n anything on these two, because there is nothing to paint on. */\n.popover {\n display: contents;\n\n &-trigger {\n display: contents;\n }\n\n &-floating {\n position: fixed;\n top: 0;\n left: 0;\n\n /* Shared by every overlay, so the order they are portaled in, which is the order they opened in, decides. */\n z-index: 1;\n\n /* Hidden until usePopoverPosition writes the first coordinates, to avoid a top-left flash. */\n visibility: hidden;\n }\n\n &-panel {\n position: relative;\n\n /* Above the arrow so the panel background hides the arrow's inner half — only the\n tip pokes out past the panel edge. */\n z-index: 1;\n color: var(--text-color);\n font-family: var(--default-font);\n font-size: var(--default-font-size);\n background: var(--background-modal);\n box-shadow: var(--card-shadow);\n border-radius: calc(var(--default-border-radius) + 4px);\n padding: 12px 14px;\n\n /* A readable measure for a popover of text. A panel that brings its own\n width — a calendar, a media grid — turns it off with `constrainWidth`.\n `.sheet-panel` overrides this by name for the sheet path; check there\n before touching the cap. */\n &-constrained {\n max-width: min(320px, calc(100vw - 24px));\n }\n\n animation: popoverIn 0.12s ease-out forwards;\n }\n\n &-arrow {\n position: absolute;\n width: 10px;\n height: 10px;\n background: var(--background-modal);\n box-shadow: var(--card-shadow);\n transform: rotate(45deg);\n }\n\n /* Arrow sits on the panel edge facing the trigger; --arrow-x / --arrow-y centre it on the trigger. */\n &-floating[data-placement='bottom'] &-arrow {\n top: -5px;\n left: var(--arrow-x, 50%);\n margin-left: -5px;\n }\n\n &-floating[data-placement='top'] &-arrow {\n bottom: -5px;\n left: var(--arrow-x, 50%);\n margin-left: -5px;\n }\n\n &-floating[data-placement='right'] &-arrow {\n left: -5px;\n top: var(--arrow-y, 50%);\n margin-top: -5px;\n }\n\n &-floating[data-placement='left'] &-arrow {\n right: -5px;\n top: var(--arrow-y, 50%);\n margin-top: -5px;\n }\n\n /* Grow from the edge nearest the trigger. */\n &-floating[data-placement='bottom'] &-panel {\n transform-origin: top center;\n }\n\n &-floating[data-placement='top'] &-panel {\n transform-origin: bottom center;\n }\n\n &-floating[data-placement='right'] &-panel {\n transform-origin: center left;\n }\n\n &-floating[data-placement='left'] &-panel {\n transform-origin: center right;\n }\n\n @keyframes popoverIn {\n from {\n opacity: 0.4;\n transform: scale(0.7);\n }\n\n to {\n opacity: 1;\n transform: scale(1);\n }\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .popover-panel {\n animation: none;\n }\n}\n"],"mappings":""}
@@ -1,2 +1,2 @@
1
- "use client";import{isHiddenByAnyAncestor as e}from"../utils/scrollAncestors.mjs";import{getVisualViewportRect as t}from"../utils/visualViewport.mjs";import{useAnchorTracking as n}from"../hooks/useAnchorTracking.mjs";import{PopoverAlign as r,PopoverPlacement as i}from"./popover.types.mjs";import{useId as a,useRef as o}from"react";var s={[i.Top]:`bottom`,[i.Bottom]:`top`,[i.Left]:`right`,[i.Right]:`left`},c=8,l=160,u=3;function d(e){let t=e;for(let e=0;e<=u;e++){let e=t.getBoundingClientRect();if(e.width>0||e.height>0)return{element:t,rect:e};let n=t.firstElementChild;if(!n)break;t=n}return{element:null,rect:e.getBoundingClientRect()}}var f;function p(){if(f!==void 0)return f;if(typeof CSS>`u`||!CSS.supports?.(`top: anchor(top, 0px)`))return f=!1;let e=document.createElement(`div`);e.style.cssText=`position: fixed; top: 0; left: 0; transform: translateY(100px); visibility: hidden`;let t=document.createElement(`div`);t.style.cssText=`anchor-name: --popover-probe; width: 1px; height: 1px`;let n=document.createElement(`div`);return n.style.cssText=`position: fixed; position-anchor: --popover-probe; top: anchor(top, 0px); left: 0; visibility: hidden`,e.append(t),document.body.append(e,n),f=Math.abs(n.getBoundingClientRect().top-t.getBoundingClientRect().top)<1,e.remove(),n.remove(),f}function m(e){return e.style.getPropertyValue(`anchor-name`).split(`,`).map(e=>e.trim()).filter(e=>e&&e!==`none`)}function h(e,t){let n=m(e);n.includes(t)||e.style.setProperty(`anchor-name`,[...n,t].join(`, `))}function g(e,t){let n=m(e).filter(e=>e!==t);n.length>0?e.style.setProperty(`anchor-name`,n.join(`, `)):e.style.removeProperty(`anchor-name`)}function _(e,t,n,r){return r?`calc(anchor(${e}, ${n}px) + ${t-n}px)`:`${t}px`}function v({isOpen:u,placement:f,align:m,offset:v,triggerRef:y,floatingRef:b,panelRef:x,arrowRef:S,matchTriggerWidth:C=!1,shrinkToFit:w=!1}){let T=`--popover-${a().replace(/[^\w-]/g,``)}`,E=o(null);function D(e){E.current!==e&&(E.current&&g(E.current,T),e&&h(e,T),E.current=e)}n({isOpen:u,triggerRef:y,elementRefs:[b,x],resizeRefs:[x],deps:[f,m,v,b,x,S,C,w],update:(n,a)=>{let o=b.current,u=x.current;if(!o||!u)return;let{element:h,rect:g}=d(n),y=h!==null&&p();D(y?h:null),y?o.style.setProperty(`position-anchor`,T):o.style.removeProperty(`position-anchor`),C?u.style.minWidth=`${g.width}px`:u.style.removeProperty(`min-width`),w&&u.style.removeProperty(`max-width`);let{width:E,height:O}=o.getBoundingClientRect(),k=t(),A=f===i.Top||f===i.Bottom;if(w&&A){let e=m===r.End?g.right-(k.left+c):m===r.Start?k.left+k.width-c-g.left:k.width-16,t=Math.min(e,k.width-16);E>t&&t>=l&&(u.style.maxWidth=`${t}px`,{width:E,height:O}=o.getBoundingClientRect())}let j={top:g.top-k.top,bottom:k.top+k.height-g.bottom,left:g.left-k.left,right:k.left+k.width-g.right},M=f===i.Top||f===i.Bottom?O:E,N=f,P=s[f];j[N]<M+v&&j[P]>j[N]&&(N=P);let F=N===`top`||N===`bottom`,I,L;N===`bottom`?I=g.bottom+v:N===`top`?I=g.top-v-O:L=N===`right`?g.right+v:g.left-v-E;let R=!1,z=!1;if(F){L=m===r.Start?g.left:m===r.End?g.right-E:g.left+g.width/2-E/2;let e=Math.max(k.left+c,Math.min(L,k.left+k.width-E-c));R=e!==L,L=e}else{I=m===r.Start?g.top:m===r.End?g.bottom-O:g.top+g.height/2-O/2;let e=Math.max(k.top+c,Math.min(I,k.top+k.height-O-c));z=e!==I,I=e}o.style.top=_(`top`,I,g.top,y&&!z),o.style.left=_(`left`,L,g.left,y&&!R),o.style.visibility=e(g,a)?`hidden`:`visible`,o.dataset.placement=N;let B=S.current;if(B){let e=B.offsetWidth/2||5;if(F){let t=g.left+g.width/2-L;B.style.setProperty(`--arrow-x`,`${Math.max(e+2,Math.min(t,E-e-2))}px`),B.style.removeProperty(`--arrow-y`)}else{let t=g.top+g.height/2-I;B.style.setProperty(`--arrow-y`,`${Math.max(e+2,Math.min(t,O-e-2))}px`),B.style.removeProperty(`--arrow-x`)}}},teardown:()=>{D(null);let e=b.current;if(!e)return;e.style.top=``,e.style.left=``,e.style.visibility=``,e.style.removeProperty(`position-anchor`);let t=x.current;t?.style.removeProperty(`min-width`),t?.style.removeProperty(`max-width`)}})}export{p as supportsAnchoring,v as usePopoverPosition};
1
+ "use client";import{isHiddenByAnyAncestor as e}from"../utils/scrollAncestors.mjs";import{getVisualViewportRect as t}from"../utils/visualViewport.mjs";import{useAnchorTracking as n}from"../hooks/useAnchorTracking.mjs";import{PopoverAlign as r,PopoverPlacement as i}from"./popover.types.mjs";import{useId as a,useRef as o}from"react";var s={[i.Top]:`bottom`,[i.Bottom]:`top`,[i.Left]:`right`,[i.Right]:`left`},c=8,l=160,u=3;function d(e){let t=e;for(let e=0;e<=u;e++){let e=t.getBoundingClientRect();if(e.width>0||e.height>0)return{element:t,rect:e};let n=t.firstElementChild;if(!n)break;t=n}return{element:null,rect:e.getBoundingClientRect()}}var f;function p(){if(f!==void 0)return f;if(typeof CSS>`u`||!CSS.supports?.(`top: anchor(top, 0px)`))return f=!1;let e=document.createElement(`div`);e.style.cssText=`position: fixed; top: 0; left: 0; transform: translateY(100px); visibility: hidden`;let t=document.createElement(`div`);t.style.cssText=`anchor-name: --popover-probe; width: 1px; height: 1px`;let n=document.createElement(`div`);return n.style.cssText=`position: fixed; position-anchor: --popover-probe; top: anchor(top, 0px); left: 0; visibility: hidden`,e.append(t),document.body.append(e,n),f=Math.abs(n.getBoundingClientRect().top-t.getBoundingClientRect().top)<1,e.remove(),n.remove(),f}function m(e){return e.style.getPropertyValue(`anchor-name`).split(`,`).map(e=>e.trim()).filter(e=>e&&e!==`none`)}function h(e,t){let n=m(e);n.includes(t)||e.style.setProperty(`anchor-name`,[...n,t].join(`, `))}function g(e,t){let n=m(e).filter(e=>e!==t);n.length>0?e.style.setProperty(`anchor-name`,n.join(`, `)):e.style.removeProperty(`anchor-name`)}var _=[`transform`,`translate`,`scale`,`rotate`],v=1e3;function y(e){return(document.getAnimations?.()??[]).filter(t=>{if(t.playState!==`running`||!(t.effect instanceof KeyframeEffect))return!1;let{target:n,pseudoElement:r}=t.effect;if(!n||r||n===e||!n.contains(e))return!1;let{endTime:i,localTime:a}=t.effect.getComputedTiming();return Number(i)-Number(a)>v?!1:t.effect.getKeyframes().some(e=>_.some(t=>t in e))})}function b(e,t,n,r){return r?`calc(anchor(${e}, ${n}px) + ${t-n}px)`:`${t}px`}function x({isOpen:u,placement:f,align:m,offset:_,triggerRef:v,floatingRef:x,panelRef:S,arrowRef:C,matchTriggerWidth:w=!1,shrinkToFit:T=!1,onShown:E}){let D=`--popover-${a().replace(/[^\w-]/g,``)}`,O=o(null),k=o(null),A=o(new WeakMap);function j(e){O.current!==e&&(O.current&&g(O.current,D),e&&h(e,D),O.current=e)}n({isOpen:u,triggerRef:v,elementRefs:[x,S],resizeRefs:[S],deps:[f,m,_,x,S,C,w,T],update:(n,a,o)=>{let u=x.current,h=S.current;if(!u||!h)return;let{element:g,rect:v}=d(n),O=g!==null&&p();j(O?g:null),O?u.style.setProperty(`position-anchor`,D):u.style.removeProperty(`position-anchor`),w?h.style.minWidth=`${v.width}px`:h.style.removeProperty(`min-width`),T&&h.style.removeProperty(`max-width`);let{width:M,height:N}=u.getBoundingClientRect(),P=t(),F=f===i.Top||f===i.Bottom;if(T&&F){let e=m===r.End?v.right-(P.left+c):m===r.Start?P.left+P.width-c-v.left:P.width-16,t=Math.min(e,P.width-16);M>t&&t>=l&&(h.style.maxWidth=`${t}px`,{width:M,height:N}=u.getBoundingClientRect())}let I={top:v.top-P.top,bottom:P.top+P.height-v.bottom,left:v.left-P.left,right:P.left+P.width-v.right},L=f===i.Top||f===i.Bottom?N:M,R=f,z=s[f];I[R]<L+_&&I[z]>I[R]&&(R=z);let B=R===`top`||R===`bottom`,V,H;R===`bottom`?V=v.bottom+_:R===`top`?V=v.top-_-N:H=R===`right`?v.right+_:v.left-_-M;let U=!1,W=!1;if(B){H=m===r.Start?v.left:m===r.End?v.right-M:v.left+v.width/2-M/2;let e=Math.max(P.left+c,Math.min(H,P.left+P.width-M-c));U=e!==H,H=e}else{V=m===r.Start?v.top:m===r.End?v.bottom-N:v.top+v.height/2-N/2;let e=Math.max(P.top+c,Math.min(V,P.top+P.height-N-c));W=e!==V,V=e}u.style.top=b(`top`,V,v.top,O&&!W),u.style.left=b(`left`,H,v.left,O&&!U);let G=k.current!==h,K=G?y(n):[];K.forEach(e=>{A.current.get(e)!==o&&(A.current.set(e,o),e.finished.then(o,o))});let q=e(v,a)||K.length>0;u.style.visibility=q?`hidden`:`visible`,!q&&G&&(k.current=h,h.getAnimations?.().forEach(e=>{e.currentTime=0}),E?.()),u.dataset.placement=R;let J=C.current;if(J){let e=J.offsetWidth/2||5;if(B){let t=v.left+v.width/2-H;J.style.setProperty(`--arrow-x`,`${Math.max(e+2,Math.min(t,M-e-2))}px`),J.style.removeProperty(`--arrow-y`)}else{let t=v.top+v.height/2-V;J.style.setProperty(`--arrow-y`,`${Math.max(e+2,Math.min(t,N-e-2))}px`),J.style.removeProperty(`--arrow-x`)}}},teardown:()=>{j(null);let e=x.current;if(!e)return;e.style.top=``,e.style.left=``,e.style.visibility=``,e.style.removeProperty(`position-anchor`);let t=S.current;t?.style.removeProperty(`min-width`),t?.style.removeProperty(`max-width`)}})}export{p as supportsAnchoring,x as usePopoverPosition};
2
2
  //# sourceMappingURL=usePopoverPosition.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"usePopoverPosition.mjs","names":[],"sources":["../../../../package/common/popover/usePopoverPosition.ts"],"sourcesContent":["import { useId, useRef, type RefObject } from 'react';\nimport { isHiddenByAnyAncestor } from 'components/common/utils/scrollAncestors';\nimport { getVisualViewportRect } from 'components/common/utils/visualViewport';\nimport { useAnchorTracking } from '../hooks/useAnchorTracking';\nimport { PopoverPlacement, PopoverAlign } from './popover.types';\n\ntype Axis = 'top' | 'bottom' | 'left' | 'right';\nconst OPPOSITE: Record<PopoverPlacement, Axis> = {\n [PopoverPlacement.Top]: 'bottom',\n [PopoverPlacement.Bottom]: 'top',\n [PopoverPlacement.Left]: 'right',\n [PopoverPlacement.Right]: 'left',\n};\n\nconst MARGIN = 8; // viewport gutter kept clear when shifting a panel back on-screen\n\n/**\n * Narrower than this and `shrinkToFit` gives up: a panel this size reads worse than\n * one that overhangs. Around four words a line for an error bubble, which is the\n * narrowest caller. A constant rather than an option, because the one caller that\n * shrinks has no reason to want a different floor; give it one the day a second\n * caller disagrees, as `shrinkToFit: boolean | number`.\n */\nconst MIN_SHRINK_WIDTH = 160;\n\n/** How far down to look for a box when the wrappers in between have none. */\nconst MAX_ANCHOR_DEPTH = 3;\n\n/**\n * The element to anchor to. A trigger with a box of its own is that element, which\n * is the case for a caller that hands over a real element (`Tooltip`).\n *\n * Popover's own trigger wrapper generates no box, deliberately, so that the\n * trigger keeps the place it would have had without Popover around it. Then the\n * real trigger is inside it, and this descends to find it, past any further\n * boxless element a consumer wrapped its trigger in: such an element has a zero\n * rect at the origin, which would otherwise send the panel to the top-left corner\n * of the screen.\n */\nfunction resolveAnchor(trigger: HTMLElement): { element: HTMLElement | null; rect: DOMRect } {\n let el: HTMLElement = trigger;\n for (let depth = 0; depth <= MAX_ANCHOR_DEPTH; depth++) {\n const rect = el.getBoundingClientRect();\n if (rect.width > 0 || rect.height > 0) return { element: el, rect };\n const child = el.firstElementChild as HTMLElement | null;\n if (!child) break;\n el = child;\n }\n return { element: null, rect: trigger.getBoundingClientRect() };\n}\n\nlet anchorSupport: boolean | undefined;\n\n// Probed, not just parsed: Firefox 155 resolves `anchor()` without the anchor's transforms, which\n// put a Dropdown in a VirtualList row, placed by `translateY`, above the list.\nexport function supportsAnchoring(): boolean {\n if (anchorSupport !== undefined) return anchorSupport;\n if (typeof CSS === 'undefined' || !CSS.supports?.('top: anchor(top, 0px)')) return (anchorSupport = false);\n const host = document.createElement('div');\n host.style.cssText = 'position: fixed; top: 0; left: 0; transform: translateY(100px); visibility: hidden';\n const anchor = document.createElement('div');\n anchor.style.cssText = 'anchor-name: --popover-probe; width: 1px; height: 1px';\n const probe = document.createElement('div');\n probe.style.cssText =\n 'position: fixed; position-anchor: --popover-probe; top: anchor(top, 0px); left: 0; visibility: hidden';\n host.append(anchor);\n document.body.append(host, probe);\n anchorSupport = Math.abs(probe.getBoundingClientRect().top - anchor.getBoundingClientRect().top) < 1;\n host.remove();\n probe.remove();\n return anchorSupport;\n}\n\nfunction anchorNames(el: HTMLElement): string[] {\n return el.style\n .getPropertyValue('anchor-name')\n .split(',')\n .map((name) => name.trim())\n .filter((name) => name && name !== 'none');\n}\n\n// Merged, not replaced: one trigger can carry a Tooltip and a Popover at once.\nfunction addAnchorName(el: HTMLElement, name: string) {\n const names = anchorNames(el);\n if (!names.includes(name)) el.style.setProperty('anchor-name', [...names, name].join(', '));\n}\n\nfunction removeAnchorName(el: HTMLElement, name: string) {\n const names = anchorNames(el).filter((n) => n !== name);\n if (names.length > 0) el.style.setProperty('anchor-name', names.join(', '));\n else el.style.removeProperty('anchor-name');\n}\n\n// Anchored, the browser moves the panel with the scroll in the same frame; the scroll listener\n// lands a frame or more later. The pixel fallback covers an anchor the browser refuses.\nfunction inset(side: 'top' | 'left', value: number, anchorEdge: number, anchored: boolean): string {\n if (!anchored) return `${value}px`;\n return `calc(anchor(${side}, ${anchorEdge}px) + ${value - anchorEdge}px)`;\n}\n\nexport interface UsePopoverPositionArgs {\n isOpen: boolean;\n placement: PopoverPlacement;\n align: PopoverAlign;\n offset: number;\n triggerRef: RefObject<HTMLElement | null>;\n /** `position: fixed` wrapper that receives top/left/visibility and the resolved-placement data attribute. */\n floatingRef: RefObject<HTMLElement | null>;\n /** Visible panel, measured to decide flip and shift. */\n panelRef: RefObject<HTMLElement | null>;\n /** Arrow element; gets `--arrow-x` / `--arrow-y` written to it. Pass a null ref when `withArrow` is false. */\n arrowRef: RefObject<HTMLElement | null>;\n /**\n * Give the panel at least the trigger's width, for a list that lines up with\n * the field it belongs to. A minimum rather than a fixed width: a menu on a\n * narrow icon button still sizes to its own content.\n */\n matchTriggerWidth?: boolean;\n /**\n * When the panel does not fit on the side its alignment puts it, narrow it\n * rather than sliding it away from the trigger. Sliding keeps the panel\n * on-screen but detaches it: an end-aligned bubble stops ending where its\n * trigger ends, and its arrow stops pointing at anything. Off by default, since\n * a panel whose width is its content, a menu or a list, would rather overhang\n * its trigger than wrap.\n *\n * Only down to `MIN_SHRINK_WIDTH`. Past that the panel is narrower than it is\n * readable, and a trigger that close to the screen edge is better served by a\n * panel that overhangs it.\n */\n shrinkToFit?: boolean;\n}\n\n/**\n * Positions a portaled, `position: fixed` popover on any of the four sides of its trigger with\n * start/center/end cross-axis alignment. Flips to the opposite side when the preferred one has no\n * room, shifts back on-screen along the cross axis, points the arrow at the trigger centre, and\n * hides (not closes) the panel once the trigger scrolls behind a clipping ancestor.\n *\n * `useAnchorTracking` supplies the lifecycle around this: mount wait, ancestor resolution, and\n * re-running on scroll, viewport resize and panel size changes.\n */\nexport function usePopoverPosition({\n isOpen,\n placement,\n align,\n offset,\n triggerRef,\n floatingRef,\n panelRef,\n arrowRef,\n matchTriggerWidth = false,\n shrinkToFit = false,\n}: UsePopoverPositionArgs) {\n const anchorName = `--popover-${useId().replace(/[^\\w-]/g, '')}`;\n const anchorRef = useRef<HTMLElement | null>(null);\n\n function setAnchor(next: HTMLElement | null) {\n if (anchorRef.current === next) return;\n if (anchorRef.current) removeAnchorName(anchorRef.current, anchorName);\n if (next) addAnchorName(next, anchorName);\n anchorRef.current = next;\n }\n\n useAnchorTracking({\n isOpen,\n triggerRef,\n elementRefs: [floatingRef, panelRef],\n // The panel's own size feeds the flip decision and, on the `top`/`left` sides, the coordinate\n // itself — so content that resizes while open has to be re-measured.\n resizeRefs: [panelRef],\n deps: [placement, align, offset, floatingRef, panelRef, arrowRef, matchTriggerWidth, shrinkToFit],\n\n update: (trigger, ancestors) => {\n const floating = floatingRef.current;\n const panel = panelRef.current;\n if (!floating || !panel) return;\n\n const { element: anchorElement, rect: t } = resolveAnchor(trigger);\n const anchored = anchorElement !== null && supportsAnchoring();\n setAnchor(anchored ? anchorElement : null);\n if (anchored) floating.style.setProperty('position-anchor', anchorName);\n else floating.style.removeProperty('position-anchor');\n // Written before the panel is measured, so `pw` below already accounts for it,\n // and taken back off when it is no longer wanted: the panel outlives the\n // switch, and an inline width would outrank whatever it switched to.\n if (matchTriggerWidth) panel.style.minWidth = `${t.width}px`;\n else panel.style.removeProperty('min-width');\n // The wrapper's box, not the panel's: `offsetWidth` rounds to whole pixels, so\n // aligning a panel 165.4px wide against a trigger edge at 406.47 left it 0.4px\n // past that edge. The panel's own rect is fractional but carries the entrance\n // animation's scale; the wrapper is the same layout box without the transform.\n // Cleared before measuring, or a cap left over from a narrower screen would be\n // read back as the panel's own width and never widen again.\n if (shrinkToFit) panel.style.removeProperty('max-width');\n let { width: pw, height: ph } = floating.getBoundingClientRect();\n // The visible area, not the full layout viewport — shrinks on mobile when a keyboard is\n // open, so the panel flips/clamps against what's actually on screen instead of ending up\n // hidden behind the keyboard.\n const vv = getVisualViewportRect();\n\n // Flipping only swaps a side for its opposite, so which axis the alignment runs\n // along is decided by the preferred placement alone, before any of that.\n const alignsHorizontally = placement === PopoverPlacement.Top || placement === PopoverPlacement.Bottom;\n\n if (shrinkToFit && alignsHorizontally) {\n const alignedSide =\n align === PopoverAlign.End\n ? t.right - (vv.left + MARGIN)\n : align === PopoverAlign.Start\n ? vv.left + vv.width - MARGIN - t.left\n : vv.width - 2 * MARGIN;\n const room = Math.min(alignedSide, vv.width - 2 * MARGIN);\n if (pw > room && room >= MIN_SHRINK_WIDTH) {\n panel.style.maxWidth = `${room}px`;\n // Wrapping makes it taller, and the height is what the flip below is decided on.\n ({ width: pw, height: ph } = floating.getBoundingClientRect());\n }\n }\n\n // Resolve the main-axis side, flipping when the preferred side lacks room and the\n // opposite side has more.\n const room: Record<Axis, number> = {\n top: t.top - vv.top,\n bottom: vv.top + vv.height - t.bottom,\n left: t.left - vv.left,\n right: vv.left + vv.width - t.right,\n };\n const need = placement === PopoverPlacement.Top || placement === PopoverPlacement.Bottom ? ph : pw;\n let side: Axis = placement as unknown as Axis;\n const opp = OPPOSITE[placement];\n if (room[side] < need + offset && room[opp] > room[side]) side = opp;\n\n const vertical = side === 'top' || side === 'bottom';\n\n // Main axis: place the panel just outside the trigger on the resolved side.\n let top: number;\n let left: number;\n if (side === 'bottom') {\n top = t.bottom + offset;\n } else if (side === 'top') {\n top = t.top - offset - ph;\n } else if (side === 'right') {\n left = t.right + offset;\n } else {\n left = t.left - offset - pw;\n }\n\n // Cross axis: align start/center/end against the trigger, then shift on-screen.\n let shiftedX = false;\n let shiftedY = false;\n if (vertical) {\n if (align === PopoverAlign.Start) left = t.left;\n else if (align === PopoverAlign.End) left = t.right - pw;\n else left = t.left + t.width / 2 - pw / 2;\n const clamped = Math.max(vv.left + MARGIN, Math.min(left!, vv.left + vv.width - pw - MARGIN));\n shiftedX = clamped !== left!;\n left = clamped;\n } else {\n if (align === PopoverAlign.Start) top = t.top;\n else if (align === PopoverAlign.End) top = t.bottom - ph;\n else top = t.top + t.height / 2 - ph / 2;\n const clamped = Math.max(vv.top + MARGIN, Math.min(top!, vv.top + vv.height - ph - MARGIN));\n shiftedY = clamped !== top!;\n top = clamped;\n }\n\n // A shifted axis is pinned to the viewport edge, so it stays in pixels: anchored, the scroll\n // would carry it off the edge until the next update pulled it back, which reads as shaking.\n floating.style.top = inset('top', top!, t.top, anchored && !shiftedY);\n floating.style.left = inset('left', left!, t.left, anchored && !shiftedX);\n floating.style.visibility = isHiddenByAnyAncestor(t, ancestors) ? 'hidden' : 'visible';\n floating.dataset.placement = side;\n\n // Arrow: centre it on the trigger along the cross axis, clamped inside the panel edges.\n const arrow = arrowRef.current;\n if (arrow) {\n const half = arrow.offsetWidth / 2 || 5;\n if (vertical) {\n const x = t.left + t.width / 2 - left!;\n arrow.style.setProperty('--arrow-x', `${Math.max(half + 2, Math.min(x, pw - half - 2))}px`);\n arrow.style.removeProperty('--arrow-y');\n } else {\n const y = t.top + t.height / 2 - top!;\n arrow.style.setProperty('--arrow-y', `${Math.max(half + 2, Math.min(y, ph - half - 2))}px`);\n arrow.style.removeProperty('--arrow-x');\n }\n }\n },\n\n teardown: () => {\n // Positioning is written as inline styles, which outrank any class the panel might\n // switch to while it stays mounted — a popover turning into a bottom sheet (`useSheet`)\n // would keep the coordinates computed for its anchored self.\n setAnchor(null);\n const floating = floatingRef.current;\n if (!floating) return;\n floating.style.top = '';\n floating.style.left = '';\n floating.style.visibility = '';\n floating.style.removeProperty('position-anchor');\n const panel = panelRef.current;\n panel?.style.removeProperty('min-width');\n panel?.style.removeProperty('max-width');\n },\n });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"usePopoverPosition.mjs","names":[],"sources":["../../../../package/common/popover/usePopoverPosition.ts"],"sourcesContent":["import { useId, useRef, type RefObject } from 'react';\nimport { isHiddenByAnyAncestor } from 'components/common/utils/scrollAncestors';\nimport { getVisualViewportRect } from 'components/common/utils/visualViewport';\nimport { useAnchorTracking } from '../hooks/useAnchorTracking';\nimport { PopoverPlacement, PopoverAlign } from './popover.types';\n\ntype Axis = 'top' | 'bottom' | 'left' | 'right';\nconst OPPOSITE: Record<PopoverPlacement, Axis> = {\n [PopoverPlacement.Top]: 'bottom',\n [PopoverPlacement.Bottom]: 'top',\n [PopoverPlacement.Left]: 'right',\n [PopoverPlacement.Right]: 'left',\n};\n\nconst MARGIN = 8; // viewport gutter kept clear when shifting a panel back on-screen\n\n/**\n * Narrower than this and `shrinkToFit` gives up: a panel this size reads worse than\n * one that overhangs. Around four words a line for an error bubble, which is the\n * narrowest caller. A constant rather than an option, because the one caller that\n * shrinks has no reason to want a different floor; give it one the day a second\n * caller disagrees, as `shrinkToFit: boolean | number`.\n */\nconst MIN_SHRINK_WIDTH = 160;\n\n/** How far down to look for a box when the wrappers in between have none. */\nconst MAX_ANCHOR_DEPTH = 3;\n\n/**\n * The element to anchor to. A trigger with a box of its own is that element, which\n * is the case for a caller that hands over a real element (`Tooltip`).\n *\n * Popover's own trigger wrapper generates no box, deliberately, so that the\n * trigger keeps the place it would have had without Popover around it. Then the\n * real trigger is inside it, and this descends to find it, past any further\n * boxless element a consumer wrapped its trigger in: such an element has a zero\n * rect at the origin, which would otherwise send the panel to the top-left corner\n * of the screen.\n */\nfunction resolveAnchor(trigger: HTMLElement): { element: HTMLElement | null; rect: DOMRect } {\n let el: HTMLElement = trigger;\n for (let depth = 0; depth <= MAX_ANCHOR_DEPTH; depth++) {\n const rect = el.getBoundingClientRect();\n if (rect.width > 0 || rect.height > 0) return { element: el, rect };\n const child = el.firstElementChild as HTMLElement | null;\n if (!child) break;\n el = child;\n }\n return { element: null, rect: trigger.getBoundingClientRect() };\n}\n\nlet anchorSupport: boolean | undefined;\n\n// Probed, not just parsed: Firefox 155 resolves `anchor()` without the anchor's transforms, which\n// put a Dropdown in a VirtualList row, placed by `translateY`, above the list.\nexport function supportsAnchoring(): boolean {\n if (anchorSupport !== undefined) return anchorSupport;\n if (typeof CSS === 'undefined' || !CSS.supports?.('top: anchor(top, 0px)')) return (anchorSupport = false);\n const host = document.createElement('div');\n host.style.cssText = 'position: fixed; top: 0; left: 0; transform: translateY(100px); visibility: hidden';\n const anchor = document.createElement('div');\n anchor.style.cssText = 'anchor-name: --popover-probe; width: 1px; height: 1px';\n const probe = document.createElement('div');\n probe.style.cssText =\n 'position: fixed; position-anchor: --popover-probe; top: anchor(top, 0px); left: 0; visibility: hidden';\n host.append(anchor);\n document.body.append(host, probe);\n anchorSupport = Math.abs(probe.getBoundingClientRect().top - anchor.getBoundingClientRect().top) < 1;\n host.remove();\n probe.remove();\n return anchorSupport;\n}\n\nfunction anchorNames(el: HTMLElement): string[] {\n return el.style\n .getPropertyValue('anchor-name')\n .split(',')\n .map((name) => name.trim())\n .filter((name) => name && name !== 'none');\n}\n\n// Merged, not replaced: one trigger can carry a Tooltip and a Popover at once.\nfunction addAnchorName(el: HTMLElement, name: string) {\n const names = anchorNames(el);\n if (!names.includes(name)) el.style.setProperty('anchor-name', [...names, name].join(', '));\n}\n\nfunction removeAnchorName(el: HTMLElement, name: string) {\n const names = anchorNames(el).filter((n) => n !== name);\n if (names.length > 0) el.style.setProperty('anchor-name', names.join(', '));\n else el.style.removeProperty('anchor-name');\n}\n\nconst MOVING_PROPERTIES = ['transform', 'translate', 'scale', 'rotate'];\n\n/**\n * Longer than this left to run and the panel shows anyway, mid-way, and is corrected when the\n * animation ends. A Modal or Drawer entrance is well under it; waiting out a long one read as\n * the panel not opening at all.\n */\nconst MAX_WAIT_MS = 1000;\n\n/** The running animations and transitions on ancestors of `trigger` that move it. */\nfunction animationsMoving(trigger: HTMLElement): Animation[] {\n return (document.getAnimations?.() ?? []).filter((animation) => {\n if (animation.playState !== 'running' || !(animation.effect instanceof KeyframeEffect)) return false;\n const { target, pseudoElement } = animation.effect;\n // A `::before` or `::after` is a box of its own: its transform moves nothing else.\n if (!target || pseudoElement || target === trigger || !target.contains(trigger)) return false;\n // Covers one that never ends, which would keep the panel hidden for good.\n const { endTime, localTime } = animation.effect.getComputedTiming();\n if (Number(endTime) - Number(localTime) > MAX_WAIT_MS) return false;\n return animation.effect.getKeyframes().some((frame) => MOVING_PROPERTIES.some((property) => property in frame));\n });\n}\n\n// Anchored, the browser moves the panel with the scroll in the same frame; the scroll listener\n// lands a frame or more later. The pixel fallback covers an anchor the browser refuses.\nfunction inset(side: 'top' | 'left', value: number, anchorEdge: number, anchored: boolean): string {\n if (!anchored) return `${value}px`;\n return `calc(anchor(${side}, ${anchorEdge}px) + ${value - anchorEdge}px)`;\n}\n\nexport interface UsePopoverPositionArgs {\n isOpen: boolean;\n placement: PopoverPlacement;\n align: PopoverAlign;\n offset: number;\n triggerRef: RefObject<HTMLElement | null>;\n /** `position: fixed` wrapper that receives top/left/visibility and the resolved-placement data attribute. */\n floatingRef: RefObject<HTMLElement | null>;\n /** Visible panel, measured to decide flip and shift. */\n panelRef: RefObject<HTMLElement | null>;\n /** Arrow element; gets `--arrow-x` / `--arrow-y` written to it. Pass a null ref when `withArrow` is false. */\n arrowRef: RefObject<HTMLElement | null>;\n /**\n * Give the panel at least the trigger's width, for a list that lines up with\n * the field it belongs to. A minimum rather than a fixed width: a menu on a\n * narrow icon button still sizes to its own content.\n */\n matchTriggerWidth?: boolean;\n /**\n * When the panel does not fit on the side its alignment puts it, narrow it\n * rather than sliding it away from the trigger. Sliding keeps the panel\n * on-screen but detaches it: an end-aligned bubble stops ending where its\n * trigger ends, and its arrow stops pointing at anything. Off by default, since\n * a panel whose width is its content, a menu or a list, would rather overhang\n * its trigger than wrap.\n *\n * Only down to `MIN_SHRINK_WIDTH`. Past that the panel is narrower than it is\n * readable, and a trigger that close to the screen edge is better served by a\n * panel that overhangs it.\n */\n shrinkToFit?: boolean;\n /**\n * Called when a newly opened panel first becomes visible. Focus has to wait for it: a panel\n * still hidden, waiting out an ancestor's animation, takes none.\n */\n onShown?: () => void;\n}\n\n/**\n * Positions a portaled, `position: fixed` popover on any of the four sides of its trigger with\n * start/center/end cross-axis alignment. Flips to the opposite side when the preferred one has no\n * room, shifts back on-screen along the cross axis, points the arrow at the trigger centre, and\n * hides (not closes) the panel once the trigger scrolls behind a clipping ancestor.\n *\n * `useAnchorTracking` supplies the lifecycle around this: mount wait, ancestor resolution, and\n * re-running on scroll, viewport resize and panel size changes.\n */\nexport function usePopoverPosition({\n isOpen,\n placement,\n align,\n offset,\n triggerRef,\n floatingRef,\n panelRef,\n arrowRef,\n matchTriggerWidth = false,\n shrinkToFit = false,\n onShown,\n}: UsePopoverPositionArgs) {\n const anchorName = `--popover-${useId().replace(/[^\\w-]/g, '')}`;\n const anchorRef = useRef<HTMLElement | null>(null);\n const shownPanelRef = useRef<HTMLElement | null>(null);\n // Every update before the first show finds the same animations again; one rerun each is enough.\n // Keyed on `rerun`, since a restarted tracking brings a new one that has to be called too.\n const awaitedRef = useRef(new WeakMap<Animation, () => void>());\n\n function setAnchor(next: HTMLElement | null) {\n if (anchorRef.current === next) return;\n if (anchorRef.current) removeAnchorName(anchorRef.current, anchorName);\n if (next) addAnchorName(next, anchorName);\n anchorRef.current = next;\n }\n\n useAnchorTracking({\n isOpen,\n triggerRef,\n elementRefs: [floatingRef, panelRef],\n // The panel's own size feeds the flip decision and, on the `top`/`left` sides, the coordinate\n // itself — so content that resizes while open has to be re-measured.\n resizeRefs: [panelRef],\n deps: [placement, align, offset, floatingRef, panelRef, arrowRef, matchTriggerWidth, shrinkToFit],\n\n update: (trigger, ancestors, rerun) => {\n const floating = floatingRef.current;\n const panel = panelRef.current;\n if (!floating || !panel) return;\n\n const { element: anchorElement, rect: t } = resolveAnchor(trigger);\n const anchored = anchorElement !== null && supportsAnchoring();\n setAnchor(anchored ? anchorElement : null);\n if (anchored) floating.style.setProperty('position-anchor', anchorName);\n else floating.style.removeProperty('position-anchor');\n // Written before the panel is measured, so `pw` below already accounts for it,\n // and taken back off when it is no longer wanted: the panel outlives the\n // switch, and an inline width would outrank whatever it switched to.\n if (matchTriggerWidth) panel.style.minWidth = `${t.width}px`;\n else panel.style.removeProperty('min-width');\n // The wrapper's box, not the panel's: `offsetWidth` rounds to whole pixels, so\n // aligning a panel 165.4px wide against a trigger edge at 406.47 left it 0.4px\n // past that edge. The panel's own rect is fractional but carries the entrance\n // animation's scale; the wrapper is the same layout box without the transform.\n // Cleared before measuring, or a cap left over from a narrower screen would be\n // read back as the panel's own width and never widen again.\n if (shrinkToFit) panel.style.removeProperty('max-width');\n let { width: pw, height: ph } = floating.getBoundingClientRect();\n // The visible area, not the full layout viewport — shrinks on mobile when a keyboard is\n // open, so the panel flips/clamps against what's actually on screen instead of ending up\n // hidden behind the keyboard.\n const vv = getVisualViewportRect();\n\n // Flipping only swaps a side for its opposite, so which axis the alignment runs\n // along is decided by the preferred placement alone, before any of that.\n const alignsHorizontally = placement === PopoverPlacement.Top || placement === PopoverPlacement.Bottom;\n\n if (shrinkToFit && alignsHorizontally) {\n const alignedSide =\n align === PopoverAlign.End\n ? t.right - (vv.left + MARGIN)\n : align === PopoverAlign.Start\n ? vv.left + vv.width - MARGIN - t.left\n : vv.width - 2 * MARGIN;\n const room = Math.min(alignedSide, vv.width - 2 * MARGIN);\n if (pw > room && room >= MIN_SHRINK_WIDTH) {\n panel.style.maxWidth = `${room}px`;\n // Wrapping makes it taller, and the height is what the flip below is decided on.\n ({ width: pw, height: ph } = floating.getBoundingClientRect());\n }\n }\n\n // Resolve the main-axis side, flipping when the preferred side lacks room and the\n // opposite side has more.\n const room: Record<Axis, number> = {\n top: t.top - vv.top,\n bottom: vv.top + vv.height - t.bottom,\n left: t.left - vv.left,\n right: vv.left + vv.width - t.right,\n };\n const need = placement === PopoverPlacement.Top || placement === PopoverPlacement.Bottom ? ph : pw;\n let side: Axis = placement as unknown as Axis;\n const opp = OPPOSITE[placement];\n if (room[side] < need + offset && room[opp] > room[side]) side = opp;\n\n const vertical = side === 'top' || side === 'bottom';\n\n // Main axis: place the panel just outside the trigger on the resolved side.\n let top: number;\n let left: number;\n if (side === 'bottom') {\n top = t.bottom + offset;\n } else if (side === 'top') {\n top = t.top - offset - ph;\n } else if (side === 'right') {\n left = t.right + offset;\n } else {\n left = t.left - offset - pw;\n }\n\n // Cross axis: align start/center/end against the trigger, then shift on-screen.\n let shiftedX = false;\n let shiftedY = false;\n if (vertical) {\n if (align === PopoverAlign.Start) left = t.left;\n else if (align === PopoverAlign.End) left = t.right - pw;\n else left = t.left + t.width / 2 - pw / 2;\n const clamped = Math.max(vv.left + MARGIN, Math.min(left!, vv.left + vv.width - pw - MARGIN));\n shiftedX = clamped !== left!;\n left = clamped;\n } else {\n if (align === PopoverAlign.Start) top = t.top;\n else if (align === PopoverAlign.End) top = t.bottom - ph;\n else top = t.top + t.height / 2 - ph / 2;\n const clamped = Math.max(vv.top + MARGIN, Math.min(top!, vv.top + vv.height - ph - MARGIN));\n shiftedY = clamped !== top!;\n top = clamped;\n }\n\n // A shifted axis is pinned to the viewport edge, so it stays in pixels: anchored, the scroll\n // would carry it off the edge until the next update pulled it back, which reads as shaking.\n floating.style.top = inset('top', top!, t.top, anchored && !shiftedY);\n floating.style.left = inset('left', left!, t.left, anchored && !shiftedX);\n // Until first shown, it waits out an ancestor that is still moving the trigger, such as a\n // Modal scaling in: placed mid-way, it showed away from the trigger and jumped at the end.\n // Awaited through `finished`, since a Web Animation fires no `animationend`; a cancelled\n // one stops moving the trigger too.\n const firstShow = shownPanelRef.current !== panel;\n const moving = firstShow ? animationsMoving(trigger) : [];\n moving.forEach((animation) => {\n if (awaitedRef.current.get(animation) === rerun) return;\n awaitedRef.current.set(animation, rerun);\n animation.finished.then(rerun, rerun);\n });\n const hidden = isHiddenByAnyAncestor(t, ancestors) || moving.length > 0;\n floating.style.visibility = hidden ? 'hidden' : 'visible';\n // The entrance animation starts on mount, so a panel first placed hidden played it unseen.\n // Per panel, since a new one mounts on every open.\n if (!hidden && firstShow) {\n shownPanelRef.current = panel;\n panel.getAnimations?.().forEach((animation) => {\n animation.currentTime = 0;\n });\n onShown?.();\n }\n floating.dataset.placement = side;\n\n // Arrow: centre it on the trigger along the cross axis, clamped inside the panel edges.\n const arrow = arrowRef.current;\n if (arrow) {\n const half = arrow.offsetWidth / 2 || 5;\n if (vertical) {\n const x = t.left + t.width / 2 - left!;\n arrow.style.setProperty('--arrow-x', `${Math.max(half + 2, Math.min(x, pw - half - 2))}px`);\n arrow.style.removeProperty('--arrow-y');\n } else {\n const y = t.top + t.height / 2 - top!;\n arrow.style.setProperty('--arrow-y', `${Math.max(half + 2, Math.min(y, ph - half - 2))}px`);\n arrow.style.removeProperty('--arrow-x');\n }\n }\n },\n\n teardown: () => {\n // Positioning is written as inline styles, which outrank any class the panel might\n // switch to while it stays mounted — a popover turning into a bottom sheet (`useSheet`)\n // would keep the coordinates computed for its anchored self.\n setAnchor(null);\n const floating = floatingRef.current;\n if (!floating) return;\n floating.style.top = '';\n floating.style.left = '';\n floating.style.visibility = '';\n floating.style.removeProperty('position-anchor');\n const panel = panelRef.current;\n panel?.style.removeProperty('min-width');\n panel?.style.removeProperty('max-width');\n },\n });\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"sheet.module.mjs","names":[],"sources":["../../../../package/common/styles/sheet.module.pcss"],"sourcesContent":["/*\n * Bottom-sheet presentation, shared by every popup that can switch away from being anchored to\n * its trigger (see `useSheet`).\n *\n * Every selector is doubled (`.sheet.sheet`) on purpose: these classes land on the same elements\n * as a component's own single-class panel styles and have to win over them. Which of two\n * equal-specificity rules wins otherwise depends on the order the bundler happens to emit the\n * two CSS modules in, which is not something a component should have to depend on.\n */\n\n.sheet.sheet {\n position: fixed;\n left: 0;\n right: 0;\n margin: 0 auto;\n width: 100%;\n\n /* Wide enough for a comfortable list on a phone, narrow enough not to span a tablet —\n `Presentation.Auto` reaches a 744px one, where full width would be a banner rather than a\n sheet. A token like the other two sheet dimensions, since this selector is doubled and a\n consumer could otherwise only beat it with an inline style. */\n max-width: var(--sheet-max-width, 560px);\n\n /* Two separate caps: how much of the screen a sheet may take at most, and how much screen\n there actually is — the latter set inline by `useSheet` from the visual viewport, since no\n viewport unit shrinks when the on-screen keyboard opens. */\n max-height: min(var(--sheet-max-height, 70vh), var(--sheet-available-height, 100vh));\n max-height: min(var(--sheet-max-height, 70dvh), var(--sheet-available-height, 100dvh));\n display: flex;\n flex-direction: column;\n z-index: 1;\n animation: sheetSlideIn 0.2s ease-out;\n}\n\n/* What a sheet looks like where it meets the screen, split out from `.sheet-panel` because a\n component whose own root is the surface (Modal) needs the look without the fill-the-root\n layout below it. */\n.sheet-surface.sheet-surface {\n overscroll-behavior: contain;\n border-radius: calc(var(--default-border-radius) + 4px) calc(var(--default-border-radius) + 4px) 0 0;\n\n /* Clears the home indicator on a phone without leaving a gap on a device that has none. */\n padding-bottom: max(var(--sheet-safe-padding, 8px), env(safe-area-inset-bottom));\n}\n\n.sheet-panel {\n composes: sheet-surface;\n}\n\n.sheet-panel.sheet-panel {\n position: static;\n width: 100%;\n box-sizing: border-box;\n margin: 0;\n display: flex;\n flex-direction: column;\n min-height: 0;\n max-height: 100%;\n\n /* Overrides a panel that caps its own width when anchored, e.g. Popover's 320px. */\n max-width: 100%;\n\n /* The sheet root owns the entrance animation; a panel that scales itself in when anchored\n (Popover) would otherwise play a second, conflicting one on top of the slide-up. */\n animation: none;\n overflow-y: auto;\n}\n\n/* For an inner list that caps its own height when anchored: the sheet caps the height instead. */\n.sheet-scroll.sheet-scroll {\n max-height: none;\n flex: 1 1 auto;\n min-height: 0;\n}\n\n.sheet-backdrop.sheet-backdrop {\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n background: #00000052;\n z-index: 1;\n animation: sheetBackdropFadeIn 0.2s ease-out;\n}\n\n@keyframes sheetSlideIn {\n from {\n transform: translateY(100%);\n }\n\n to {\n transform: translateY(0);\n }\n}\n\n@keyframes sheetBackdropFadeIn {\n from {\n opacity: 0;\n }\n\n to {\n opacity: 1;\n }\n}\n\n/* Neither animation has a fill mode, so removing them lands both at their natural state: the\n panel already at translateY(0), the backdrop already opaque. */\n@media (prefers-reduced-motion: reduce) {\n .sheet-panel,\n .sheet-backdrop.sheet-backdrop {\n animation: none;\n }\n}\n"],"mappings":""}
1
+ {"version":3,"file":"sheet.module.mjs","names":[],"sources":["../../../../package/common/styles/sheet.module.pcss"],"sourcesContent":["/*\n * Bottom-sheet presentation, shared by every popup that can switch away from being anchored to\n * its trigger (see `useSheet`).\n *\n * Every selector is doubled (`.sheet.sheet`) on purpose: these classes land on the same elements\n * as a component's own single-class panel styles and have to win over them. Which of two\n * equal-specificity rules wins otherwise depends on the order the bundler happens to emit the\n * two CSS modules in, which is not something a component should have to depend on.\n */\n\n.sheet.sheet {\n position: fixed;\n left: 0;\n right: 0;\n margin: 0 auto;\n width: 100%;\n\n /* Wide enough for a comfortable list on a phone, narrow enough not to span a tablet —\n `Presentation.Auto` reaches a 744px one, where full width would be a banner rather than a\n sheet. A token like the other two sheet dimensions, since this selector is doubled and a\n consumer could otherwise only beat it with an inline style. */\n max-width: var(--sheet-max-width, 560px);\n\n /* Two separate caps: how much of the screen a sheet may take at most, and how much screen\n there actually is — the latter set inline by `useSheet` from the visual viewport, since no\n viewport unit shrinks when the on-screen keyboard opens. */\n max-height: min(var(--sheet-max-height, 70vh), var(--sheet-available-height, 100vh));\n max-height: min(var(--sheet-max-height, 70dvh), var(--sheet-available-height, 100dvh));\n display: flex;\n flex-direction: column;\n z-index: 1;\n animation: sheetSlideIn 0.2s ease-out;\n}\n\n/* What a sheet looks like where it meets the screen, split out from `.sheet-panel` because a\n component whose own root is the surface (Modal) needs the look without the fill-the-root\n layout below it. */\n.sheet-surface.sheet-surface {\n overscroll-behavior: contain;\n border-radius: calc(var(--default-border-radius) + 4px) calc(var(--default-border-radius) + 4px) 0 0;\n\n /* Clears the home indicator on a phone without leaving a gap on a device that has none. */\n padding-bottom: max(var(--sheet-safe-padding, 8px), env(safe-area-inset-bottom));\n}\n\n.sheet-panel {\n composes: sheet-surface;\n}\n\n.sheet-panel.sheet-panel {\n position: static;\n width: 100%;\n box-sizing: border-box;\n margin: 0;\n display: flex;\n flex-direction: column;\n min-height: 0;\n max-height: 100%;\n\n /* Overrides a panel that caps its own width when anchored, e.g. Popover's 320px. */\n max-width: 100%;\n\n /* The sheet root owns the entrance animation; a panel that scales itself in when anchored\n (Popover) would otherwise play a second, conflicting one on top of the slide-up. */\n animation: none;\n overflow-y: auto;\n}\n\n/* For an inner list that caps its own height when anchored: the sheet caps the height instead. */\n.sheet-scroll.sheet-scroll {\n max-height: none;\n flex: 1 1 auto;\n min-height: 0;\n}\n\n.sheet-backdrop.sheet-backdrop {\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n background: #00000052;\n\n /* Shared by every overlay, so the order they are portaled in, which is the order they opened in, decides. */\n z-index: 1;\n animation: sheetBackdropFadeIn 0.2s ease-out;\n}\n\n@keyframes sheetSlideIn {\n from {\n transform: translateY(100%);\n }\n\n to {\n transform: translateY(0);\n }\n}\n\n@keyframes sheetBackdropFadeIn {\n from {\n opacity: 0;\n }\n\n to {\n opacity: 1;\n }\n}\n\n/* Neither animation has a fill mode, so removing them lands both at their natural state: the\n panel already at translateY(0), the backdrop already opaque. */\n@media (prefers-reduced-motion: reduce) {\n .sheet-panel,\n .sheet-backdrop.sheet-backdrop {\n animation: none;\n }\n}\n"],"mappings":""}