@funstack/ui 0.1.0-alpha.7 → 0.1.0-alpha.8

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.
@@ -10,7 +10,30 @@ function positionArea(side, align) {
10
10
  if (align === "center") return side;
11
11
  return `${side} ${side === "top" || side === "bottom" ? align === "start" ? "span-right" : "span-left" : align === "start" ? "span-bottom" : "span-top"}`;
12
12
  }
13
+ /** Custom property carrying the x coordinate of a point-placed popup (viewport px). */
14
+ const MENU_POINT_X = "--menu-point-x";
15
+ /** Custom property carrying the y coordinate of a point-placed popup (viewport px). */
16
+ const MENU_POINT_Y = "--menu-point-y";
17
+ /** The dedupe key of the shipped rules (`<style href precedence>`). */
18
+ const MENU_POINT_HREF = "@funstack/ui/menu-point";
19
+ /** The React stylesheet precedence bucket of the library's functional CSS. */
20
+ const FSUI_PRECEDENCE = "fsui";
21
+ /**
22
+ * The library's only shipped CSS: three `@position-try` options that mirror a point-placed popup
23
+ * (`left` / `top` at the point, inline) around the point when it does not fit: upward, leftward,
24
+ * both. Functional at-rules with no selectors: no element is styled. `100%` resolves against the
25
+ * viewport, the top layer's containing block, which `clientX` / `clientY` are measured in.
26
+ */
27
+ const MENU_POINT_RULES = `/* @funstack/ui: placement of a menu at a point. Functional only: the base position is inline on the popup (left/top at the point); these options mirror it around the point when it does not fit. */
28
+ @position-try --fsui-menu-up { top: auto; bottom: calc(100% - var(${MENU_POINT_Y})); }\n@position-try --fsui-menu-left { left: auto; right: calc(100% - var(${MENU_POINT_X})); }\n@position-try --fsui-menu-up-left { top: auto; bottom: calc(100% - var(${MENU_POINT_Y})); left: auto; right: calc(100% - var(${MENU_POINT_X})); }\n`;
29
+ /**
30
+ * Two equivalent spellings of the point fallback list, alternated by the placement's generation:
31
+ * changing `position-try-fallbacks` clears the remembered "last successful position option", so a
32
+ * re-show at a new point does not keep the previous flip (design 4.3). The first two options are
33
+ * order-independent (only one of them can fix a given overflow).
34
+ */
35
+ const MENU_POINT_FALLBACKS = ["--fsui-menu-up, --fsui-menu-left, --fsui-menu-up-left", "--fsui-menu-left, --fsui-menu-up, --fsui-menu-up-left"];
13
36
  //#endregion
14
- export { POSITION_TRY_FALLBACKS, positionArea };
37
+ export { FSUI_PRECEDENCE, MENU_POINT_FALLBACKS, MENU_POINT_HREF, MENU_POINT_RULES, MENU_POINT_X, MENU_POINT_Y, POSITION_TRY_FALLBACKS, positionArea };
15
38
 
16
39
  //# sourceMappingURL=anchor-position.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"anchor-position.mjs","names":[],"sources":["../../src/internal/anchor-position.ts"],"sourcesContent":["// Placement of a popover against its implicit anchor (the invoker set by `commandfor`,\n// `interestfor` or `showPopover({ source })`): inline `position-area` from a side and an\n// alignment, plus `position-try-fallbacks` that flip it when it does not fit. Shared by Tooltip,\n// Popover and Menu (design/components/tooltip.md 7.1, popover.md 8).\n//\n// No \"use client\" directive: pure functions, importable from anywhere (conventions 7.1).\n\n/** Physical side of the anchor the popup is placed on. */\nexport type AnchorSide = \"top\" | \"bottom\" | \"left\" | \"right\";\n/** Alignment along that side. */\nexport type AnchorAlign = \"start\" | \"center\" | \"end\";\n\n/** Flip to the opposite side, then the other axis, then both, when the popup does not fit. */\nexport const POSITION_TRY_FALLBACKS = \"flip-block, flip-inline, flip-block flip-inline\";\n\n/**\n * The `position-area` for a side and an alignment (physical keywords): `bottom` + `center` is\n * `bottom`, `bottom` + `start` is `bottom span-right`, `right` + `start` is `right span-bottom`,\n * and so on.\n */\nexport function positionArea(side: AnchorSide, align: AnchorAlign): string {\n if (align === \"center\") return side;\n const vertical = side === \"top\" || side === \"bottom\";\n const span = vertical\n ? align === \"start\"\n ? \"span-right\"\n : \"span-left\"\n : align === \"start\"\n ? \"span-bottom\"\n : \"span-top\";\n return `${side} ${span}`;\n}\n"],"mappings":";;AAaA,MAAa,yBAAyB;;;;;;AAOtC,SAAgB,aAAa,MAAkB,OAA4B;CACzE,IAAI,UAAU,UAAU,OAAO;CAS/B,OAAO,GAAG,KAAK,GARE,SAAS,SAAS,SAAS,WAExC,UAAU,UACR,eACA,cACF,UAAU,UACR,gBACA;AAER"}
1
+ {"version":3,"file":"anchor-position.mjs","names":[],"sources":["../../src/internal/anchor-position.ts"],"sourcesContent":["// Placement of a popover against its implicit anchor (the invoker set by `commandfor`,\n// `interestfor` or `showPopover({ source })`): inline `position-area` from a side and an\n// alignment, plus `position-try-fallbacks` that flip it when it does not fit. Shared by Tooltip,\n// Popover and Menu (design/components/tooltip.md 7.1, popover.md 8).\n//\n// No \"use client\" directive: pure functions, importable from anywhere (conventions 7.1).\n\n/** Physical side of the anchor the popup is placed on. */\nexport type AnchorSide = \"top\" | \"bottom\" | \"left\" | \"right\";\n/** Alignment along that side. */\nexport type AnchorAlign = \"start\" | \"center\" | \"end\";\n\n/** Flip to the opposite side, then the other axis, then both, when the popup does not fit. */\nexport const POSITION_TRY_FALLBACKS = \"flip-block, flip-inline, flip-block flip-inline\";\n\n/**\n * The `position-area` for a side and an alignment (physical keywords): `bottom` + `center` is\n * `bottom`, `bottom` + `start` is `bottom span-right`, `right` + `start` is `right span-bottom`,\n * and so on.\n */\nexport function positionArea(side: AnchorSide, align: AnchorAlign): string {\n if (align === \"center\") return side;\n const vertical = side === \"top\" || side === \"bottom\";\n const span = vertical\n ? align === \"start\"\n ? \"span-right\"\n : \"span-left\"\n : align === \"start\"\n ? \"span-bottom\"\n : \"span-top\";\n return `${side} ${span}`;\n}\n\n// --- Placement at a point (design/components/context-menu.md 4.2, conventions 12) ------------\n\n/** Custom property carrying the x coordinate of a point-placed popup (viewport px). */\nexport const MENU_POINT_X = \"--menu-point-x\";\n/** Custom property carrying the y coordinate of a point-placed popup (viewport px). */\nexport const MENU_POINT_Y = \"--menu-point-y\";\n\n/** The dedupe key of the shipped rules (`<style href precedence>`). */\nexport const MENU_POINT_HREF = \"@funstack/ui/menu-point\";\n/** The React stylesheet precedence bucket of the library's functional CSS. */\nexport const FSUI_PRECEDENCE = \"fsui\";\n\n/**\n * The library's only shipped CSS: three `@position-try` options that mirror a point-placed popup\n * (`left` / `top` at the point, inline) around the point when it does not fit: upward, leftward,\n * both. Functional at-rules with no selectors: no element is styled. `100%` resolves against the\n * viewport, the top layer's containing block, which `clientX` / `clientY` are measured in.\n */\nexport const MENU_POINT_RULES =\n \"/* @funstack/ui: placement of a menu at a point. Functional only: the base position is inline \" +\n \"on the popup (left/top at the point); these options mirror it around the point when it does \" +\n \"not fit. */\\n\" +\n `@position-try --fsui-menu-up { top: auto; bottom: calc(100% - var(${MENU_POINT_Y})); }\\n` +\n `@position-try --fsui-menu-left { left: auto; right: calc(100% - var(${MENU_POINT_X})); }\\n` +\n `@position-try --fsui-menu-up-left { top: auto; bottom: calc(100% - var(${MENU_POINT_Y})); ` +\n `left: auto; right: calc(100% - var(${MENU_POINT_X})); }\\n`;\n\n/**\n * Two equivalent spellings of the point fallback list, alternated by the placement's generation:\n * changing `position-try-fallbacks` clears the remembered \"last successful position option\", so a\n * re-show at a new point does not keep the previous flip (design 4.3). The first two options are\n * order-independent (only one of them can fix a given overflow).\n */\nexport const MENU_POINT_FALLBACKS = [\n \"--fsui-menu-up, --fsui-menu-left, --fsui-menu-up-left\",\n \"--fsui-menu-left, --fsui-menu-up, --fsui-menu-up-left\",\n] as const;\n"],"mappings":";;AAaA,MAAa,yBAAyB;;;;;;AAOtC,SAAgB,aAAa,MAAkB,OAA4B;CACzE,IAAI,UAAU,UAAU,OAAO;CAS/B,OAAO,GAAG,KAAK,GARE,SAAS,SAAS,SAAS,WAExC,UAAU,UACR,eACA,cACF,UAAU,UACR,gBACA;AAER;;AAKA,MAAa,eAAe;;AAE5B,MAAa,eAAe;;AAG5B,MAAa,kBAAkB;;AAE/B,MAAa,kBAAkB;;;;;;;AAQ/B,MAAa,mBACX;oEAGqE,aAAa,6EACX,aAAa,gFACV,aAAa,yCACjD,aAAa;;;;;;;AAQrD,MAAa,uBAAuB,CAClC,yDACA,uDACF"}
@@ -54,6 +54,7 @@ function usePopoverPopup(ctx, options) {
54
54
  const isLibrary = ctx.mode === "library";
55
55
  const { popupRef, triggerRef } = ctx;
56
56
  const opening = useRef(false);
57
+ const reshowing = useRef(false);
57
58
  const awaitingClose = useRef(false);
58
59
  const focusWasInside = useRef(false);
59
60
  const warnedFocus = useRef(false);
@@ -105,14 +106,30 @@ function usePopoverPopup(ctx, options) {
105
106
  if (added) target.setAttribute("autofocus", "");
106
107
  opening.current = true;
107
108
  try {
108
- const source = triggerRef.current;
109
- element.showPopover(source !== null && source.isConnected ? { source } : void 0);
109
+ const source = options.source !== void 0 ? options.source() : triggerRef.current;
110
+ element.showPopover(source != null && source.isConnected ? { source } : void 0);
110
111
  } finally {
111
112
  opening.current = false;
112
113
  if (added) target.removeAttribute("autofocus");
113
114
  }
114
115
  if (quiet && isPopoverOpen(element) && element.contains(target)) target.focus({ focusVisible: false });
115
116
  };
117
+ const reshow = () => {
118
+ const element = popupRef.current;
119
+ if (!isLibrary || element === null || !isPopoverOpen(element)) return false;
120
+ opening.current = true;
121
+ reshowing.current = true;
122
+ try {
123
+ element.hidePopover();
124
+ showPopup(element);
125
+ } finally {
126
+ opening.current = false;
127
+ reshowing.current = false;
128
+ }
129
+ awaitingClose.current = false;
130
+ focusWasInside.current = false;
131
+ return isPopoverOpen(element);
132
+ };
116
133
  const reconcileOnMount = useEffectEvent(() => {
117
134
  const element = popupRef.current;
118
135
  if (element === null) return;
@@ -150,7 +167,7 @@ function usePopoverPopup(ctx, options) {
150
167
  if (isLibrary && !opening.current) {
151
168
  event.preventDefault();
152
169
  ctx.requestOpen(event, "activate");
153
- } else ctx.nextOpenGeneration();
170
+ } else if (!reshowing.current) ctx.nextOpenGeneration();
154
171
  contentProbe.forget();
155
172
  } else {
156
173
  awaitingClose.current = true;
@@ -252,7 +269,8 @@ function usePopoverPopup(ctx, options) {
252
269
  return {
253
270
  setElement,
254
271
  visible: ctx.committed || exit.held,
255
- wrapContent
272
+ wrapContent,
273
+ reshow
256
274
  };
257
275
  }
258
276
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"use-popover-popup.mjs","names":[],"sources":["../../src/internal/use-popover-popup.ts"],"sourcesContent":["\"use client\";\nimport {\n useCallback,\n useEffect,\n useEffectEvent,\n useInsertionEffect,\n useLayoutEffect,\n useRef,\n useState,\n} from \"react\";\nimport type { ReactNode, RefObject, ViewTransitionClass } from \"react\";\nimport { useContentFocusProbe } from \"./content-focus-probe.js\";\nimport { useExitAnimation } from \"./use-exit-animation.js\";\nimport type { PopoverRootValue } from \"./use-popover-root.js\";\n\nexport interface UsePopoverPopupOptions {\n /** Public name of the part for development warnings (`\"Popover.Popup\"`). */\n readonly part: string;\n /** Element to focus on open: a ref, or a selector resolved inside the popup (design 6.1). */\n readonly initialFocus: RefObject<HTMLElement | null> | string | undefined;\n /**\n * Replaces `initialFocus` with a function called right before each library open (Menu: the\n * first or last item by open cause). `null` leaves the choice to the platform. A\n * `QuietFocusTarget` asks for focus without the focus ring (a pointer-opened menu).\n */\n readonly resolveInitialFocus?: (popup: HTMLElement) => HTMLElement | QuietFocusTarget | null;\n readonly keepMounted: boolean | undefined;\n /** The consumer's `transition`; decides how a library-driven close keeps its content (4.4). */\n readonly transition: ViewTransitionClass | undefined;\n /** Runs after the library's own `beforetoggle` handling (Menu: focus bookkeeping). */\n readonly onBeforeToggle?: (event: ToggleEvent) => void;\n /** Runs after the library's own `toggle` handling (Menu: close the stack on Tab-out). */\n readonly onToggle?: (event: ToggleEvent) => void;\n}\n\n/**\n * An initial focus target opened by a pointer: where the engine reads `focus({ focusVisible })`,\n * the library focuses it with `focusVisible: false` right after `showPopover()` instead of\n * writing `autofocus` (WebKit shows the ring for an `autofocus` target after a click; review H1).\n * Elsewhere it is an ordinary target.\n */\nexport interface QuietFocusTarget {\n readonly element: HTMLElement;\n readonly focusVisible: false;\n}\n\nexport interface PopoverPopupHook {\n /** Callback ref for the popup element; pass as `useRender`'s `ref`. */\n readonly setElement: (element: HTMLElement) => () => void;\n /**\n * Whether the children are shown: committed open, or held while the hide animates (4.4).\n * Library mode only; JS-free mode always renders them.\n */\n readonly visible: boolean;\n /**\n * Wraps the library-mode content: call it only while `visible` (`visible ? wrapContent(x) :\n * null`), or inside the `<Activity>` for `keepMounted`. When a close commit unmounts the\n * content while it holds focus (a library-driven close with `transition`, where the focused\n * element is gone before `hidePopover()` runs and the platform restores nothing), the hide\n * that follows returns focus to the Trigger (design 6.2).\n */\n readonly wrapContent: (children: ReactNode) => ReactNode;\n}\n\nconst isDev = process.env.NODE_ENV !== \"production\";\n\nfunction isPopoverOpen(element: Element): boolean {\n try {\n return element.matches(\":popover-open\");\n } catch {\n return false;\n }\n}\n\nlet focusVisibleOption: boolean | undefined;\n\n/**\n * The engine reads the `focusVisible` member of `focus()`'s options (Chromium 145, Firefox 104,\n * Safari 18.4). Detected once through a getter: the bindings convert the dictionary before the\n * method runs, so a detached element is enough and nothing is focused.\n */\nfunction supportsFocusVisibleOption(doc: Document): boolean {\n if (focusVisibleOption === undefined) {\n let read = false;\n try {\n doc.createElement(\"div\").focus({\n get focusVisible() {\n read = true;\n return false;\n },\n } as FocusOptions);\n } catch {\n read = false;\n }\n focusVisibleOption = read;\n }\n return focusVisibleOption;\n}\n\n/** Resolves `initialFocus` against the popup (a ref, or a selector inside the popup). */\nfunction resolveInitialFocus(\n popup: HTMLElement,\n initialFocus: RefObject<HTMLElement | null> | string,\n): HTMLElement | null {\n if (typeof initialFocus === \"string\") return popup.querySelector<HTMLElement>(initialFocus);\n return initialFocus.current;\n}\n\n/** Whether `element` sits inside a popover that is not shown (an outer popup that closed). */\nfunction insideHiddenPopover(element: Element): boolean {\n for (let node = element.parentElement; node !== null; node = node.parentElement) {\n if (node.hasAttribute(\"popover\") && !isPopoverOpen(node)) return true;\n }\n return false;\n}\n\n/**\n * Everything in a popover-like Popup except rendering (design/components/popover.md 14.2): mount\n * reconciliation, the apply effect, the native `beforetoggle` / `toggle` / `command` listeners,\n * close intents, the focus-return fallback, the JS-free `aria-expanded` sync, the deferred\n * unmount while a hide animates, and `initialFocus`. Shared by Popover and Menu.\n */\nexport function usePopoverPopup(\n ctx: PopoverRootValue,\n options: UsePopoverPopupOptions,\n): PopoverPopupHook {\n const { part, initialFocus, transition, onBeforeToggle: onBeforeToggleOption } = options;\n const onToggleOption = options.onToggle;\n const isLibrary = ctx.mode === \"library\";\n const { popupRef, triggerRef } = ctx;\n\n // The opening `beforetoggle` lets this one through: the library's own `showPopover()`.\n const opening = useRef(false);\n // Set by the synchronous `beforetoggle` of a close, cleared by `toggle` a task later: the\n // popup is hidden but its `toggle` has not arrived, so the apply effect must not re-open it\n // from a commit that lands in between (design 2.5).\n const awaitingClose = useRef(false);\n // Whether focus was inside the popup when its last close began (design 6.2 rule 1).\n const focusWasInside = useRef(false);\n const warnedFocus = useRef(false);\n\n // --- `held`: children stay mounted while the hide animates (design 4.4) -------------------\n\n const exit = useExitAnimation();\n\n // A library-driven close commits `false` first and hides in the layout effect, so without\n // `transition` the children are held through the close commit (\"previous value in state\").\n // With `transition` the close commit unmounts them inside the view transition, whose old\n // snapshot carries the content out.\n const [prevCommitted, setPrevCommitted] = useState(ctx.committed);\n if (prevCommitted !== ctx.committed) {\n setPrevCommitted(ctx.committed);\n if (isLibrary && prevCommitted && transition === undefined) exit.holdDuringRender();\n }\n\n // Whether the content unmounted while holding focus since the popup was last shown (H1).\n const contentProbe = useContentFocusProbe(popupRef);\n const { wrapContent } = contentProbe;\n\n const syncExpanded = (element: HTMLElement): void => {\n // JS-free mode only (design 9): read the DOM, not `newState`, so a stale task cannot lie.\n triggerRef.current?.setAttribute(\"aria-expanded\", String(isPopoverOpen(element)));\n };\n\n /**\n * Design 6.2 rule 2, for a close that began with focus inside: when focus is now on `<body>`\n * or still inside the hidden popup, focus the Trigger (connected, not inside a hidden\n * popover), else blur what holds focus.\n */\n const returnFocus = (element: HTMLElement): void => {\n const doc = element.ownerDocument;\n const active = doc.activeElement;\n if (active !== null && active !== doc.body && !element.contains(active)) return;\n const trigger = triggerRef.current;\n if (trigger !== null && trigger.isConnected && !insideHiddenPopover(trigger)) {\n trigger.focus();\n } else if (active instanceof HTMLElement && active !== doc.body) {\n active.blur();\n }\n };\n\n // --- Opening ------------------------------------------------------------------------------\n\n const resolveFocusTarget = (element: HTMLElement): HTMLElement | QuietFocusTarget | null => {\n if (options.resolveInitialFocus !== undefined) {\n const resolved = options.resolveInitialFocus(element);\n if (resolved === null) return null;\n const target = resolved instanceof HTMLElement ? resolved : resolved.element;\n return element.contains(target) ? resolved : null;\n }\n if (initialFocus === undefined) return null;\n const target = resolveInitialFocus(element, initialFocus);\n if (target !== null && element.contains(target)) return target;\n if (isDev && !warnedFocus.current) {\n warnedFocus.current = true;\n console.error(\n target === null\n ? `${part}: \\`initialFocus\\` resolved to no element; the platform picks the focus target.`\n : `${part}: \\`initialFocus\\` is outside the popup; the platform picks the focus target.`,\n );\n }\n return null;\n };\n\n const showPopup = (element: HTMLElement): void => {\n const resolved = resolveFocusTarget(element);\n const target =\n resolved === null || resolved instanceof HTMLElement ? resolved : resolved.element;\n // A pointer open asks for no focus ring: `focus({ focusVisible: false })` after the show,\n // where the engine reads that option (QuietFocusTarget).\n const quiet =\n target !== null &&\n !(resolved instanceof HTMLElement) &&\n supportsFocusVisibleOption(element.ownerDocument);\n // Otherwise the popover focusing steps honour `autofocus` (and run the dialog focusing steps\n // for a <dialog>, in every engine); React drops `autoFocus` on client-rendered content.\n // Never `.focus()` (6.1) except for the quiet target above.\n const added = target !== null && !quiet && !target.hasAttribute(\"autofocus\");\n if (added) target.setAttribute(\"autofocus\", \"\");\n opening.current = true;\n try {\n const source = triggerRef.current;\n element.showPopover(source !== null && source.isConnected ? { source } : undefined);\n } finally {\n opening.current = false;\n if (added) target.removeAttribute(\"autofocus\");\n }\n if (quiet && isPopoverOpen(element) && element.contains(target)) {\n target.focus({ focusVisible: false } as FocusOptions);\n }\n };\n\n // Mount reconciliation (design 2.4): a click before hydration opened the popover natively.\n // Runs before the apply effect; the dispatch makes the apply effect wait for it.\n const reconcileOnMount = useEffectEvent(() => {\n const element = popupRef.current;\n if (element === null) return;\n if (!isLibrary) {\n syncExpanded(element);\n return;\n }\n if (isPopoverOpen(element)) ctx.requestOpen(undefined, \"platform\");\n });\n useLayoutEffect(() => {\n reconcileOnMount();\n }, []);\n\n // The apply effect (design 2.5): after every commit that re-renders the Popup, once nothing is\n // in flight, the DOM follows committed state. Since the Root's value is memoised (#48) an\n // unrelated Root render does not re-render the Popup, so this does not run for it; every path\n // that makes the DOM diverge from state goes through a dispatch, whose `pending` flips re-render\n // the Popup (review of #48, finding 4). Do not rely on an unrelated render to re-apply state.\n // One effect covers the normal open, `defaultOpen`, a controlled open\n // or close, a library-driven close after `openChangeAction(false)` settled, the re-open of a\n // refused dismissal, and the close after a refused or failed pre-hydration open.\n useLayoutEffect(() => {\n if (!isLibrary) return;\n const element = popupRef.current;\n if (element === null || !element.isConnected) return;\n if (!ctx.pending && ctx.latestInFlight() === null) {\n const shown = isPopoverOpen(element);\n if (ctx.committed && !shown) {\n if (!awaitingClose.current) showPopup(element);\n } else if (!ctx.committed && shown) {\n // An echo: the `toggle` listener finds `false` intended and skips the dispatch.\n ctx.recordCloseIntent(undefined);\n element.hidePopover();\n // The close commit may have unmounted the focused content before the hide (H1): the\n // platform restores focus only from inside the popover, so do it now.\n if (focusWasInside.current) returnFocus(element);\n }\n }\n // Content that unmounted while the popup stays open (not a close): forget it.\n if (ctx.committed && isPopoverOpen(element)) contentProbe.forget();\n // A hold the close commit set (or a dismissal's notification commit) that no hide animation\n // is going to release: the popup is already hidden and its `toggle` has been handled.\n if (\n exit.held &&\n !ctx.committed &&\n !exit.waiting() &&\n !awaitingClose.current &&\n !isPopoverOpen(element)\n ) {\n exit.release();\n }\n });\n\n // --- Native listeners (conventions 7.4; filtered on target: React 19.3.1 emulates bubbling\n // of `toggle`, and a nested popover's events reach this element through the real DOM too) ---\n\n const onBeforeToggle = (event: Event): void => {\n const element = popupRef.current;\n if (element === null || event.target !== element) return;\n const toggle = event as ToggleEvent;\n if (toggle.newState === \"open\") {\n // Every open the library did not start itself is routed through the Root (design 2.2):\n // a foreign `show-popover` / `toggle-popover` invoker, a script `showPopover()`.\n if (isLibrary && !opening.current) {\n event.preventDefault();\n ctx.requestOpen(event, \"activate\");\n } else {\n // A Close action that resolves after this open must not close it (design 3.4).\n ctx.nextOpenGeneration();\n }\n contentProbe.forget();\n } else {\n awaitingClose.current = true;\n exit.recordBaseline(element);\n const active = element.ownerDocument.activeElement;\n focusWasInside.current =\n (active !== null && element.contains(active)) || contentProbe.hadFocus();\n contentProbe.forget();\n }\n onBeforeToggleOption?.(toggle);\n };\n\n const onToggle = (event: Event): void => {\n const element = popupRef.current;\n if (element === null || event.target !== element) return;\n awaitingClose.current = false;\n if (!isLibrary) syncExpanded(element);\n\n if (!isPopoverOpen(element)) {\n // Focus return for what the platform leaves out (design 6.2): a nested popover restores\n // nothing, and an opener that is gone leaves focus inside the hidden popup.\n const wasInside = focusWasInside.current;\n focusWasInside.current = false;\n if (wasInside) returnFocus(element);\n\n const intent = ctx.takeCloseIntent();\n if (isLibrary) {\n // `false` already intended: the library's own hide (or a dismissal during a pending\n // part-initiated close). With `transition` such a close needs no hold (4.4).\n const echo = !ctx.intended();\n if (!echo || transition === undefined) {\n const animations = exit.exitAnimations(element);\n if (animations.length > 0) {\n // Before the notification, so its commit keeps the children (urgent, internal).\n exit.holdUntil(animations);\n } else {\n // Nothing animates (Firefox, or no CSS exit): release a hold now; the commit is\n // invisible, the popup is already `display: none`.\n exit.release();\n }\n }\n ctx.notifyClosed(intent?.event ?? event, intent?.reason ?? \"dismiss\");\n }\n }\n onToggleOption?.(event as ToggleEvent);\n };\n\n const onCommand = (event: Event): void => {\n const element = popupRef.current;\n if (element === null || event.target !== element || event.defaultPrevented) return;\n const command = String((event as Event & { command?: string }).command ?? \"\").toLowerCase();\n // A foreign invoker closing the popup (design 2.3). Opens are caught at `beforetoggle`.\n if (command === \"hide-popover\" || (command === \"toggle-popover\" && isPopoverOpen(element))) {\n ctx.recordCloseIntent({ reason: \"activate\", event });\n }\n };\n\n // The listeners the callback ref attaches call the latest render's handlers. An insertion\n // effect publishes them before refs attach and before any layout effect (whose\n // `hidePopover()` dispatches `beforetoggle` synchronously).\n const handlers = useRef({ onBeforeToggle, onToggle, onCommand });\n useInsertionEffect(() => {\n handlers.current = { onBeforeToggle, onToggle, onCommand };\n });\n\n // --- Element ------------------------------------------------------------------------------\n\n // The listeners attach in the callback ref, so they are in place from the commit that mounts\n // the element (no passive-effect window, no extra render). JS-free mode: there is no \"right\n // before showPopover()\" (the browser runs the command), so the `autofocus` attribute stays on\n // the `initialFocus` target while it is mounted (as Dialog).\n const setElement = useCallback(\n (element: HTMLElement) => {\n if (isDev && element.tagName !== \"DIALOG\" && element.tagName !== \"DIV\") {\n console.error(\n `${part}: expected to render a <dialog> or a <div> but got ` +\n `<${element.tagName.toLowerCase()}>.`,\n );\n }\n popupRef.current = element;\n const listeners: [string, EventListener][] = [\n [\"beforetoggle\", (event) => handlers.current.onBeforeToggle(event)],\n [\"toggle\", (event) => handlers.current.onToggle(event)],\n [\"command\", (event) => handlers.current.onCommand(event)],\n ];\n for (const [type, listener] of listeners) element.addEventListener(type, listener);\n\n let autofocused: HTMLElement | null = null;\n if (!isLibrary && initialFocus !== undefined) {\n const target = resolveInitialFocus(element, initialFocus);\n if (target !== null && element.contains(target) && !target.hasAttribute(\"autofocus\")) {\n target.setAttribute(\"autofocus\", \"\");\n autofocused = target;\n }\n }\n\n return () => {\n for (const [type, listener] of listeners) element.removeEventListener(type, listener);\n autofocused?.removeAttribute(\"autofocus\");\n if (popupRef.current === element) popupRef.current = null;\n };\n },\n [part, popupRef, isLibrary, initialFocus],\n );\n\n // Development check (design 9): `role=\"dialog\"` needs a name once the popup is (or can be)\n // shown.\n const shown = !isLibrary || ctx.committed;\n const warnedName = useRef(false);\n useEffect(() => {\n const element = popupRef.current;\n if (!isDev || element === null || !shown || warnedName.current) return;\n if (element.hasAttribute(\"aria-label\")) return;\n const labelledBy = (element.getAttribute(\"aria-labelledby\") ?? \"\").split(/\\s+/);\n const doc = element.ownerDocument;\n if (labelledBy.some((id) => id !== \"\" && doc.getElementById(id) !== null)) return;\n const role = element.getAttribute(\"role\");\n if (role !== \"dialog\" && role !== \"alertdialog\" && role !== \"menu\") return;\n warnedName.current = true;\n console.error(\n `${part}: the popup has no accessible name. Render a Title, or give the Popup an ` +\n \"`aria-label`.\",\n );\n }, [popupRef, shown, part]);\n\n return { setElement, visible: ctx.committed || exit.held, wrapContent };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"use-popover-popup.mjs","names":[],"sources":["../../src/internal/use-popover-popup.ts"],"sourcesContent":["\"use client\";\nimport {\n useCallback,\n useEffect,\n useEffectEvent,\n useInsertionEffect,\n useLayoutEffect,\n useRef,\n useState,\n} from \"react\";\nimport type { ReactNode, RefObject, ViewTransitionClass } from \"react\";\nimport { useContentFocusProbe } from \"./content-focus-probe.js\";\nimport { useExitAnimation } from \"./use-exit-animation.js\";\nimport type { PopoverRootValue } from \"./use-popover-root.js\";\n\nexport interface UsePopoverPopupOptions {\n /** Public name of the part for development warnings (`\"Popover.Popup\"`). */\n readonly part: string;\n /** Element to focus on open: a ref, or a selector resolved inside the popup (design 6.1). */\n readonly initialFocus: RefObject<HTMLElement | null> | string | undefined;\n /**\n * Replaces `initialFocus` with a function called right before each library open (Menu: the\n * first or last item by open cause). `null` leaves the choice to the platform. A\n * `QuietFocusTarget` asks for focus without the focus ring (a pointer-opened menu).\n */\n readonly resolveInitialFocus?: (popup: HTMLElement) => HTMLElement | QuietFocusTarget | null;\n readonly keepMounted: boolean | undefined;\n /** The consumer's `transition`; decides how a library-driven close keeps its content (4.4). */\n readonly transition: ViewTransitionClass | undefined;\n /** Runs after the library's own `beforetoggle` handling (Menu: focus bookkeeping). */\n readonly onBeforeToggle?: (event: ToggleEvent) => void;\n /** Runs after the library's own `toggle` handling (Menu: the outside right-click listeners). */\n readonly onToggle?: (event: ToggleEvent) => void;\n /**\n * The `source` of each library `showPopover({ source })`, read right before the call. Default:\n * the registered Trigger. Menu passes the ContextTrigger region for a point open\n * (design/components/context-menu.md 14).\n */\n readonly source?: () => HTMLElement | null;\n}\n\n/**\n * An initial focus target opened by a pointer: where the engine reads `focus({ focusVisible })`,\n * the library focuses it with `focusVisible: false` right after `showPopover()` instead of\n * writing `autofocus` (WebKit shows the ring for an `autofocus` target after a click; review H1).\n * Elsewhere it is an ordinary target.\n */\nexport interface QuietFocusTarget {\n readonly element: HTMLElement;\n readonly focusVisible: false;\n}\n\nexport interface PopoverPopupHook {\n /** Callback ref for the popup element; pass as `useRender`'s `ref`. */\n readonly setElement: (element: HTMLElement) => () => void;\n /**\n * Whether the children are shown: committed open, or held while the hide animates (4.4).\n * Library mode only; JS-free mode always renders them.\n */\n readonly visible: boolean;\n /**\n * Wraps the library-mode content: call it only while `visible` (`visible ? wrapContent(x) :\n * null`), or inside the `<Activity>` for `keepMounted`. When a close commit unmounts the\n * content while it holds focus (a library-driven close with `transition`, where the focused\n * element is gone before `hidePopover()` runs and the platform restores nothing), the hide\n * that follows returns focus to the Trigger (design 6.2).\n */\n readonly wrapContent: (children: ReactNode) => ReactNode;\n /**\n * Re-shows a shown popup in place (`hidePopover()` + `showPopover({ source })` under the\n * opening flag, initial focus resolved again): Menu's move to a new point\n * (design/components/context-menu.md 4.3). The platform coalesces the two toggles into one\n * `toggle` (`open` to `open`), so nothing is dispatched. No-op (`false`) when the popup is not\n * shown.\n */\n readonly reshow: () => boolean;\n}\n\nconst isDev = process.env.NODE_ENV !== \"production\";\n\nfunction isPopoverOpen(element: Element): boolean {\n try {\n return element.matches(\":popover-open\");\n } catch {\n return false;\n }\n}\n\nlet focusVisibleOption: boolean | undefined;\n\n/**\n * The engine reads the `focusVisible` member of `focus()`'s options (Chromium 145, Firefox 104,\n * Safari 18.4). Detected once through a getter: the bindings convert the dictionary before the\n * method runs, so a detached element is enough and nothing is focused.\n */\nfunction supportsFocusVisibleOption(doc: Document): boolean {\n if (focusVisibleOption === undefined) {\n let read = false;\n try {\n doc.createElement(\"div\").focus({\n get focusVisible() {\n read = true;\n return false;\n },\n } as FocusOptions);\n } catch {\n read = false;\n }\n focusVisibleOption = read;\n }\n return focusVisibleOption;\n}\n\n/** Resolves `initialFocus` against the popup (a ref, or a selector inside the popup). */\nfunction resolveInitialFocus(\n popup: HTMLElement,\n initialFocus: RefObject<HTMLElement | null> | string,\n): HTMLElement | null {\n if (typeof initialFocus === \"string\") return popup.querySelector<HTMLElement>(initialFocus);\n return initialFocus.current;\n}\n\n/** Whether `element` sits inside a popover that is not shown (an outer popup that closed). */\nfunction insideHiddenPopover(element: Element): boolean {\n for (let node = element.parentElement; node !== null; node = node.parentElement) {\n if (node.hasAttribute(\"popover\") && !isPopoverOpen(node)) return true;\n }\n return false;\n}\n\n/**\n * Everything in a popover-like Popup except rendering (design/components/popover.md 14.2): mount\n * reconciliation, the apply effect, the native `beforetoggle` / `toggle` / `command` listeners,\n * close intents, the focus-return fallback, the JS-free `aria-expanded` sync, the deferred\n * unmount while a hide animates, and `initialFocus`. Shared by Popover and Menu.\n */\nexport function usePopoverPopup(\n ctx: PopoverRootValue,\n options: UsePopoverPopupOptions,\n): PopoverPopupHook {\n const { part, initialFocus, transition, onBeforeToggle: onBeforeToggleOption } = options;\n const onToggleOption = options.onToggle;\n const isLibrary = ctx.mode === \"library\";\n const { popupRef, triggerRef } = ctx;\n\n // The opening `beforetoggle` lets this one through: the library's own `showPopover()`.\n const opening = useRef(false);\n // Set across `reshow()`: the show is not a new open.\n const reshowing = useRef(false);\n // Set by the synchronous `beforetoggle` of a close, cleared by `toggle` a task later: the\n // popup is hidden but its `toggle` has not arrived, so the apply effect must not re-open it\n // from a commit that lands in between (design 2.5).\n const awaitingClose = useRef(false);\n // Whether focus was inside the popup when its last close began (design 6.2 rule 1).\n const focusWasInside = useRef(false);\n const warnedFocus = useRef(false);\n\n // --- `held`: children stay mounted while the hide animates (design 4.4) -------------------\n\n const exit = useExitAnimation();\n\n // A library-driven close commits `false` first and hides in the layout effect, so without\n // `transition` the children are held through the close commit (\"previous value in state\").\n // With `transition` the close commit unmounts them inside the view transition, whose old\n // snapshot carries the content out.\n const [prevCommitted, setPrevCommitted] = useState(ctx.committed);\n if (prevCommitted !== ctx.committed) {\n setPrevCommitted(ctx.committed);\n if (isLibrary && prevCommitted && transition === undefined) exit.holdDuringRender();\n }\n\n // Whether the content unmounted while holding focus since the popup was last shown (H1).\n const contentProbe = useContentFocusProbe(popupRef);\n const { wrapContent } = contentProbe;\n\n const syncExpanded = (element: HTMLElement): void => {\n // JS-free mode only (design 9): read the DOM, not `newState`, so a stale task cannot lie.\n triggerRef.current?.setAttribute(\"aria-expanded\", String(isPopoverOpen(element)));\n };\n\n /**\n * Design 6.2 rule 2, for a close that began with focus inside: when focus is now on `<body>`\n * or still inside the hidden popup, focus the Trigger (connected, not inside a hidden\n * popover), else blur what holds focus.\n */\n const returnFocus = (element: HTMLElement): void => {\n const doc = element.ownerDocument;\n const active = doc.activeElement;\n if (active !== null && active !== doc.body && !element.contains(active)) return;\n const trigger = triggerRef.current;\n if (trigger !== null && trigger.isConnected && !insideHiddenPopover(trigger)) {\n trigger.focus();\n } else if (active instanceof HTMLElement && active !== doc.body) {\n active.blur();\n }\n };\n\n // --- Opening ------------------------------------------------------------------------------\n\n const resolveFocusTarget = (element: HTMLElement): HTMLElement | QuietFocusTarget | null => {\n if (options.resolveInitialFocus !== undefined) {\n const resolved = options.resolveInitialFocus(element);\n if (resolved === null) return null;\n const target = resolved instanceof HTMLElement ? resolved : resolved.element;\n return element.contains(target) ? resolved : null;\n }\n if (initialFocus === undefined) return null;\n const target = resolveInitialFocus(element, initialFocus);\n if (target !== null && element.contains(target)) return target;\n if (isDev && !warnedFocus.current) {\n warnedFocus.current = true;\n console.error(\n target === null\n ? `${part}: \\`initialFocus\\` resolved to no element; the platform picks the focus target.`\n : `${part}: \\`initialFocus\\` is outside the popup; the platform picks the focus target.`,\n );\n }\n return null;\n };\n\n const showPopup = (element: HTMLElement): void => {\n const resolved = resolveFocusTarget(element);\n const target =\n resolved === null || resolved instanceof HTMLElement ? resolved : resolved.element;\n // A pointer open asks for no focus ring: `focus({ focusVisible: false })` after the show,\n // where the engine reads that option (QuietFocusTarget).\n const quiet =\n target !== null &&\n !(resolved instanceof HTMLElement) &&\n supportsFocusVisibleOption(element.ownerDocument);\n // Otherwise the popover focusing steps honour `autofocus` (and run the dialog focusing steps\n // for a <dialog>, in every engine); React drops `autoFocus` on client-rendered content.\n // Never `.focus()` (6.1) except for the quiet target above.\n const added = target !== null && !quiet && !target.hasAttribute(\"autofocus\");\n if (added) target.setAttribute(\"autofocus\", \"\");\n opening.current = true;\n try {\n const source = options.source !== undefined ? options.source() : triggerRef.current;\n element.showPopover(source != null && source.isConnected ? { source } : undefined);\n } finally {\n opening.current = false;\n if (added) target.removeAttribute(\"autofocus\");\n }\n if (quiet && isPopoverOpen(element) && element.contains(target)) {\n target.focus({ focusVisible: false } as FocusOptions);\n }\n };\n\n const reshow = (): boolean => {\n const element = popupRef.current;\n if (!isLibrary || element === null || !isPopoverOpen(element)) return false;\n // The hide half's `beforetoggle` records `awaitingClose` / `focusWasInside`; the coalesced\n // `toggle` finds the popup open again, so it clears `awaitingClose`, returns no focus and\n // notifies nothing. The opening flag covers both halves (conventions 3.5, the move).\n opening.current = true;\n reshowing.current = true;\n try {\n element.hidePopover();\n showPopup(element);\n } finally {\n opening.current = false;\n reshowing.current = false;\n }\n // Whether or not the engine fires the coalesced `toggle`, the close bookkeeping of the hide\n // half is void: the popup is open again.\n awaitingClose.current = false;\n focusWasInside.current = false;\n return isPopoverOpen(element);\n };\n\n // Mount reconciliation (design 2.4): a click before hydration opened the popover natively.\n // Runs before the apply effect; the dispatch makes the apply effect wait for it.\n const reconcileOnMount = useEffectEvent(() => {\n const element = popupRef.current;\n if (element === null) return;\n if (!isLibrary) {\n syncExpanded(element);\n return;\n }\n if (isPopoverOpen(element)) ctx.requestOpen(undefined, \"platform\");\n });\n useLayoutEffect(() => {\n reconcileOnMount();\n }, []);\n\n // The apply effect (design 2.5): after every commit that re-renders the Popup, once nothing is\n // in flight, the DOM follows committed state. Since the Root's value is memoised (#48) an\n // unrelated Root render does not re-render the Popup, so this does not run for it; every path\n // that makes the DOM diverge from state goes through a dispatch, whose `pending` flips re-render\n // the Popup (review of #48, finding 4). Do not rely on an unrelated render to re-apply state.\n // One effect covers the normal open, `defaultOpen`, a controlled open\n // or close, a library-driven close after `openChangeAction(false)` settled, the re-open of a\n // refused dismissal, and the close after a refused or failed pre-hydration open.\n useLayoutEffect(() => {\n if (!isLibrary) return;\n const element = popupRef.current;\n if (element === null || !element.isConnected) return;\n if (!ctx.pending && ctx.latestInFlight() === null) {\n const shown = isPopoverOpen(element);\n if (ctx.committed && !shown) {\n if (!awaitingClose.current) showPopup(element);\n } else if (!ctx.committed && shown) {\n // An echo: the `toggle` listener finds `false` intended and skips the dispatch.\n ctx.recordCloseIntent(undefined);\n element.hidePopover();\n // The close commit may have unmounted the focused content before the hide (H1): the\n // platform restores focus only from inside the popover, so do it now.\n if (focusWasInside.current) returnFocus(element);\n }\n }\n // Content that unmounted while the popup stays open (not a close): forget it.\n if (ctx.committed && isPopoverOpen(element)) contentProbe.forget();\n // A hold the close commit set (or a dismissal's notification commit) that no hide animation\n // is going to release: the popup is already hidden and its `toggle` has been handled.\n if (\n exit.held &&\n !ctx.committed &&\n !exit.waiting() &&\n !awaitingClose.current &&\n !isPopoverOpen(element)\n ) {\n exit.release();\n }\n });\n\n // --- Native listeners (conventions 7.4; filtered on target: React 19.3.1 emulates bubbling\n // of `toggle`, and a nested popover's events reach this element through the real DOM too) ---\n\n const onBeforeToggle = (event: Event): void => {\n const element = popupRef.current;\n if (element === null || event.target !== element) return;\n const toggle = event as ToggleEvent;\n if (toggle.newState === \"open\") {\n // Every open the library did not start itself is routed through the Root (design 2.2):\n // a foreign `show-popover` / `toggle-popover` invoker, a script `showPopover()`.\n if (isLibrary && !opening.current) {\n event.preventDefault();\n ctx.requestOpen(event, \"activate\");\n } else if (!reshowing.current) {\n // A Close action that resolves after this open must not close it (design 3.4). A re-show\n // (Menu's move) is the same open: an item action pending across it still closes.\n ctx.nextOpenGeneration();\n }\n contentProbe.forget();\n } else {\n awaitingClose.current = true;\n exit.recordBaseline(element);\n const active = element.ownerDocument.activeElement;\n focusWasInside.current =\n (active !== null && element.contains(active)) || contentProbe.hadFocus();\n contentProbe.forget();\n }\n onBeforeToggleOption?.(toggle);\n };\n\n const onToggle = (event: Event): void => {\n const element = popupRef.current;\n if (element === null || event.target !== element) return;\n awaitingClose.current = false;\n if (!isLibrary) syncExpanded(element);\n\n if (!isPopoverOpen(element)) {\n // Focus return for what the platform leaves out (design 6.2): a nested popover restores\n // nothing, and an opener that is gone leaves focus inside the hidden popup.\n const wasInside = focusWasInside.current;\n focusWasInside.current = false;\n if (wasInside) returnFocus(element);\n\n const intent = ctx.takeCloseIntent();\n if (isLibrary) {\n // `false` already intended: the library's own hide (or a dismissal during a pending\n // part-initiated close). With `transition` such a close needs no hold (4.4).\n const echo = !ctx.intended();\n if (!echo || transition === undefined) {\n const animations = exit.exitAnimations(element);\n if (animations.length > 0) {\n // Before the notification, so its commit keeps the children (urgent, internal).\n exit.holdUntil(animations);\n } else {\n // Nothing animates (Firefox, or no CSS exit): release a hold now; the commit is\n // invisible, the popup is already `display: none`.\n exit.release();\n }\n }\n ctx.notifyClosed(intent?.event ?? event, intent?.reason ?? \"dismiss\");\n }\n }\n onToggleOption?.(event as ToggleEvent);\n };\n\n const onCommand = (event: Event): void => {\n const element = popupRef.current;\n if (element === null || event.target !== element || event.defaultPrevented) return;\n const command = String((event as Event & { command?: string }).command ?? \"\").toLowerCase();\n // A foreign invoker closing the popup (design 2.3). Opens are caught at `beforetoggle`.\n if (command === \"hide-popover\" || (command === \"toggle-popover\" && isPopoverOpen(element))) {\n ctx.recordCloseIntent({ reason: \"activate\", event });\n }\n };\n\n // The listeners the callback ref attaches call the latest render's handlers. An insertion\n // effect publishes them before refs attach and before any layout effect (whose\n // `hidePopover()` dispatches `beforetoggle` synchronously).\n const handlers = useRef({ onBeforeToggle, onToggle, onCommand });\n useInsertionEffect(() => {\n handlers.current = { onBeforeToggle, onToggle, onCommand };\n });\n\n // --- Element ------------------------------------------------------------------------------\n\n // The listeners attach in the callback ref, so they are in place from the commit that mounts\n // the element (no passive-effect window, no extra render). JS-free mode: there is no \"right\n // before showPopover()\" (the browser runs the command), so the `autofocus` attribute stays on\n // the `initialFocus` target while it is mounted (as Dialog).\n const setElement = useCallback(\n (element: HTMLElement) => {\n if (isDev && element.tagName !== \"DIALOG\" && element.tagName !== \"DIV\") {\n console.error(\n `${part}: expected to render a <dialog> or a <div> but got ` +\n `<${element.tagName.toLowerCase()}>.`,\n );\n }\n popupRef.current = element;\n const listeners: [string, EventListener][] = [\n [\"beforetoggle\", (event) => handlers.current.onBeforeToggle(event)],\n [\"toggle\", (event) => handlers.current.onToggle(event)],\n [\"command\", (event) => handlers.current.onCommand(event)],\n ];\n for (const [type, listener] of listeners) element.addEventListener(type, listener);\n\n let autofocused: HTMLElement | null = null;\n if (!isLibrary && initialFocus !== undefined) {\n const target = resolveInitialFocus(element, initialFocus);\n if (target !== null && element.contains(target) && !target.hasAttribute(\"autofocus\")) {\n target.setAttribute(\"autofocus\", \"\");\n autofocused = target;\n }\n }\n\n return () => {\n for (const [type, listener] of listeners) element.removeEventListener(type, listener);\n autofocused?.removeAttribute(\"autofocus\");\n if (popupRef.current === element) popupRef.current = null;\n };\n },\n [part, popupRef, isLibrary, initialFocus],\n );\n\n // Development check (design 9): `role=\"dialog\"` needs a name once the popup is (or can be)\n // shown.\n const shown = !isLibrary || ctx.committed;\n const warnedName = useRef(false);\n useEffect(() => {\n const element = popupRef.current;\n if (!isDev || element === null || !shown || warnedName.current) return;\n if (element.hasAttribute(\"aria-label\")) return;\n const labelledBy = (element.getAttribute(\"aria-labelledby\") ?? \"\").split(/\\s+/);\n const doc = element.ownerDocument;\n if (labelledBy.some((id) => id !== \"\" && doc.getElementById(id) !== null)) return;\n const role = element.getAttribute(\"role\");\n if (role !== \"dialog\" && role !== \"alertdialog\" && role !== \"menu\") return;\n warnedName.current = true;\n console.error(\n `${part}: the popup has no accessible name. Render a Title, or give the Popup an ` +\n \"`aria-label`.\",\n );\n }, [popupRef, shown, part]);\n\n return { setElement, visible: ctx.committed || exit.held, wrapContent, reshow };\n}\n"],"mappings":"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"}
@@ -1,6 +1,7 @@
1
1
  import "../internal/platform-attributes.mjs";
2
2
  import { MenuItem, MenuItemDetails, MenuItemProps, MenuItemState } from "./menu-item.mjs";
3
3
  import { MenuCheckboxItem, MenuCheckboxItemProps, MenuCheckboxItemState } from "./menu-checkbox-item.mjs";
4
+ import { MenuContextTrigger, MenuContextTriggerProps, MenuContextTriggerState } from "./menu-context-trigger.mjs";
4
5
  import { MenuGroup, MenuGroupLabel, MenuGroupLabelProps, MenuGroupLabelState, MenuGroupProps, MenuGroupState, MenuSeparator, MenuSeparatorProps, MenuSeparatorState } from "./menu-group.mjs";
5
6
  import { MenuAlign, MenuPopup, MenuPopupProps, MenuPopupState, MenuSide } from "./menu-popup.mjs";
6
7
  import { MenuRadioGroup, MenuRadioGroupProps, MenuRadioGroupState } from "./menu-radio-group.mjs";
@@ -15,6 +16,8 @@ import { MenuSubmenuTrigger, MenuSubmenuTriggerProps, MenuSubmenuTriggerState }
15
16
  * `CheckboxItem`, `RadioGroup` > `RadioItem`, `Group` > `GroupLabel`, `Separator`, and submenus
16
17
  * (a `Root` inside a `Popup`, opened by its `SubmenuTrigger`). An item's `action` runs in its own
17
18
  * transition with `data-pending` on the item, and the whole menu closes when it settles.
19
+ * `ContextTrigger` is a region whose right-click (or Shift+F10, or a touch long-press) opens the
20
+ * same `Popup` at the pointer; a Root may hold a `Trigger` and a `ContextTrigger`.
18
21
  */
19
22
  export declare const Menu: {
20
23
  readonly Root: typeof MenuRoot;
@@ -28,7 +31,8 @@ export declare const Menu: {
28
31
  readonly GroupLabel: typeof MenuGroupLabel;
29
32
  readonly Separator: typeof MenuSeparator;
30
33
  readonly SubmenuTrigger: typeof MenuSubmenuTrigger;
34
+ readonly ContextTrigger: typeof MenuContextTrigger;
31
35
  };
32
36
  //#endregion
33
- export type { MenuAlign, MenuCheckboxItemProps, MenuCheckboxItemState, MenuGroupLabelProps, MenuGroupLabelState, MenuGroupProps, MenuGroupState, MenuItemDetails, MenuItemProps, MenuItemState, MenuOpenChangeDetails, MenuPopupProps, MenuPopupState, MenuRadioGroupProps, MenuRadioGroupState, MenuRadioItemProps, MenuRadioItemState, MenuRootProps, MenuRootState, MenuSeparatorProps, MenuSeparatorState, MenuSide, MenuSubmenuTriggerProps, MenuSubmenuTriggerState, MenuTriggerProps, MenuTriggerState };
37
+ export type { MenuAlign, MenuCheckboxItemProps, MenuCheckboxItemState, MenuContextTriggerProps, MenuContextTriggerState, MenuGroupLabelProps, MenuGroupLabelState, MenuGroupProps, MenuGroupState, MenuItemDetails, MenuItemProps, MenuItemState, MenuOpenChangeDetails, MenuPopupProps, MenuPopupState, MenuRadioGroupProps, MenuRadioGroupState, MenuRadioItemProps, MenuRadioItemState, MenuRootProps, MenuRootState, MenuSeparatorProps, MenuSeparatorState, MenuSide, MenuSubmenuTriggerProps, MenuSubmenuTriggerState, MenuTriggerProps, MenuTriggerState };
34
38
  //# sourceMappingURL=index.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/menu/index.ts"],"mappings":";;;;;;;;;;;;;;;;;;qBAoBa;WACX,aAAI;WACJ,gBAAO;WACP,cAAK;WACL,aAAI;WACJ,qBAAY;WACZ,mBAAU;WACV,kBAAS;WACT,cAAK;WACL,mBAAU;WACV,kBAAS;WACT,uBAAc"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/menu/index.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;qBAuBa;WACX,aAAI;WACJ,gBAAO;WACP,cAAK;WACL,aAAI;WACJ,qBAAY;WACZ,mBAAU;WACV,kBAAS;WACT,cAAK;WACL,mBAAU;WACV,kBAAS;WACT,uBAAc;WACd,uBAAc"}
@@ -1,4 +1,5 @@
1
1
  import { MenuCheckboxItem } from "./menu-checkbox-item.mjs";
2
+ import { MenuContextTrigger } from "./menu-context-trigger.mjs";
2
3
  import { MenuGroup, MenuGroupLabel, MenuSeparator } from "./menu-group.mjs";
3
4
  import { MenuItem } from "./menu-item.mjs";
4
5
  import { MenuPopup } from "./menu-popup.mjs";
@@ -14,6 +15,8 @@ import { MenuSubmenuTrigger } from "./menu-submenu-trigger.mjs";
14
15
  * `CheckboxItem`, `RadioGroup` > `RadioItem`, `Group` > `GroupLabel`, `Separator`, and submenus
15
16
  * (a `Root` inside a `Popup`, opened by its `SubmenuTrigger`). An item's `action` runs in its own
16
17
  * transition with `data-pending` on the item, and the whole menu closes when it settles.
18
+ * `ContextTrigger` is a region whose right-click (or Shift+F10, or a touch long-press) opens the
19
+ * same `Popup` at the pointer; a Root may hold a `Trigger` and a `ContextTrigger`.
17
20
  */
18
21
  const Menu = {
19
22
  Root: MenuRoot,
@@ -26,7 +29,8 @@ const Menu = {
26
29
  Group: MenuGroup,
27
30
  GroupLabel: MenuGroupLabel,
28
31
  Separator: MenuSeparator,
29
- SubmenuTrigger: MenuSubmenuTrigger
32
+ SubmenuTrigger: MenuSubmenuTrigger,
33
+ ContextTrigger: MenuContextTrigger
30
34
  };
31
35
  //#endregion
32
36
  export { Menu };
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../../src/menu/index.ts"],"sourcesContent":["// Ships the platform attribute typings (`command`, `commandfor`, `focusgroup`) with this subpath\n// (conventions 7.4). Type-only: the built JS has no import of it.\nimport \"../internal/platform-attributes.js\";\nimport { MenuCheckboxItem } from \"./menu-checkbox-item.js\";\nimport { MenuGroup, MenuGroupLabel, MenuSeparator } from \"./menu-group.js\";\nimport { MenuItem } from \"./menu-item.js\";\nimport { MenuPopup } from \"./menu-popup.js\";\nimport { MenuRadioGroup } from \"./menu-radio-group.js\";\nimport { MenuRadioItem } from \"./menu-radio-item.js\";\nimport { MenuRoot } from \"./menu-root.js\";\nimport { MenuSubmenuTrigger } from \"./menu-submenu-trigger.js\";\nimport { MenuTrigger } from \"./menu-trigger.js\";\n\n/**\n * Menu button (WAI-ARIA APG Menu Button + Menu) on `popover=\"auto\"`: `Root` (no DOM) > `Trigger`\n * (`button[aria-haspopup=menu]`) + `Popup` (`div[popover=auto][role=menu]`) > `Item`,\n * `CheckboxItem`, `RadioGroup` > `RadioItem`, `Group` > `GroupLabel`, `Separator`, and submenus\n * (a `Root` inside a `Popup`, opened by its `SubmenuTrigger`). An item's `action` runs in its own\n * transition with `data-pending` on the item, and the whole menu closes when it settles.\n */\nexport const Menu = {\n Root: MenuRoot,\n Trigger: MenuTrigger,\n Popup: MenuPopup,\n Item: MenuItem,\n CheckboxItem: MenuCheckboxItem,\n RadioGroup: MenuRadioGroup,\n RadioItem: MenuRadioItem,\n Group: MenuGroup,\n GroupLabel: MenuGroupLabel,\n Separator: MenuSeparator,\n SubmenuTrigger: MenuSubmenuTrigger,\n} as const;\n\nexport type { MenuRootProps, MenuRootState, MenuOpenChangeDetails } from \"./menu-root.js\";\nexport type { MenuTriggerProps, MenuTriggerState } from \"./menu-trigger.js\";\nexport type { MenuPopupProps, MenuPopupState, MenuSide, MenuAlign } from \"./menu-popup.js\";\nexport type { MenuItemProps, MenuItemState, MenuItemDetails } from \"./menu-item.js\";\nexport type { MenuCheckboxItemProps, MenuCheckboxItemState } from \"./menu-checkbox-item.js\";\nexport type { MenuRadioGroupProps, MenuRadioGroupState } from \"./menu-radio-group.js\";\nexport type { MenuRadioItemProps, MenuRadioItemState } from \"./menu-radio-item.js\";\nexport type {\n MenuGroupProps,\n MenuGroupState,\n MenuGroupLabelProps,\n MenuGroupLabelState,\n MenuSeparatorProps,\n MenuSeparatorState,\n} from \"./menu-group.js\";\nexport type { MenuSubmenuTriggerProps, MenuSubmenuTriggerState } from \"./menu-submenu-trigger.js\";\n"],"mappings":";;;;;;;;;;;;;;;;;AAoBA,MAAa,OAAO;CAClB,MAAM;CACN,SAAS;CACT,OAAO;CACP,MAAM;CACN,cAAc;CACd,YAAY;CACZ,WAAW;CACX,OAAO;CACP,YAAY;CACZ,WAAW;CACX,gBAAgB;AAClB"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../src/menu/index.ts"],"sourcesContent":["// Ships the platform attribute typings (`command`, `commandfor`, `focusgroup`) with this subpath\n// (conventions 7.4). Type-only: the built JS has no import of it.\nimport \"../internal/platform-attributes.js\";\nimport { MenuCheckboxItem } from \"./menu-checkbox-item.js\";\nimport { MenuContextTrigger } from \"./menu-context-trigger.js\";\nimport { MenuGroup, MenuGroupLabel, MenuSeparator } from \"./menu-group.js\";\nimport { MenuItem } from \"./menu-item.js\";\nimport { MenuPopup } from \"./menu-popup.js\";\nimport { MenuRadioGroup } from \"./menu-radio-group.js\";\nimport { MenuRadioItem } from \"./menu-radio-item.js\";\nimport { MenuRoot } from \"./menu-root.js\";\nimport { MenuSubmenuTrigger } from \"./menu-submenu-trigger.js\";\nimport { MenuTrigger } from \"./menu-trigger.js\";\n\n/**\n * Menu button (WAI-ARIA APG Menu Button + Menu) on `popover=\"auto\"`: `Root` (no DOM) > `Trigger`\n * (`button[aria-haspopup=menu]`) + `Popup` (`div[popover=auto][role=menu]`) > `Item`,\n * `CheckboxItem`, `RadioGroup` > `RadioItem`, `Group` > `GroupLabel`, `Separator`, and submenus\n * (a `Root` inside a `Popup`, opened by its `SubmenuTrigger`). An item's `action` runs in its own\n * transition with `data-pending` on the item, and the whole menu closes when it settles.\n * `ContextTrigger` is a region whose right-click (or Shift+F10, or a touch long-press) opens the\n * same `Popup` at the pointer; a Root may hold a `Trigger` and a `ContextTrigger`.\n */\nexport const Menu = {\n Root: MenuRoot,\n Trigger: MenuTrigger,\n Popup: MenuPopup,\n Item: MenuItem,\n CheckboxItem: MenuCheckboxItem,\n RadioGroup: MenuRadioGroup,\n RadioItem: MenuRadioItem,\n Group: MenuGroup,\n GroupLabel: MenuGroupLabel,\n Separator: MenuSeparator,\n SubmenuTrigger: MenuSubmenuTrigger,\n ContextTrigger: MenuContextTrigger,\n} as const;\n\nexport type { MenuRootProps, MenuRootState, MenuOpenChangeDetails } from \"./menu-root.js\";\nexport type { MenuTriggerProps, MenuTriggerState } from \"./menu-trigger.js\";\nexport type { MenuPopupProps, MenuPopupState, MenuSide, MenuAlign } from \"./menu-popup.js\";\nexport type { MenuItemProps, MenuItemState, MenuItemDetails } from \"./menu-item.js\";\nexport type { MenuCheckboxItemProps, MenuCheckboxItemState } from \"./menu-checkbox-item.js\";\nexport type { MenuRadioGroupProps, MenuRadioGroupState } from \"./menu-radio-group.js\";\nexport type { MenuRadioItemProps, MenuRadioItemState } from \"./menu-radio-item.js\";\nexport type {\n MenuGroupProps,\n MenuGroupState,\n MenuGroupLabelProps,\n MenuGroupLabelState,\n MenuSeparatorProps,\n MenuSeparatorState,\n} from \"./menu-group.js\";\nexport type { MenuSubmenuTriggerProps, MenuSubmenuTriggerState } from \"./menu-submenu-trigger.js\";\nexport type { MenuContextTriggerProps, MenuContextTriggerState } from \"./menu-context-trigger.js\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAuBA,MAAa,OAAO;CAClB,MAAM;CACN,SAAS;CACT,OAAO;CACP,MAAM;CACN,cAAc;CACd,YAAY;CACZ,WAAW;CACX,OAAO;CACP,YAAY;CACZ,WAAW;CACX,gBAAgB;CAChB,gBAAgB;AAClB"}
@@ -0,0 +1,46 @@
1
+ import { RenderProp, StateValue } from "../internal/types.mjs";
2
+ import { CSSProperties, ComponentPropsWithRef, ReactNode } from "react";
3
+ //#region src/menu/menu-context-trigger.d.ts
4
+ export interface MenuContextTriggerState {
5
+ /** Optimistic open state (the Root's). */
6
+ readonly open: boolean;
7
+ /** The Root's transition is pending (an open or a close in flight). */
8
+ readonly pending: boolean;
9
+ readonly disabled: boolean;
10
+ }
11
+ export interface MenuContextTriggerProps extends Omit<ComponentPropsWithRef<"div">, "onContextMenu" | "onPointerDown" | "onPointerMove" | "onPointerUp" | "onPointerCancel" | "className" | "style" | "children"> {
12
+ /** The region does nothing; a `contextmenu` bubbles on to an outer region. `data-disabled`. */
13
+ disabled?: boolean;
14
+ /**
15
+ * What a `contextmenu` (or long-press) on an editable target inside the region opens: the
16
+ * browser's own menu (`"native"`, default: copy and paste keep working) or this menu.
17
+ * Editable: a text-taking `<input>`, a `<textarea>`, an element with `isContentEditable`.
18
+ */
19
+ editableTargets?: "native" | "menu";
20
+ /** Milliseconds a touch or pen pointer must stay down (within 10 px) to open. Default 500. */
21
+ longPressDelay?: number;
22
+ className?: StateValue<string | undefined, MenuContextTriggerState>;
23
+ style?: StateValue<CSSProperties | undefined, MenuContextTriggerState>;
24
+ children?: StateValue<ReactNode, MenuContextTriggerState>;
25
+ render?: RenderProp<ComponentPropsWithRef<"div">, MenuContextTriggerState>;
26
+ }
27
+ /**
28
+ * A region whose `contextmenu` (right-click, Ctrl+click, Shift+F10 / the ContextMenu key at a
29
+ * focused descendant) or touch / pen long-press opens the Root's `Menu.Popup` at the pointer
30
+ * (design/components/context-menu.md). A plain element (default `div`) with no role and no ARIA:
31
+ * the region keeps its own semantics (`render={<tr />}`), and keyboard users reach it through
32
+ * focusable content inside it. A second right-click inside the region while the menu is open moves
33
+ * the menu there. Render the `Menu.Popup` as a sibling of the region where the markup allows; in
34
+ * a table row (`render={<tr />}`), inside one of its cells: events from inside the popup are
35
+ * never the region's.
36
+ *
37
+ * Owned keys (conventions 1.2): `onContextMenu`, `onPointerDown`, `onPointerMove`,
38
+ * `onPointerUp`, `onPointerCancel`, `data-open`, `data-pending`, `data-disabled`, and the style
39
+ * key `WebkitTouchCallout` (`"none"`; the consumer's `style` wins).
40
+ */
41
+ export declare function MenuContextTrigger({ disabled, editableTargets, longPressDelay, className, style, children, render, ...rest }: MenuContextTriggerProps): ReactNode;
42
+ export declare namespace MenuContextTrigger {
43
+ var displayName: string;
44
+ }
45
+ //#endregion
46
+ //# sourceMappingURL=menu-context-trigger.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"menu-context-trigger.d.mts","names":[],"sources":["../../src/menu/menu-context-trigger.tsx"],"mappings":";;;iBAuBiB;;WAEN;;WAEA;WACA;;iBAGM,gCAAgC,KAC/C;;EAWA;;;;;;EAMA;;EAEA;EACA,YAAY,+BAA+B;EAC3C,QAAQ,WAAW,2BAA2B;EAC9C,WAAW,WAAW,WAAW;EACjC,SAAS,WAAW,8BAA8B;;;;;;;;;;;;;;;;wBA6BpC,qBACd,UACA,iBACA,gBACA,WACA,OACA,UACA,WACG,QACF,0BAAuB;yBATV"}
@@ -0,0 +1,226 @@
1
+ "use client";
2
+ import { dataAttr } from "../internal/data-attr.mjs";
3
+ import { useDevWarning } from "../internal/use-dev-warning.mjs";
4
+ import { resolveStateValue } from "../internal/render-element.mjs";
5
+ import { useRender } from "../internal/use-render.mjs";
6
+ import { MenuRegionContext, claimPress, isPressClaimed, markHandled, useMenuContext } from "./menu-context.mjs";
7
+ import { capturePointer, isEditableTarget } from "./menu-dom.mjs";
8
+ import { useCallback, useEffect, useRef } from "react";
9
+ import { jsx } from "react/jsx-runtime";
10
+ //#region src/menu/menu-context-trigger.tsx
11
+ /** Movement that cancels a long-press (Base UI's slop). */
12
+ const LONG_PRESS_SLOP = 10;
13
+ const isDev = process.env.NODE_ENV !== "production";
14
+ /**
15
+ * A region whose `contextmenu` (right-click, Ctrl+click, Shift+F10 / the ContextMenu key at a
16
+ * focused descendant) or touch / pen long-press opens the Root's `Menu.Popup` at the pointer
17
+ * (design/components/context-menu.md). A plain element (default `div`) with no role and no ARIA:
18
+ * the region keeps its own semantics (`render={<tr />}`), and keyboard users reach it through
19
+ * focusable content inside it. A second right-click inside the region while the menu is open moves
20
+ * the menu there. Render the `Menu.Popup` as a sibling of the region where the markup allows; in
21
+ * a table row (`render={<tr />}`), inside one of its cells: events from inside the popup are
22
+ * never the region's.
23
+ *
24
+ * Owned keys (conventions 1.2): `onContextMenu`, `onPointerDown`, `onPointerMove`,
25
+ * `onPointerUp`, `onPointerCancel`, `data-open`, `data-pending`, `data-disabled`, and the style
26
+ * key `WebkitTouchCallout` (`"none"`; the consumer's `style` wins).
27
+ */
28
+ function MenuContextTrigger({ disabled = false, editableTargets = "native", longPressDelay = 500, className, style, children, render, ...rest }) {
29
+ const menu = useMenuContext("Menu.ContextTrigger");
30
+ const { popover } = menu;
31
+ useDevWarning(menu.submenu, "Menu.ContextTrigger: rendered in a submenu. A Menu.Root inside a Menu.Popup is a submenu; a context menu region belongs to a top-level Menu.Root.");
32
+ useDevWarning(longPressDelay < 0, "Menu.ContextTrigger: `longPressDelay` is negative; it is a number of milliseconds.");
33
+ const sequence = useRef(0);
34
+ const lastPointerDown = useRef(-1);
35
+ const lastKeyDown = useRef(-1);
36
+ const longPress = useRef(null);
37
+ const disarmSwallow = useRef(null);
38
+ const warnedNoPopup = useRef(false);
39
+ const cancelLongPress = useCallback(() => {
40
+ if (longPress.current !== null) clearTimeout(longPress.current.timer);
41
+ longPress.current = null;
42
+ }, []);
43
+ useEffect(() => () => {
44
+ cancelLongPress();
45
+ disarmSwallow.current?.();
46
+ }, [cancelLongPress]);
47
+ useEffect(() => {
48
+ if (disabled) cancelLongPress();
49
+ }, [disabled, cancelLongPress]);
50
+ const open = popover.optimistic;
51
+ useEffect(() => {
52
+ if (!open) disarmSwallow.current?.();
53
+ }, [open]);
54
+ const modalityRef = useCallback((element) => {
55
+ const onKeyDown = () => {
56
+ lastKeyDown.current = ++sequence.current;
57
+ };
58
+ const onPointerDown = () => {
59
+ lastPointerDown.current = ++sequence.current;
60
+ };
61
+ element.addEventListener("keydown", onKeyDown, true);
62
+ element.addEventListener("pointerdown", onPointerDown, true);
63
+ return () => {
64
+ element.removeEventListener("keydown", onKeyDown, true);
65
+ element.removeEventListener("pointerdown", onPointerDown, true);
66
+ };
67
+ }, []);
68
+ /** The popup element to capture on and open, or `null` (no Popup mounted: nothing opens). */
69
+ const popupElement = () => {
70
+ const popup = popover.popupRef.current;
71
+ if (popup === null && isDev && !warnedNoPopup.current) {
72
+ warnedNoPopup.current = true;
73
+ console.error("Menu.ContextTrigger: a context menu was requested but the Menu.Root has no mounted Menu.Popup; the browser's own menu shows instead.");
74
+ }
75
+ return popup;
76
+ };
77
+ /**
78
+ * One-shot swallow of the `click` an engine may synthesise when the long-press ends (2.2),
79
+ * scoped to that release (review H1): the click comes in the same task as the `pointerup` of
80
+ * `pointerId`, so the swallow ends one task after that `pointerup` (or `pointercancel`), and at
81
+ * once on any `keydown`, the next `pointerdown`, or the menu closing. iOS synthesises no click
82
+ * after a long-press; a swallow left armed would eat the next keyboard activation
83
+ * (`element.click()`).
84
+ */
85
+ const armClickSwallow = (doc, pointerId) => {
86
+ disarmSwallow.current?.();
87
+ let released;
88
+ const onClick = (event) => {
89
+ event.preventDefault();
90
+ event.stopPropagation();
91
+ disarm();
92
+ };
93
+ const onRelease = (event) => {
94
+ if (event.pointerId !== pointerId) return;
95
+ if (released === void 0) released = setTimeout(disarm, 0);
96
+ };
97
+ const disarm = () => {
98
+ if (released !== void 0) clearTimeout(released);
99
+ doc.removeEventListener("click", onClick, true);
100
+ doc.removeEventListener("pointerdown", disarm, true);
101
+ doc.removeEventListener("keydown", disarm, true);
102
+ doc.removeEventListener("pointerup", onRelease, true);
103
+ doc.removeEventListener("pointercancel", onRelease, true);
104
+ if (disarmSwallow.current === disarm) disarmSwallow.current = null;
105
+ };
106
+ doc.addEventListener("click", onClick, true);
107
+ doc.addEventListener("pointerdown", disarm, true);
108
+ doc.addEventListener("keydown", disarm, true);
109
+ doc.addEventListener("pointerup", onRelease, true);
110
+ doc.addEventListener("pointercancel", onRelease, true);
111
+ disarmSwallow.current = disarm;
112
+ };
113
+ /**
114
+ * The event comes from this Root's own popup, rendered inside the region (a table row, where a
115
+ * `<div>` cannot be a sibling of the `<tr>`): never a move or a long-press of the region.
116
+ */
117
+ const fromOwnPopup = (target) => {
118
+ const popup = popover.popupRef.current;
119
+ return popup !== null && target instanceof Node && popup.contains(target);
120
+ };
121
+ const onContextMenu = (event) => {
122
+ const native = event.nativeEvent;
123
+ if (native.defaultPrevented || disabled || fromOwnPopup(native.target)) return;
124
+ if (editableTargets === "native" && isEditableTarget(native.target)) return;
125
+ const popup = popupElement();
126
+ if (popup === null) return;
127
+ native.preventDefault();
128
+ markHandled(native, menu);
129
+ cancelLongPress();
130
+ const keyboard = native.button === -1 || lastKeyDown.current > lastPointerDown.current;
131
+ const pointerId = native.pointerId;
132
+ const id = typeof pointerId === "number" ? pointerId : void 0;
133
+ if (native.buttons !== 0) capturePointer(popup, id);
134
+ menu.openAt({
135
+ x: native.clientX,
136
+ y: native.clientY
137
+ }, {
138
+ source: event.currentTarget,
139
+ event: native,
140
+ pointer: !keyboard,
141
+ pointerId: id
142
+ });
143
+ };
144
+ const onPointerDown = (event) => {
145
+ const native = event.nativeEvent;
146
+ if (longPress.current !== null) {
147
+ cancelLongPress();
148
+ return;
149
+ }
150
+ if (disabled || native.pointerType !== "touch" && native.pointerType !== "pen") return;
151
+ if (fromOwnPopup(native.target)) return;
152
+ if (!native.isPrimary || isPressClaimed(native)) return;
153
+ if (editableTargets === "native" && isEditableTarget(native.target)) return;
154
+ claimPress(native);
155
+ const source = event.currentTarget;
156
+ const { pointerId, clientX, clientY } = native;
157
+ const timer = setTimeout(() => {
158
+ longPress.current = null;
159
+ const popup = popupElement();
160
+ if (popup === null) return;
161
+ capturePointer(popup, pointerId);
162
+ armClickSwallow(source.ownerDocument, pointerId);
163
+ menu.openAt({
164
+ x: clientX,
165
+ y: clientY
166
+ }, {
167
+ source,
168
+ event: native,
169
+ pointer: true,
170
+ pointerId
171
+ });
172
+ }, Math.max(0, longPressDelay));
173
+ longPress.current = {
174
+ pointerId,
175
+ x: clientX,
176
+ y: clientY,
177
+ timer
178
+ };
179
+ };
180
+ const onPointerMove = (event) => {
181
+ const press = longPress.current;
182
+ if (press === null || event.pointerId !== press.pointerId) return;
183
+ if (Math.hypot(event.clientX - press.x, event.clientY - press.y) > LONG_PRESS_SLOP) cancelLongPress();
184
+ };
185
+ const onPointerEnd = (event) => {
186
+ const press = longPress.current;
187
+ if (press !== null && event.pointerId === press.pointerId) cancelLongPress();
188
+ };
189
+ const state = {
190
+ open: popover.optimistic,
191
+ pending: popover.pending,
192
+ disabled
193
+ };
194
+ const libraryProps = {
195
+ onContextMenu,
196
+ onPointerDown,
197
+ onPointerMove,
198
+ onPointerUp: onPointerEnd,
199
+ onPointerCancel: onPointerEnd,
200
+ "data-open": dataAttr(state.open),
201
+ "data-pending": dataAttr(state.pending),
202
+ "data-disabled": dataAttr(disabled)
203
+ };
204
+ return useRender({
205
+ defaultElement: "div",
206
+ render,
207
+ state,
208
+ className,
209
+ style: (s) => ({
210
+ WebkitTouchCallout: "none",
211
+ ...resolveStateValue(style, s)
212
+ }),
213
+ children: (s) => /* @__PURE__ */ jsx(MenuRegionContext, {
214
+ value: true,
215
+ children: resolveStateValue(children, s)
216
+ }),
217
+ props: rest,
218
+ libraryProps,
219
+ ref: modalityRef
220
+ });
221
+ }
222
+ MenuContextTrigger.displayName = "MenuContextTrigger";
223
+ //#endregion
224
+ export { MenuContextTrigger };
225
+
226
+ //# sourceMappingURL=menu-context-trigger.mjs.map