@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.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"menu-context-trigger.mjs","names":[],"sources":["../../src/menu/menu-context-trigger.tsx"],"sourcesContent":["\"use client\";\nimport { useCallback, useEffect, useRef } from \"react\";\nimport type {\n ComponentPropsWithRef,\n CSSProperties,\n MouseEvent,\n PointerEvent,\n ReactNode,\n} from \"react\";\nimport { dataAttr } from \"../internal/data-attr.js\";\nimport type { RenderProp, StateValue } from \"../internal/types.js\";\nimport { useDevWarning } from \"../internal/use-dev-warning.js\";\nimport { resolveStateValue, useRender } from \"../internal/use-render.js\";\nimport type { DataAttributes } from \"../internal/use-render.js\";\nimport {\n claimPress,\n isPressClaimed,\n markHandled,\n MenuRegionContext,\n useMenuContext,\n} from \"./menu-context.js\";\nimport { capturePointer, isEditableTarget } from \"./menu-dom.js\";\n\nexport interface MenuContextTriggerState {\n /** Optimistic open state (the Root's). */\n readonly open: boolean;\n /** The Root's transition is pending (an open or a close in flight). */\n readonly pending: boolean;\n readonly disabled: boolean;\n}\n\nexport interface MenuContextTriggerProps extends Omit<\n ComponentPropsWithRef<\"div\">,\n | \"onContextMenu\"\n | \"onPointerDown\"\n | \"onPointerMove\"\n | \"onPointerUp\"\n | \"onPointerCancel\"\n | \"className\"\n | \"style\"\n | \"children\"\n> {\n /** The region does nothing; a `contextmenu` bubbles on to an outer region. `data-disabled`. */\n disabled?: boolean;\n /**\n * What a `contextmenu` (or long-press) on an editable target inside the region opens: the\n * browser's own menu (`\"native\"`, default: copy and paste keep working) or this menu.\n * Editable: a text-taking `<input>`, a `<textarea>`, an element with `isContentEditable`.\n */\n editableTargets?: \"native\" | \"menu\";\n /** Milliseconds a touch or pen pointer must stay down (within 10 px) to open. Default 500. */\n longPressDelay?: number;\n className?: StateValue<string | undefined, MenuContextTriggerState>;\n style?: StateValue<CSSProperties | undefined, MenuContextTriggerState>;\n children?: StateValue<ReactNode, MenuContextTriggerState>;\n render?: RenderProp<ComponentPropsWithRef<\"div\">, MenuContextTriggerState>;\n}\n\n/** Movement that cancels a long-press (Base UI's slop). */\nconst LONG_PRESS_SLOP = 10;\n\nconst isDev = process.env.NODE_ENV !== \"production\";\n\ninterface LongPress {\n readonly pointerId: number;\n readonly x: number;\n readonly y: number;\n readonly timer: ReturnType<typeof setTimeout>;\n}\n\n/**\n * A region whose `contextmenu` (right-click, Ctrl+click, Shift+F10 / the ContextMenu key at a\n * focused descendant) or touch / pen long-press opens the Root's `Menu.Popup` at the pointer\n * (design/components/context-menu.md). A plain element (default `div`) with no role and no ARIA:\n * the region keeps its own semantics (`render={<tr />}`), and keyboard users reach it through\n * focusable content inside it. A second right-click inside the region while the menu is open moves\n * the menu there. Render the `Menu.Popup` as a sibling of the region where the markup allows; in\n * a table row (`render={<tr />}`), inside one of its cells: events from inside the popup are\n * never the region's.\n *\n * Owned keys (conventions 1.2): `onContextMenu`, `onPointerDown`, `onPointerMove`,\n * `onPointerUp`, `onPointerCancel`, `data-open`, `data-pending`, `data-disabled`, and the style\n * key `WebkitTouchCallout` (`\"none\"`; the consumer's `style` wins).\n */\nexport function MenuContextTrigger({\n disabled = false,\n editableTargets = \"native\",\n longPressDelay = 500,\n className,\n style,\n children,\n render,\n ...rest\n}: MenuContextTriggerProps) {\n const menu = useMenuContext(\"Menu.ContextTrigger\");\n const { popover } = menu;\n useDevWarning(\n menu.submenu,\n \"Menu.ContextTrigger: rendered in a submenu. A Menu.Root inside a Menu.Popup is a submenu; a \" +\n \"context menu region belongs to a top-level Menu.Root.\",\n );\n useDevWarning(\n longPressDelay < 0,\n \"Menu.ContextTrigger: `longPressDelay` is negative; it is a number of milliseconds.\",\n );\n\n // Modality bookkeeping (design 2.1): a sequence number, not a clock, so two events in the same\n // millisecond still order.\n const sequence = useRef(0);\n const lastPointerDown = useRef(-1);\n const lastKeyDown = useRef(-1);\n const longPress = useRef<LongPress | null>(null);\n // Disarms the one-shot click swallow armed by a long-press open.\n const disarmSwallow = useRef<(() => void) | null>(null);\n const warnedNoPopup = useRef(false);\n\n const cancelLongPress = useCallback(() => {\n if (longPress.current !== null) clearTimeout(longPress.current.timer);\n longPress.current = null;\n }, []);\n\n useEffect(\n () => () => {\n cancelLongPress();\n disarmSwallow.current?.();\n },\n [cancelLongPress],\n );\n // A region disabled mid-gesture opens nothing.\n useEffect(() => {\n if (disabled) cancelLongPress();\n }, [disabled, cancelLongPress]);\n\n // A close ends any armed click swallow (review H1).\n const open = popover.optimistic;\n useEffect(() => {\n if (!open) disarmSwallow.current?.();\n }, [open]);\n\n // The modality bookkeeping observes `keydown` and `pointerdown` without owning them (the region\n // is full of the consumer's interactive content), in the capture phase, so a descendant's\n // `stopPropagation()` (a slider, a drag handle) does not hide them (review L2).\n const modalityRef = useCallback((element: HTMLElement) => {\n const onKeyDown = () => {\n lastKeyDown.current = ++sequence.current;\n };\n const onPointerDown = () => {\n lastPointerDown.current = ++sequence.current;\n };\n element.addEventListener(\"keydown\", onKeyDown, true);\n element.addEventListener(\"pointerdown\", onPointerDown, true);\n return () => {\n element.removeEventListener(\"keydown\", onKeyDown, true);\n element.removeEventListener(\"pointerdown\", onPointerDown, true);\n };\n }, []);\n\n /** The popup element to capture on and open, or `null` (no Popup mounted: nothing opens). */\n const popupElement = (): HTMLElement | null => {\n const popup = popover.popupRef.current;\n if (popup === null && isDev && !warnedNoPopup.current) {\n warnedNoPopup.current = true;\n console.error(\n \"Menu.ContextTrigger: a context menu was requested but the Menu.Root has no mounted \" +\n \"Menu.Popup; the browser's own menu shows instead.\",\n );\n }\n return popup;\n };\n\n /**\n * One-shot swallow of the `click` an engine may synthesise when the long-press ends (2.2),\n * scoped to that release (review H1): the click comes in the same task as the `pointerup` of\n * `pointerId`, so the swallow ends one task after that `pointerup` (or `pointercancel`), and at\n * once on any `keydown`, the next `pointerdown`, or the menu closing. iOS synthesises no click\n * after a long-press; a swallow left armed would eat the next keyboard activation\n * (`element.click()`).\n */\n const armClickSwallow = (doc: Document, pointerId: number) => {\n disarmSwallow.current?.();\n let released: ReturnType<typeof setTimeout> | undefined;\n const onClick = (event: Event) => {\n event.preventDefault();\n event.stopPropagation();\n disarm();\n };\n const onRelease = (event: Event) => {\n if ((event as globalThis.PointerEvent).pointerId !== pointerId) return;\n if (released === undefined) released = setTimeout(disarm, 0);\n };\n const disarm = () => {\n if (released !== undefined) clearTimeout(released);\n doc.removeEventListener(\"click\", onClick, true);\n doc.removeEventListener(\"pointerdown\", disarm, true);\n doc.removeEventListener(\"keydown\", disarm, true);\n doc.removeEventListener(\"pointerup\", onRelease, true);\n doc.removeEventListener(\"pointercancel\", onRelease, true);\n if (disarmSwallow.current === disarm) disarmSwallow.current = null;\n };\n doc.addEventListener(\"click\", onClick, true);\n doc.addEventListener(\"pointerdown\", disarm, true);\n doc.addEventListener(\"keydown\", disarm, true);\n doc.addEventListener(\"pointerup\", onRelease, true);\n doc.addEventListener(\"pointercancel\", onRelease, true);\n disarmSwallow.current = disarm;\n };\n\n /**\n * The event comes from this Root's own popup, rendered inside the region (a table row, where a\n * `<div>` cannot be a sibling of the `<tr>`): never a move or a long-press of the region.\n */\n const fromOwnPopup = (target: EventTarget | null): boolean => {\n const popup = popover.popupRef.current;\n return popup !== null && target instanceof Node && popup.contains(target);\n };\n\n const onContextMenu = (event: MouseEvent<HTMLElement>) => {\n const native = event.nativeEvent;\n // An inner region or a consumer handler was first (the Popup's own handler prevents a\n // right-click inside the menu); a disabled region lets the event bubble on to an outer\n // region (design 2.4).\n if (native.defaultPrevented || disabled || fromOwnPopup(native.target)) return;\n // Copy and paste keep the browser's menu (2.3).\n if (editableTargets === \"native\" && isEditableTarget(native.target)) return;\n const popup = popupElement();\n if (popup === null) return;\n native.preventDefault();\n markHandled(native, menu);\n // Android and pen: the platform's own `contextmenu` arrived during our timer; it opens.\n cancelLongPress();\n const keyboard = native.button === -1 || lastKeyDown.current > lastPointerDown.current;\n const pointerId = (native as globalThis.PointerEvent).pointerId;\n const id = typeof pointerId === \"number\" ? pointerId : undefined;\n // whatwg/html#10905: in Firefox and WebKit the pending `pointerup` would light-dismiss a\n // menu opened between `pointerdown` and `pointerup`. Capture now, on the still-hidden popup:\n // the open commit lands at an unpredictable time relative to the `pointerup`.\n if (native.buttons !== 0) capturePointer(popup, id);\n menu.openAt(\n { x: native.clientX, y: native.clientY },\n { source: event.currentTarget, event: native, pointer: !keyboard, pointerId: id },\n );\n };\n\n // The long-press only; the modality bookkeeping is `modalityRef`'s.\n const onPointerDown = (event: PointerEvent<HTMLElement>) => {\n const native = event.nativeEvent;\n if (longPress.current !== null) {\n // A second finger: not a long-press.\n cancelLongPress();\n return;\n }\n if (disabled || (native.pointerType !== \"touch\" && native.pointerType !== \"pen\")) return;\n // A press inside any Menu.Popup (this Root's, or another menu inside the region) is claimed by\n // that popup; a press an inner region already times is claimed by it.\n if (fromOwnPopup(native.target)) return;\n if (!native.isPrimary || isPressClaimed(native)) return;\n if (editableTargets === \"native\" && isEditableTarget(native.target)) return;\n // The innermost enabled region times the press; outer regions leave it (as `contextmenu`'s\n // `defaultPrevented` rule, design 2.4).\n claimPress(native);\n const source = event.currentTarget;\n const { pointerId, clientX, clientY } = native;\n const timer = setTimeout(\n () => {\n longPress.current = null;\n const popup = popupElement();\n if (popup === null) return;\n // The finger is still down: its `pointerup` would light-dismiss in WebKit.\n capturePointer(popup, pointerId);\n armClickSwallow(source.ownerDocument, pointerId);\n menu.openAt(\n { x: clientX, y: clientY },\n { source, event: native, pointer: true, pointerId },\n );\n },\n Math.max(0, longPressDelay),\n );\n longPress.current = { pointerId, x: clientX, y: clientY, timer };\n };\n\n const onPointerMove = (event: PointerEvent<HTMLElement>) => {\n const press = longPress.current;\n if (press === null || event.pointerId !== press.pointerId) return;\n if (Math.hypot(event.clientX - press.x, event.clientY - press.y) > LONG_PRESS_SLOP) {\n cancelLongPress();\n }\n };\n\n // `pointercancel`: the browser took the gesture for panning (the region stays scrollable).\n const onPointerEnd = (event: PointerEvent<HTMLElement>) => {\n const press = longPress.current;\n if (press !== null && event.pointerId === press.pointerId) cancelLongPress();\n };\n\n const state: MenuContextTriggerState = {\n open: popover.optimistic,\n pending: popover.pending,\n disabled,\n };\n\n const libraryProps: ComponentPropsWithRef<\"div\"> & DataAttributes = {\n onContextMenu,\n onPointerDown,\n onPointerMove,\n onPointerUp: onPointerEnd,\n onPointerCancel: onPointerEnd,\n \"data-open\": dataAttr(state.open),\n \"data-pending\": dataAttr(state.pending),\n \"data-disabled\": dataAttr(disabled),\n };\n\n return useRender<ComponentPropsWithRef<\"div\">, MenuContextTriggerState>({\n defaultElement: \"div\",\n render,\n state,\n className,\n // iOS link and image previews appear at about the long-press time (2.2).\n style: (s) => ({ WebkitTouchCallout: \"none\", ...resolveStateValue(style, s) }),\n // A Menu.Root inside is a nested region's menu (no \"outside its Popup\" warning).\n children: (s) => (\n <MenuRegionContext value={true}>{resolveStateValue(children, s)}</MenuRegionContext>\n ),\n props: rest,\n libraryProps,\n ref: modalityRef,\n });\n}\n\nMenuContextTrigger.displayName = \"MenuContextTrigger\";\n"],"mappings":";;;;;;;;;;;AA2DA,MAAM,kBAAkB;AAExB,MAAM,QAAQ,QAAQ,IAAI,aAAa;;;;;;;;;;;;;;;AAuBvC,SAAgB,mBAAmB,EACjC,WAAW,OACX,kBAAkB,UAClB,iBAAiB,KACjB,WACA,OACA,UACA,QACA,GAAG,QACuB;CAC1B,MAAM,OAAO,eAAe,qBAAqB;CACjD,MAAM,EAAE,YAAY;CACpB,cACE,KAAK,SACL,mJAEF;CACA,cACE,iBAAiB,GACjB,oFACF;CAIA,MAAM,WAAW,OAAO,CAAC;CACzB,MAAM,kBAAkB,OAAO,EAAE;CACjC,MAAM,cAAc,OAAO,EAAE;CAC7B,MAAM,YAAY,OAAyB,IAAI;CAE/C,MAAM,gBAAgB,OAA4B,IAAI;CACtD,MAAM,gBAAgB,OAAO,KAAK;CAElC,MAAM,kBAAkB,kBAAkB;EACxC,IAAI,UAAU,YAAY,MAAM,aAAa,UAAU,QAAQ,KAAK;EACpE,UAAU,UAAU;CACtB,GAAG,CAAC,CAAC;CAEL,sBACc;EACV,gBAAgB;EAChB,cAAc,UAAU;CAC1B,GACA,CAAC,eAAe,CAClB;CAEA,gBAAgB;EACd,IAAI,UAAU,gBAAgB;CAChC,GAAG,CAAC,UAAU,eAAe,CAAC;CAG9B,MAAM,OAAO,QAAQ;CACrB,gBAAgB;EACd,IAAI,CAAC,MAAM,cAAc,UAAU;CACrC,GAAG,CAAC,IAAI,CAAC;CAKT,MAAM,cAAc,aAAa,YAAyB;EACxD,MAAM,kBAAkB;GACtB,YAAY,UAAU,EAAE,SAAS;EACnC;EACA,MAAM,sBAAsB;GAC1B,gBAAgB,UAAU,EAAE,SAAS;EACvC;EACA,QAAQ,iBAAiB,WAAW,WAAW,IAAI;EACnD,QAAQ,iBAAiB,eAAe,eAAe,IAAI;EAC3D,aAAa;GACX,QAAQ,oBAAoB,WAAW,WAAW,IAAI;GACtD,QAAQ,oBAAoB,eAAe,eAAe,IAAI;EAChE;CACF,GAAG,CAAC,CAAC;;CAGL,MAAM,qBAAyC;EAC7C,MAAM,QAAQ,QAAQ,SAAS;EAC/B,IAAI,UAAU,QAAQ,SAAS,CAAC,cAAc,SAAS;GACrD,cAAc,UAAU;GACxB,QAAQ,MACN,sIAEF;EACF;EACA,OAAO;CACT;;;;;;;;;CAUA,MAAM,mBAAmB,KAAe,cAAsB;EAC5D,cAAc,UAAU;EACxB,IAAI;EACJ,MAAM,WAAW,UAAiB;GAChC,MAAM,eAAe;GACrB,MAAM,gBAAgB;GACtB,OAAO;EACT;EACA,MAAM,aAAa,UAAiB;GAClC,IAAK,MAAkC,cAAc,WAAW;GAChE,IAAI,aAAa,KAAA,GAAW,WAAW,WAAW,QAAQ,CAAC;EAC7D;EACA,MAAM,eAAe;GACnB,IAAI,aAAa,KAAA,GAAW,aAAa,QAAQ;GACjD,IAAI,oBAAoB,SAAS,SAAS,IAAI;GAC9C,IAAI,oBAAoB,eAAe,QAAQ,IAAI;GACnD,IAAI,oBAAoB,WAAW,QAAQ,IAAI;GAC/C,IAAI,oBAAoB,aAAa,WAAW,IAAI;GACpD,IAAI,oBAAoB,iBAAiB,WAAW,IAAI;GACxD,IAAI,cAAc,YAAY,QAAQ,cAAc,UAAU;EAChE;EACA,IAAI,iBAAiB,SAAS,SAAS,IAAI;EAC3C,IAAI,iBAAiB,eAAe,QAAQ,IAAI;EAChD,IAAI,iBAAiB,WAAW,QAAQ,IAAI;EAC5C,IAAI,iBAAiB,aAAa,WAAW,IAAI;EACjD,IAAI,iBAAiB,iBAAiB,WAAW,IAAI;EACrD,cAAc,UAAU;CAC1B;;;;;CAMA,MAAM,gBAAgB,WAAwC;EAC5D,MAAM,QAAQ,QAAQ,SAAS;EAC/B,OAAO,UAAU,QAAQ,kBAAkB,QAAQ,MAAM,SAAS,MAAM;CAC1E;CAEA,MAAM,iBAAiB,UAAmC;EACxD,MAAM,SAAS,MAAM;EAIrB,IAAI,OAAO,oBAAoB,YAAY,aAAa,OAAO,MAAM,GAAG;EAExE,IAAI,oBAAoB,YAAY,iBAAiB,OAAO,MAAM,GAAG;EACrE,MAAM,QAAQ,aAAa;EAC3B,IAAI,UAAU,MAAM;EACpB,OAAO,eAAe;EACtB,YAAY,QAAQ,IAAI;EAExB,gBAAgB;EAChB,MAAM,WAAW,OAAO,WAAW,MAAM,YAAY,UAAU,gBAAgB;EAC/E,MAAM,YAAa,OAAmC;EACtD,MAAM,KAAK,OAAO,cAAc,WAAW,YAAY,KAAA;EAIvD,IAAI,OAAO,YAAY,GAAG,eAAe,OAAO,EAAE;EAClD,KAAK,OACH;GAAE,GAAG,OAAO;GAAS,GAAG,OAAO;EAAQ,GACvC;GAAE,QAAQ,MAAM;GAAe,OAAO;GAAQ,SAAS,CAAC;GAAU,WAAW;EAAG,CAClF;CACF;CAGA,MAAM,iBAAiB,UAAqC;EAC1D,MAAM,SAAS,MAAM;EACrB,IAAI,UAAU,YAAY,MAAM;GAE9B,gBAAgB;GAChB;EACF;EACA,IAAI,YAAa,OAAO,gBAAgB,WAAW,OAAO,gBAAgB,OAAQ;EAGlF,IAAI,aAAa,OAAO,MAAM,GAAG;EACjC,IAAI,CAAC,OAAO,aAAa,eAAe,MAAM,GAAG;EACjD,IAAI,oBAAoB,YAAY,iBAAiB,OAAO,MAAM,GAAG;EAGrE,WAAW,MAAM;EACjB,MAAM,SAAS,MAAM;EACrB,MAAM,EAAE,WAAW,SAAS,YAAY;EACxC,MAAM,QAAQ,iBACN;GACJ,UAAU,UAAU;GACpB,MAAM,QAAQ,aAAa;GAC3B,IAAI,UAAU,MAAM;GAEpB,eAAe,OAAO,SAAS;GAC/B,gBAAgB,OAAO,eAAe,SAAS;GAC/C,KAAK,OACH;IAAE,GAAG;IAAS,GAAG;GAAQ,GACzB;IAAE;IAAQ,OAAO;IAAQ,SAAS;IAAM;GAAU,CACpD;EACF,GACA,KAAK,IAAI,GAAG,cAAc,CAC5B;EACA,UAAU,UAAU;GAAE;GAAW,GAAG;GAAS,GAAG;GAAS;EAAM;CACjE;CAEA,MAAM,iBAAiB,UAAqC;EAC1D,MAAM,QAAQ,UAAU;EACxB,IAAI,UAAU,QAAQ,MAAM,cAAc,MAAM,WAAW;EAC3D,IAAI,KAAK,MAAM,MAAM,UAAU,MAAM,GAAG,MAAM,UAAU,MAAM,CAAC,IAAI,iBACjE,gBAAgB;CAEpB;CAGA,MAAM,gBAAgB,UAAqC;EACzD,MAAM,QAAQ,UAAU;EACxB,IAAI,UAAU,QAAQ,MAAM,cAAc,MAAM,WAAW,gBAAgB;CAC7E;CAEA,MAAM,QAAiC;EACrC,MAAM,QAAQ;EACd,SAAS,QAAQ;EACjB;CACF;CAEA,MAAM,eAA8D;EAClE;EACA;EACA;EACA,aAAa;EACb,iBAAiB;EACjB,aAAa,SAAS,MAAM,IAAI;EAChC,gBAAgB,SAAS,MAAM,OAAO;EACtC,iBAAiB,SAAS,QAAQ;CACpC;CAEA,OAAO,UAAiE;EACtE,gBAAgB;EAChB;EACA;EACA;EAEA,QAAQ,OAAO;GAAE,oBAAoB;GAAQ,GAAG,kBAAkB,OAAO,CAAC;EAAE;EAE5E,WAAW,MACT,oBAAC,mBAAD;GAAmB,OAAO;GAAO,UAAA,kBAAkB,UAAU,CAAC;EAAqB,CAAA;EAErF,OAAO;EACP;EACA,KAAK;CACP,CAAC;AACH;AAEA,mBAAmB,cAAc"}
@@ -1,8 +1,41 @@
1
1
  "use client";
2
2
  import { createContext, use } from "react";
3
3
  //#region src/menu/menu-context.ts
4
+ const ANCHOR_PLACEMENT = { kind: "anchor" };
4
5
  const MenuContext = createContext(null);
5
6
  /**
7
+ * `true` inside a `Menu.ContextTrigger`: a `Menu.Root` there is a nested context region's own
8
+ * top-level menu (context-menu.md 2.4), not a misplaced submenu.
9
+ */
10
+ const MenuRegionContext = createContext(false);
11
+ /**
12
+ * `contextmenu` events a ContextTrigger handled (opened or moved its menu), with the top-level
13
+ * menu they belong to: that menu's document listener leaves its menu open for them
14
+ * (context-menu.md 5 and 14).
15
+ */
16
+ const handledEvents = /* @__PURE__ */ new WeakMap();
17
+ /**
18
+ * `pointerdown` events that are not a long-press of an enclosing region: claimed by the innermost
19
+ * region that started its timer for them (nested regions), and by every `Menu.Popup` for the
20
+ * presses inside it (a menu inside a region, review M1). As `contextmenu`'s `defaultPrevented`
21
+ * rule (context-menu.md 2.4).
22
+ */
23
+ const claimedPresses = /* @__PURE__ */ new WeakSet();
24
+ function claimPress(event) {
25
+ claimedPresses.add(event);
26
+ }
27
+ function isPressClaimed(event) {
28
+ return claimedPresses.has(event);
29
+ }
30
+ /** The top-level menu's identity: its popup ref object, stable for the Root's lifetime. */
31
+ const menuKey = (menu) => menu.root.popover.popupRef;
32
+ function markHandled(event, menu) {
33
+ handledEvents.set(event, menuKey(menu));
34
+ }
35
+ function isHandledBy(event, menu) {
36
+ return handledEvents.get(event) === menuKey(menu);
37
+ }
38
+ /**
6
39
  * The close an activated item runs once its action settled: the whole stack, through the
7
40
  * top-level Root (design 2.2). Recorded at activation: if the menu was dismissed and opened again
8
41
  * meanwhile, the close belongs to the previous opening and does nothing (as `useActionTrigger`).
@@ -23,6 +56,6 @@ const MenuListContext = createContext(null);
23
56
  const MenuGroupContext = createContext(null);
24
57
  const MenuRadioGroupContext = createContext(null);
25
58
  //#endregion
26
- export { MenuContext, MenuGroupContext, MenuListContext, MenuRadioGroupContext, closeStackLater, useMenuContext };
59
+ export { ANCHOR_PLACEMENT, MenuContext, MenuGroupContext, MenuListContext, MenuRadioGroupContext, MenuRegionContext, claimPress, closeStackLater, isHandledBy, isPressClaimed, markHandled, useMenuContext };
27
60
 
28
61
  //# sourceMappingURL=menu-context.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"menu-context.mjs","names":[],"sources":["../../src/menu/menu-context.ts"],"sourcesContent":["\"use client\";\nimport { createContext, use } from \"react\";\nimport type { DelayedPair } from \"../internal/use-delayed-pair.js\";\nimport type { PopoverRootValue } from \"../internal/use-popover-root.js\";\n\n/**\n * Where focus goes when the menu opens (design/components/menu.md 6.1): the first enabled item,\n * the last one (ArrowUp on the Trigger), or nowhere (a hover-opened submenu keeps focus on its\n * SubmenuTrigger).\n */\nexport type MenuOpenCause = \"first\" | \"last\" | \"none\";\n\n/** How the open being shown should place focus (design 6.1; review H1). */\nexport interface MenuOpenFocus {\n readonly cause: MenuOpenCause;\n /**\n * The open came from a pointer click (`detail > 0`): the item is focused without the focus\n * ring where the engine supports `focus({ focusVisible: false })`.\n */\n readonly pointer: boolean;\n}\n\n/** One menu level: what its Root provides to its Trigger, Popup and items. */\nexport interface MenuContextValue {\n /** The shared popover machinery (`usePopoverRoot`). */\n readonly popover: PopoverRootValue;\n /** This Root is nested in a `Menu.Popup`. */\n readonly submenu: boolean;\n /** The top-level menu: item activation closes the whole stack through it (design 2.2). */\n readonly root: MenuContextValue;\n /** The enclosing menu of a submenu; `null` at the top level. */\n readonly parent: MenuContextValue | null;\n readonly loop: boolean;\n readonly delay: number;\n readonly closeDelay: number;\n /** The Trigger's id (`aria-labelledby` of the Popup). */\n readonly triggerId: string;\n /** Record the focus target of the next open (and whether a pointer asked for it). */\n readonly setOpenCause: (cause: MenuOpenCause, pointer?: boolean) => void;\n /** Read and clear the focus target of the open being shown (default `\"first\"`, keyboard). */\n readonly takeOpenCause: () => MenuOpenFocus;\n /** Submenu hover-intent timers, shared by the SubmenuTrigger and the Popup. */\n readonly timers: DelayedPair;\n}\n\nexport const MenuContext = createContext<MenuContextValue | null>(null);\n\n/**\n * The close an activated item runs once its action settled: the whole stack, through the\n * top-level Root (design 2.2). Recorded at activation: if the menu was dismissed and opened again\n * meanwhile, the close belongs to the previous opening and does nothing (as `useActionTrigger`).\n */\nexport function closeStackLater(menu: MenuContextValue, event: Event): () => void {\n const { popover } = menu.root;\n const generation = popover.openGeneration();\n return () => {\n if (popover.openGeneration() === generation) popover.requestClose(event, \"activate\");\n };\n}\n\nexport function useMenuContext(part: string): MenuContextValue {\n const context = use(MenuContext);\n if (context === null) {\n throw new Error(`${part} must be rendered inside a Menu.Root.`);\n }\n return context;\n}\n\n/** What a `Menu.Popup` tells the items rendered in it. */\nexport interface MenuListContextValue {\n /** The menu this popup belongs to. */\n readonly menu: MenuContextValue;\n /**\n * `experimental_focusgroup` is on and the engine implements it: items render no `tabindex`\n * (Chromium leaves `tabindex=\"-1\"` elements out of a focusgroup) and the JS arrow keys step\n * aside.\n */\n readonly nativeFocusgroup: boolean;\n /** A typeahead buffer is active: Space is a character, not an activation (design 5.1). */\n readonly typingAhead: () => boolean;\n}\n\nexport const MenuListContext = createContext<MenuListContextValue | null>(null);\n\n/** `Menu.Group` / `Menu.RadioGroup`: the id their `Menu.GroupLabel` renders. */\nexport interface MenuGroupContextValue {\n readonly labelId: string;\n}\n\nexport const MenuGroupContext = createContext<MenuGroupContextValue | null>(null);\n\n/** `Menu.RadioGroup`'s state for its `Menu.RadioItem`s. */\nexport interface MenuRadioGroupContextValue {\n /** Optimistic value. */\n readonly value: string | null;\n readonly pending: boolean;\n /**\n * Dispatch `value` through the group's transition; `then` runs once the change action\n * resolved (an item's `closeOnActivate`).\n */\n readonly select: (value: string, event: Event, then: (() => void) | undefined) => void;\n}\n\nexport const MenuRadioGroupContext = createContext<MenuRadioGroupContextValue | null>(null);\n"],"mappings":";;;AA6CA,MAAa,cAAc,cAAuC,IAAI;;;;;;AAOtE,SAAgB,gBAAgB,MAAwB,OAA0B;CAChF,MAAM,EAAE,YAAY,KAAK;CACzB,MAAM,aAAa,QAAQ,eAAe;CAC1C,aAAa;EACX,IAAI,QAAQ,eAAe,MAAM,YAAY,QAAQ,aAAa,OAAO,UAAU;CACrF;AACF;AAEA,SAAgB,eAAe,MAAgC;CAC7D,MAAM,UAAU,IAAI,WAAW;CAC/B,IAAI,YAAY,MACd,MAAM,IAAI,MAAM,GAAG,KAAK,sCAAsC;CAEhE,OAAO;AACT;AAgBA,MAAa,kBAAkB,cAA2C,IAAI;AAO9E,MAAa,mBAAmB,cAA4C,IAAI;AAchF,MAAa,wBAAwB,cAAiD,IAAI"}
1
+ {"version":3,"file":"menu-context.mjs","names":[],"sources":["../../src/menu/menu-context.ts"],"sourcesContent":["\"use client\";\nimport { createContext, use } from \"react\";\nimport type { DelayedPair } from \"../internal/use-delayed-pair.js\";\nimport type { PopoverRootValue } from \"../internal/use-popover-root.js\";\n\n/**\n * Where focus goes when the menu opens (design/components/menu.md 6.1): the first enabled item,\n * the last one (ArrowUp on the Trigger), or nowhere (a hover-opened submenu keeps focus on its\n * SubmenuTrigger).\n */\nexport type MenuOpenCause = \"first\" | \"last\" | \"none\";\n\n/** How the open being shown should place focus (design 6.1; review H1). */\nexport interface MenuOpenFocus {\n readonly cause: MenuOpenCause;\n /**\n * The open came from a pointer click (`detail > 0`): the item is focused without the focus\n * ring where the engine supports `focus({ focusVisible: false })`.\n */\n readonly pointer: boolean;\n}\n\n/**\n * How the current open is placed (design/components/context-menu.md 3.1): against the Trigger\n * (`anchor`: a Menu.Trigger, a foreign invoker, a controlled or platform open), or at a point\n * (`point`: a Menu.ContextTrigger). `generation` grows with every point open or move, so a second\n * right-click at a new point is a state change while the menu is open (4.3).\n */\nexport type MenuPlacement =\n | { readonly kind: \"anchor\" }\n | {\n readonly kind: \"point\";\n readonly x: number;\n readonly y: number;\n readonly generation: number;\n /** The region that opened: the `source` of `showPopover({ source })` (nesting). */\n readonly source: HTMLElement;\n /** The pointer held down at the open, re-captured after a move's re-show (2.1). */\n readonly pointerId: number | undefined;\n };\n\nexport const ANCHOR_PLACEMENT: MenuPlacement = { kind: \"anchor\" };\n\n/** What a ContextTrigger passes to `openAt`. */\nexport interface MenuOpenAtOptions {\n readonly source: HTMLElement;\n /** The `contextmenu` event, or the long-press `pointerdown` (`details.event`). */\n readonly event: Event;\n /** A pointer opened (quiet focus); `false` for the keyboard (focus ring). */\n readonly pointer: boolean;\n readonly pointerId: number | undefined;\n}\n\n/** One menu level: what its Root provides to its Trigger, Popup and items. */\nexport interface MenuContextValue {\n /** The shared popover machinery (`usePopoverRoot`). */\n readonly popover: PopoverRootValue;\n /** This Root is nested in a `Menu.Popup`. */\n readonly submenu: boolean;\n /** The top-level menu: item activation closes the whole stack through it (design 2.2). */\n readonly root: MenuContextValue;\n /** The enclosing menu of a submenu; `null` at the top level. */\n readonly parent: MenuContextValue | null;\n readonly loop: boolean;\n readonly delay: number;\n readonly closeDelay: number;\n /** The Trigger's id (`aria-labelledby` of the Popup). */\n readonly triggerId: string;\n /** Record the focus target of the next open (and whether a pointer asked for it). */\n readonly setOpenCause: (cause: MenuOpenCause, pointer?: boolean) => void;\n /** Read and clear the focus target of the open being shown (default `\"first\"`, keyboard). */\n readonly takeOpenCause: () => MenuOpenFocus;\n /** Submenu hover-intent timers, shared by the SubmenuTrigger and the Popup. */\n readonly timers: DelayedPair;\n /** Placement of the current (or last) open (context-menu.md 3.1). */\n readonly placement: MenuPlacement;\n /**\n * A ContextTrigger opens the menu at `point`, or moves it there when it is already open (no\n * dispatch, 4.3).\n */\n readonly openAt: (\n point: { readonly x: number; readonly y: number },\n options: MenuOpenAtOptions,\n ) => void;\n /**\n * Back to the anchored placement once a point open has been shown and hidden (the popup is no\n * longer visible): the next open that is not a ContextTrigger's (controlled, platform, foreign\n * invoker) places against the Trigger. Ignored when a newer point was recorded meanwhile.\n */\n readonly resetPlacement: (generation: number) => void;\n}\n\nexport const MenuContext = createContext<MenuContextValue | null>(null);\n\n/**\n * `true` inside a `Menu.ContextTrigger`: a `Menu.Root` there is a nested context region's own\n * top-level menu (context-menu.md 2.4), not a misplaced submenu.\n */\nexport const MenuRegionContext = createContext(false);\n\n/**\n * `contextmenu` events a ContextTrigger handled (opened or moved its menu), with the top-level\n * menu they belong to: that menu's document listener leaves its menu open for them\n * (context-menu.md 5 and 14).\n */\nconst handledEvents = new WeakMap<Event, object>();\n\n/**\n * `pointerdown` events that are not a long-press of an enclosing region: claimed by the innermost\n * region that started its timer for them (nested regions), and by every `Menu.Popup` for the\n * presses inside it (a menu inside a region, review M1). As `contextmenu`'s `defaultPrevented`\n * rule (context-menu.md 2.4).\n */\nconst claimedPresses = new WeakSet<Event>();\n\nexport function claimPress(event: Event): void {\n claimedPresses.add(event);\n}\n\nexport function isPressClaimed(event: Event): boolean {\n return claimedPresses.has(event);\n}\n\n/** The top-level menu's identity: its popup ref object, stable for the Root's lifetime. */\nconst menuKey = (menu: MenuContextValue): object => menu.root.popover.popupRef;\n\nexport function markHandled(event: Event, menu: MenuContextValue): void {\n handledEvents.set(event, menuKey(menu));\n}\n\nexport function isHandledBy(event: Event, menu: MenuContextValue): boolean {\n return handledEvents.get(event) === menuKey(menu);\n}\n\n/**\n * The close an activated item runs once its action settled: the whole stack, through the\n * top-level Root (design 2.2). Recorded at activation: if the menu was dismissed and opened again\n * meanwhile, the close belongs to the previous opening and does nothing (as `useActionTrigger`).\n */\nexport function closeStackLater(menu: MenuContextValue, event: Event): () => void {\n const { popover } = menu.root;\n const generation = popover.openGeneration();\n return () => {\n if (popover.openGeneration() === generation) popover.requestClose(event, \"activate\");\n };\n}\n\nexport function useMenuContext(part: string): MenuContextValue {\n const context = use(MenuContext);\n if (context === null) {\n throw new Error(`${part} must be rendered inside a Menu.Root.`);\n }\n return context;\n}\n\n/** What a `Menu.Popup` tells the items rendered in it. */\nexport interface MenuListContextValue {\n /** The menu this popup belongs to. */\n readonly menu: MenuContextValue;\n /**\n * `experimental_focusgroup` is on and the engine implements it: items render no `tabindex`\n * (Chromium leaves `tabindex=\"-1\"` elements out of a focusgroup) and the JS arrow keys step\n * aside.\n */\n readonly nativeFocusgroup: boolean;\n /** A typeahead buffer is active: Space is a character, not an activation (design 5.1). */\n readonly typingAhead: () => boolean;\n}\n\nexport const MenuListContext = createContext<MenuListContextValue | null>(null);\n\n/** `Menu.Group` / `Menu.RadioGroup`: the id their `Menu.GroupLabel` renders. */\nexport interface MenuGroupContextValue {\n readonly labelId: string;\n}\n\nexport const MenuGroupContext = createContext<MenuGroupContextValue | null>(null);\n\n/** `Menu.RadioGroup`'s state for its `Menu.RadioItem`s. */\nexport interface MenuRadioGroupContextValue {\n /** Optimistic value. */\n readonly value: string | null;\n readonly pending: boolean;\n /**\n * Dispatch `value` through the group's transition; `then` runs once the change action\n * resolved (an item's `closeOnActivate`).\n */\n readonly select: (value: string, event: Event, then: (() => void) | undefined) => void;\n}\n\nexport const MenuRadioGroupContext = createContext<MenuRadioGroupContextValue | null>(null);\n"],"mappings":";;;AAyCA,MAAa,mBAAkC,EAAE,MAAM,SAAS;AAmDhE,MAAa,cAAc,cAAuC,IAAI;;;;;AAMtE,MAAa,oBAAoB,cAAc,KAAK;;;;;;AAOpD,MAAM,gCAAgB,IAAI,QAAuB;;;;;;;AAQjD,MAAM,iCAAiB,IAAI,QAAe;AAE1C,SAAgB,WAAW,OAAoB;CAC7C,eAAe,IAAI,KAAK;AAC1B;AAEA,SAAgB,eAAe,OAAuB;CACpD,OAAO,eAAe,IAAI,KAAK;AACjC;;AAGA,MAAM,WAAW,SAAmC,KAAK,KAAK,QAAQ;AAEtE,SAAgB,YAAY,OAAc,MAA8B;CACtE,cAAc,IAAI,OAAO,QAAQ,IAAI,CAAC;AACxC;AAEA,SAAgB,YAAY,OAAc,MAAiC;CACzE,OAAO,cAAc,IAAI,KAAK,MAAM,QAAQ,IAAI;AAClD;;;;;;AAOA,SAAgB,gBAAgB,MAAwB,OAA0B;CAChF,MAAM,EAAE,YAAY,KAAK;CACzB,MAAM,aAAa,QAAQ,eAAe;CAC1C,aAAa;EACX,IAAI,QAAQ,eAAe,MAAM,YAAY,QAAQ,aAAa,OAAO,UAAU;CACrF;AACF;AAEA,SAAgB,eAAe,MAAgC;CAC7D,MAAM,UAAU,IAAI,WAAW;CAC/B,IAAI,YAAY,MACd,MAAM,IAAI,MAAM,GAAG,KAAK,sCAAsC;CAEhE,OAAO;AACT;AAgBA,MAAa,kBAAkB,cAA2C,IAAI;AAO9E,MAAa,mBAAmB,cAA4C,IAAI;AAchF,MAAa,wBAAwB,cAAiD,IAAI"}
@@ -34,10 +34,46 @@ function typeaheadText(item) {
34
34
  function isRtl(element) {
35
35
  return getComputedStyle(element).direction === "rtl";
36
36
  }
37
+ /**
38
+ * `setPointerCapture`, which throws `NotFoundError` for a pointer the engine no longer tracks
39
+ * (context-menu.md 2.1). A silent no-op when no button is down.
40
+ */
41
+ function capturePointer(element, pointerId) {
42
+ if (element === null || pointerId === void 0) return;
43
+ try {
44
+ element.setPointerCapture(pointerId);
45
+ } catch {}
46
+ }
47
+ /** `<input>` types that take no text: their `contextmenu` is not an editing one. */
48
+ const NON_TEXT_INPUTS = /* @__PURE__ */ new Set([
49
+ "button",
50
+ "submit",
51
+ "reset",
52
+ "checkbox",
53
+ "radio",
54
+ "range",
55
+ "color",
56
+ "file",
57
+ "image",
58
+ "hidden"
59
+ ]);
60
+ /**
61
+ * An editable target keeps the browser's own context menu by default (context-menu.md 2.3): a
62
+ * text-taking `<input>`, a `<textarea>`, or editable content.
63
+ */
64
+ function isEditableTarget(target) {
65
+ if (target === null || target.nodeType !== 1) return false;
66
+ const element = target;
67
+ if (element.closest("textarea") !== null) return true;
68
+ const input = element.closest("input");
69
+ if (input !== null && !NON_TEXT_INPUTS.has(input.type)) return true;
70
+ if (typeof element.isContentEditable === "boolean") return element.isContentEditable;
71
+ return element.closest("[contenteditable]:not([contenteditable=\"false\"])") !== null;
72
+ }
37
73
  function hasModifier(event) {
38
74
  return event.altKey || event.ctrlKey || event.metaKey;
39
75
  }
40
76
  //#endregion
41
- export { edgeItem, focusItem, hasModifier, isDisabledItem, isMenuItem, isRtl, menuItems, typeaheadText };
77
+ export { capturePointer, edgeItem, focusItem, hasModifier, isDisabledItem, isEditableTarget, isMenuItem, isRtl, menuItems, typeaheadText };
42
78
 
43
79
  //# sourceMappingURL=menu-dom.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"menu-dom.mjs","names":[],"sources":["../../src/menu/menu-dom.ts"],"sourcesContent":["\"use client\";\n// DOM helpers shared by the Menu parts. Items are read from the DOM when they are needed (a key\n// press, an open), never registered (design/components/menu.md 13 and 14).\n\nconst ITEM_SELECTOR = '[role=\"menuitem\"], [role=\"menuitemcheckbox\"], [role=\"menuitemradio\"]';\n\n/** The items of `popup` in DOM order, without the items of its submenus. Disabled included. */\nexport function menuItems(popup: Element): HTMLElement[] {\n return Array.from(popup.querySelectorAll<HTMLElement>(ITEM_SELECTOR)).filter(\n (item) => item.closest('[role=\"menu\"]') === popup,\n );\n}\n\nexport function isMenuItem(element: Element, popup: Element): boolean {\n return element.matches(ITEM_SELECTOR) && element.closest('[role=\"menu\"]') === popup;\n}\n\n/**\n * Disabled by its `disabled` prop. Keys on `data-disabled`, not `aria-disabled`: an item is also\n * `aria-disabled` while its own action is pending (#81). A pending item is an ordinary item for\n * navigation; only the open focus (`edgeItem`) skips items with the `disabled` prop. Arrow keys,\n * Home / End and typeahead skip nothing.\n */\nexport function isDisabledItem(item: Element): boolean {\n return item.hasAttribute(\"data-disabled\");\n}\n\n/** The first (or last) enabled item; the first (or last) item if all are disabled. */\nexport function edgeItem(popup: Element, which: \"first\" | \"last\"): HTMLElement | null {\n const items = menuItems(popup);\n const ordered = which === \"first\" ? items : [...items].reverse();\n return ordered.find((item) => !isDisabledItem(item)) ?? ordered[0] ?? null;\n}\n\n/** Keyboard focus: the ring shows even where the engine's heuristic would not (as Tabs). */\nexport function focusItem(item: HTMLElement): void {\n item.focus({ focusVisible: true } as FocusOptions);\n}\n\n/** The text typeahead matches: the item's `label` prop, else its text content. */\nexport function typeaheadText(item: Element): string {\n return (item.getAttribute(\"data-typeahead\") ?? item.textContent ?? \"\").trim().toLowerCase();\n}\n\nexport function isRtl(element: Element): boolean {\n return getComputedStyle(element).direction === \"rtl\";\n}\n\nexport function hasModifier(event: { altKey: boolean; ctrlKey: boolean; metaKey: boolean }) {\n return event.altKey || event.ctrlKey || event.metaKey;\n}\n"],"mappings":";;AAIA,MAAM,gBAAgB;;AAGtB,SAAgB,UAAU,OAA+B;CACvD,OAAO,MAAM,KAAK,MAAM,iBAA8B,aAAa,CAAC,CAAC,CAAC,QACnE,SAAS,KAAK,QAAQ,iBAAe,MAAM,KAC9C;AACF;AAEA,SAAgB,WAAW,SAAkB,OAAyB;CACpE,OAAO,QAAQ,QAAQ,aAAa,KAAK,QAAQ,QAAQ,iBAAe,MAAM;AAChF;;;;;;;AAQA,SAAgB,eAAe,MAAwB;CACrD,OAAO,KAAK,aAAa,eAAe;AAC1C;;AAGA,SAAgB,SAAS,OAAgB,OAA6C;CACpF,MAAM,QAAQ,UAAU,KAAK;CAC7B,MAAM,UAAU,UAAU,UAAU,QAAQ,CAAC,GAAG,KAAK,CAAC,CAAC,QAAQ;CAC/D,OAAO,QAAQ,MAAM,SAAS,CAAC,eAAe,IAAI,CAAC,KAAK,QAAQ,MAAM;AACxE;;AAGA,SAAgB,UAAU,MAAyB;CACjD,KAAK,MAAM,EAAE,cAAc,KAAK,CAAiB;AACnD;;AAGA,SAAgB,cAAc,MAAuB;CACnD,QAAQ,KAAK,aAAa,gBAAgB,KAAK,KAAK,eAAe,GAAA,CAAI,KAAK,CAAC,CAAC,YAAY;AAC5F;AAEA,SAAgB,MAAM,SAA2B;CAC/C,OAAO,iBAAiB,OAAO,CAAC,CAAC,cAAc;AACjD;AAEA,SAAgB,YAAY,OAAgE;CAC1F,OAAO,MAAM,UAAU,MAAM,WAAW,MAAM;AAChD"}
1
+ {"version":3,"file":"menu-dom.mjs","names":[],"sources":["../../src/menu/menu-dom.ts"],"sourcesContent":["\"use client\";\n// DOM helpers shared by the Menu parts. Items are read from the DOM when they are needed (a key\n// press, an open), never registered (design/components/menu.md 13 and 14).\n\nconst ITEM_SELECTOR = '[role=\"menuitem\"], [role=\"menuitemcheckbox\"], [role=\"menuitemradio\"]';\n\n/** The items of `popup` in DOM order, without the items of its submenus. Disabled included. */\nexport function menuItems(popup: Element): HTMLElement[] {\n return Array.from(popup.querySelectorAll<HTMLElement>(ITEM_SELECTOR)).filter(\n (item) => item.closest('[role=\"menu\"]') === popup,\n );\n}\n\nexport function isMenuItem(element: Element, popup: Element): boolean {\n return element.matches(ITEM_SELECTOR) && element.closest('[role=\"menu\"]') === popup;\n}\n\n/**\n * Disabled by its `disabled` prop. Keys on `data-disabled`, not `aria-disabled`: an item is also\n * `aria-disabled` while its own action is pending (#81). A pending item is an ordinary item for\n * navigation; only the open focus (`edgeItem`) skips items with the `disabled` prop. Arrow keys,\n * Home / End and typeahead skip nothing.\n */\nexport function isDisabledItem(item: Element): boolean {\n return item.hasAttribute(\"data-disabled\");\n}\n\n/** The first (or last) enabled item; the first (or last) item if all are disabled. */\nexport function edgeItem(popup: Element, which: \"first\" | \"last\"): HTMLElement | null {\n const items = menuItems(popup);\n const ordered = which === \"first\" ? items : [...items].reverse();\n return ordered.find((item) => !isDisabledItem(item)) ?? ordered[0] ?? null;\n}\n\n/** Keyboard focus: the ring shows even where the engine's heuristic would not (as Tabs). */\nexport function focusItem(item: HTMLElement): void {\n item.focus({ focusVisible: true } as FocusOptions);\n}\n\n/** The text typeahead matches: the item's `label` prop, else its text content. */\nexport function typeaheadText(item: Element): string {\n return (item.getAttribute(\"data-typeahead\") ?? item.textContent ?? \"\").trim().toLowerCase();\n}\n\nexport function isRtl(element: Element): boolean {\n return getComputedStyle(element).direction === \"rtl\";\n}\n\n/**\n * `setPointerCapture`, which throws `NotFoundError` for a pointer the engine no longer tracks\n * (context-menu.md 2.1). A silent no-op when no button is down.\n */\nexport function capturePointer(element: Element | null, pointerId: number | undefined): void {\n if (element === null || pointerId === undefined) return;\n try {\n element.setPointerCapture(pointerId);\n } catch {\n // Not an active pointer (a synthetic event, a released button): nothing to capture.\n }\n}\n\n/** `<input>` types that take no text: their `contextmenu` is not an editing one. */\nconst NON_TEXT_INPUTS = new Set([\n \"button\",\n \"submit\",\n \"reset\",\n \"checkbox\",\n \"radio\",\n \"range\",\n \"color\",\n \"file\",\n \"image\",\n \"hidden\",\n]);\n\n/**\n * An editable target keeps the browser's own context menu by default (context-menu.md 2.3): a\n * text-taking `<input>`, a `<textarea>`, or editable content.\n */\nexport function isEditableTarget(target: EventTarget | null): boolean {\n if (target === null || (target as Node).nodeType !== 1) return false;\n const element = target as HTMLElement;\n if (element.closest(\"textarea\") !== null) return true;\n const input = element.closest(\"input\");\n if (input !== null && !NON_TEXT_INPUTS.has(input.type)) return true;\n // `isContentEditable` where implemented (every engine; not jsdom).\n if (typeof element.isContentEditable === \"boolean\") return element.isContentEditable;\n return element.closest('[contenteditable]:not([contenteditable=\"false\"])') !== null;\n}\n\nexport function hasModifier(event: { altKey: boolean; ctrlKey: boolean; metaKey: boolean }) {\n return event.altKey || event.ctrlKey || event.metaKey;\n}\n"],"mappings":";;AAIA,MAAM,gBAAgB;;AAGtB,SAAgB,UAAU,OAA+B;CACvD,OAAO,MAAM,KAAK,MAAM,iBAA8B,aAAa,CAAC,CAAC,CAAC,QACnE,SAAS,KAAK,QAAQ,iBAAe,MAAM,KAC9C;AACF;AAEA,SAAgB,WAAW,SAAkB,OAAyB;CACpE,OAAO,QAAQ,QAAQ,aAAa,KAAK,QAAQ,QAAQ,iBAAe,MAAM;AAChF;;;;;;;AAQA,SAAgB,eAAe,MAAwB;CACrD,OAAO,KAAK,aAAa,eAAe;AAC1C;;AAGA,SAAgB,SAAS,OAAgB,OAA6C;CACpF,MAAM,QAAQ,UAAU,KAAK;CAC7B,MAAM,UAAU,UAAU,UAAU,QAAQ,CAAC,GAAG,KAAK,CAAC,CAAC,QAAQ;CAC/D,OAAO,QAAQ,MAAM,SAAS,CAAC,eAAe,IAAI,CAAC,KAAK,QAAQ,MAAM;AACxE;;AAGA,SAAgB,UAAU,MAAyB;CACjD,KAAK,MAAM,EAAE,cAAc,KAAK,CAAiB;AACnD;;AAGA,SAAgB,cAAc,MAAuB;CACnD,QAAQ,KAAK,aAAa,gBAAgB,KAAK,KAAK,eAAe,GAAA,CAAI,KAAK,CAAC,CAAC,YAAY;AAC5F;AAEA,SAAgB,MAAM,SAA2B;CAC/C,OAAO,iBAAiB,OAAO,CAAC,CAAC,cAAc;AACjD;;;;;AAMA,SAAgB,eAAe,SAAyB,WAAqC;CAC3F,IAAI,YAAY,QAAQ,cAAc,KAAA,GAAW;CACjD,IAAI;EACF,QAAQ,kBAAkB,SAAS;CACrC,QAAQ,CAER;AACF;;AAGA,MAAM,kCAAkB,IAAI,IAAI;CAC9B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;;;;;AAMD,SAAgB,iBAAiB,QAAqC;CACpE,IAAI,WAAW,QAAS,OAAgB,aAAa,GAAG,OAAO;CAC/D,MAAM,UAAU;CAChB,IAAI,QAAQ,QAAQ,UAAU,MAAM,MAAM,OAAO;CACjD,MAAM,QAAQ,QAAQ,QAAQ,OAAO;CACrC,IAAI,UAAU,QAAQ,CAAC,gBAAgB,IAAI,MAAM,IAAI,GAAG,OAAO;CAE/D,IAAI,OAAO,QAAQ,sBAAsB,WAAW,OAAO,QAAQ;CACnE,OAAO,QAAQ,QAAQ,oDAAkD,MAAM;AACjF;AAEA,SAAgB,YAAY,OAAgE;CAC1F,OAAO,MAAM,UAAU,MAAM,WAAW,MAAM;AAChD"}
@@ -7,17 +7,26 @@ export type MenuAlign = AnchorAlign;
7
7
  export interface MenuPopupState {
8
8
  /** Committed open state. Read `:popover-open` for the DOM truth. */
9
9
  readonly open: boolean;
10
+ /** `null` with `side={null}`, and while the menu is placed at a point (`context`). */
10
11
  readonly side: MenuSide | null;
11
12
  readonly align: MenuAlign;
12
13
  /** This popup belongs to a submenu. */
13
14
  readonly submenu: boolean;
15
+ /**
16
+ * The current open came from a `Menu.ContextTrigger`: the popup is placed at the pointer, with
17
+ * `--menu-point-x` / `--menu-point-y` set inline (`data-context`).
18
+ */
19
+ readonly context: boolean;
14
20
  }
15
- export interface MenuPopupProps extends Omit<ComponentPropsWithRef<"div">, "id" | "popover" | "role" | "aria-labelledby" | "onKeyDown" | "onFocus" | "onBlur" | "onMouseDown" | "onPointerEnter" | "onPointerLeave" | "onToggle" | "onBeforeToggle" | "className" | "style" | "children"> {
21
+ export interface MenuPopupProps extends Omit<ComponentPropsWithRef<"div">, "id" | "popover" | "role" | "aria-labelledby" | "onKeyDown" | "onFocus" | "onBlur" | "onMouseDown" | "onContextMenu" | "onPointerEnter" | "onPointerLeave" | "onToggle" | "onBeforeToggle" | "className" | "style" | "children"> {
16
22
  /**
17
23
  * Placement against the Trigger through inline `position-area`, with `position-try-fallbacks`
18
24
  * flipping it when it does not fit. Default `"bottom"` for a top-level menu, `"right"` for a
19
25
  * submenu (whose fallback is `flip-inline` only). `null` emits no inline positioning (and no
20
- * `data-side` / `data-align`): the stylesheet owns placement.
26
+ * `data-side` / `data-align`): the stylesheet owns placement. Ignored for an open from a
27
+ * `Menu.ContextTrigger`, which is placed at the pointer; there `null` also opts out of the
28
+ * inline point placement and the library's `@position-try` rules, leaving only
29
+ * `--menu-point-x` / `--menu-point-y`.
21
30
  */
22
31
  side?: MenuSide | null;
23
32
  /** Alignment along the side. Default `"start"`. Ignored when `side` is `null`. */
@@ -58,8 +67,13 @@ export interface MenuPopupProps extends Omit<ComponentPropsWithRef<"div">, "id"
58
67
  *
59
68
  * Owned keys (conventions 1.2): `id`, `popover`, `role`, `aria-labelledby` (token-merged),
60
69
  * `tabIndex`, `focusgroup` (with the flag), `onKeyDown`, `onBlur`, `onMouseDown` (a press on
61
- * non-item space keeps focus on the current item), `onPointerEnter` / `onPointerLeave`, and
62
- * `data-side` / `data-align` unless `side` is `null`.
70
+ * non-item space keeps focus on the current item), `onContextMenu` (no native menu over this
71
+ * one), `onPointerEnter` / `onPointerLeave`, `data-side` / `data-align` unless `side` is `null`
72
+ * or the menu is placed at a point, and `data-context` for an open from a `Menu.ContextTrigger`
73
+ * (placed at the pointer: `--menu-point-x` / `--menu-point-y` inline, and unless `side` is
74
+ * `null`, `left` / `top` at the point with the library's `@position-try` fallbacks, whose rules
75
+ * it renders as a hoisted `<style>`). A top-level menu closes at once on a right- or
76
+ * middle-click outside it.
63
77
  */
64
78
  export declare function MenuPopup({ side: sideProp, align, keepMounted, experimental_focusgroup, transition, className, style, children, render, ...rest }: MenuPopupProps): ReactNode;
65
79
  export declare namespace MenuPopup {
@@ -1 +1 @@
1
- {"version":3,"file":"menu-popup.d.mts","names":[],"sources":["../../src/menu/menu-popup.tsx"],"mappings":";;;;YAkCY,WAAW;YACX,YAAY;iBAEP;;WAEN;WACA,MAAM;WACN,OAAO;;WAEP;;iBAGM,uBAAuB,KACtC;;;;;;;EAuBA,OAAO;;EAEP,QAAQ;;;;;;EAMR;;;;;;;EAOA;;;;;;;EAOA,aAAa;EACb,YAAY,+BAA+B;EAC3C,QAAQ,WAAW,2BAA2B;EAC9C,WAAW,WAAW,WAAW;;EAEjC,SAAS,WAAW,8BAA8B;;;;;;;;;;;;;;;wBA4BpC,YACd,MAAM,UACN,OACA,aACA,yBACA,YACA,WACA,OACA,UACA,WACG,QACF,iBAAc;yBAXD"}
1
+ {"version":3,"file":"menu-popup.d.mts","names":[],"sources":["../../src/menu/menu-popup.tsx"],"mappings":";;;;YAoDY,WAAW;YACX,YAAY;iBAEP;;WAEN;;WAEA,MAAM;WACN,OAAO;;WAEP;;;;;WAKA;;iBAGM,uBAAuB,KACtC;;;;;;;;;;EA2BA,OAAO;;EAEP,QAAQ;;;;;;EAMR;;;;;;;EAOA;;;;;;;EAOA,aAAa;EACb,YAAY,+BAA+B;EAC3C,QAAQ,WAAW,2BAA2B;EAC9C,WAAW,WAAW,WAAW;;EAEjC,SAAS,WAAW,8BAA8B;;;;;;;;;;;;;;;;;;;;wBAsFpC,YACd,MAAM,UACN,OACA,aACA,yBACA,YACA,WACA,OACA,UACA,WACG,QACF,iBAAc;yBAXD"}
@@ -4,13 +4,13 @@ import { useTransitionBoundary } from "../internal/transition-boundary.mjs";
4
4
  import { mergeRefs } from "../internal/merge-props.mjs";
5
5
  import { resolveStateValue } from "../internal/render-element.mjs";
6
6
  import { useRender } from "../internal/use-render.mjs";
7
- import { POSITION_TRY_FALLBACKS, positionArea } from "../internal/anchor-position.mjs";
7
+ import { FSUI_PRECEDENCE, MENU_POINT_FALLBACKS, MENU_POINT_HREF, MENU_POINT_RULES, MENU_POINT_X, MENU_POINT_Y, POSITION_TRY_FALLBACKS, positionArea } from "../internal/anchor-position.mjs";
8
8
  import { usePopoverPopup } from "../internal/use-popover-popup.mjs";
9
- import { MenuListContext, useMenuContext } from "./menu-context.mjs";
10
- import { edgeItem, focusItem, hasModifier, isMenuItem, isRtl, menuItems, typeaheadText } from "./menu-dom.mjs";
9
+ import { MenuListContext, claimPress, isHandledBy, useMenuContext } from "./menu-context.mjs";
10
+ import { capturePointer, edgeItem, focusItem, hasModifier, isMenuItem, isRtl, menuItems, typeaheadText } from "./menu-dom.mjs";
11
11
  import { scheduleHoverClose } from "./menu-hover.mjs";
12
- import { Activity, useCallback, useMemo, useRef, useSyncExternalStore } from "react";
13
- import { jsx } from "react/jsx-runtime";
12
+ import { Activity, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useSyncExternalStore } from "react";
13
+ import { jsx, jsxs } from "react/jsx-runtime";
14
14
  //#region src/menu/menu-popup.tsx
15
15
  /** Engine capabilities never change while the page lives. */
16
16
  const subscribeNever = () => () => {};
@@ -20,6 +20,57 @@ const isDev = process.env.NODE_ENV !== "production";
20
20
  /** What a press may focus inside a popup: items, the popup itself, the consumer's controls. */
21
21
  const FOCUSABLE_SELECTOR = "a[href], button, input, select, textarea, summary, [tabindex], [contenteditable]:not([contenteditable=\"false\"])";
22
22
  /**
23
+ * The inline placement of a point open (context-menu.md 3.3): the point as two custom
24
+ * properties, and unless the stylesheet owns placement, `left` / `top` at it with the other two
25
+ * insets `auto` (longhands, so React never sees a shorthand next to its longhands),
26
+ * `position-area: none` (a consumer's anchored `position-area` would otherwise place the popup
27
+ * inside a grid area) and the library's fallbacks, spelled by generation parity (4.3).
28
+ */
29
+ function pointStyle(point, stylesheetOwned) {
30
+ const vars = {
31
+ [MENU_POINT_X]: `${point.x}px`,
32
+ [MENU_POINT_Y]: `${point.y}px`
33
+ };
34
+ if (stylesheetOwned) return vars;
35
+ return {
36
+ ...vars,
37
+ top: `var(${MENU_POINT_Y})`,
38
+ right: "auto",
39
+ bottom: "auto",
40
+ left: `var(${MENU_POINT_X})`,
41
+ positionArea: "none",
42
+ positionTryFallbacks: MENU_POINT_FALLBACKS[point.generation % 2]
43
+ };
44
+ }
45
+ /**
46
+ * The outside right- and middle-click close of a top-level menu (context-menu.md 5): a
47
+ * `contextmenu` outside the menu tree that this menu's ContextTrigger did not handle, or a
48
+ * middle-button `pointerdown` outside it, hides the root popup at once (`"dismiss"` through the
49
+ * `toggle` notification). Chromium's click-based light dismiss ignores those buttons. Returns the
50
+ * cleanup.
51
+ */
52
+ function listenOutside(popup, menu) {
53
+ const doc = popup.ownerDocument;
54
+ const inside = (target) => target instanceof Node && popup.contains(target);
55
+ const close = () => {
56
+ if (popup.matches(":popover-open")) popup.hidePopover();
57
+ };
58
+ const onContextMenu = (event) => {
59
+ if (inside(event.target) || isHandledBy(event, menu)) return;
60
+ close();
61
+ };
62
+ const onPointerDown = (event) => {
63
+ if (event.button !== 1 || inside(event.target)) return;
64
+ close();
65
+ };
66
+ doc.addEventListener("contextmenu", onContextMenu);
67
+ doc.addEventListener("pointerdown", onPointerDown, true);
68
+ return () => {
69
+ doc.removeEventListener("contextmenu", onContextMenu);
70
+ doc.removeEventListener("pointerdown", onPointerDown, true);
71
+ };
72
+ }
73
+ /**
23
74
  * The menu itself: `<div popover="auto" role="menu" aria-labelledby={trigger} tabindex="-1">`,
24
75
  * placed against its Trigger with CSS Anchor Positioning. Owns the keyboard model (design 5):
25
76
  * ArrowUp / ArrowDown / Home / End move focus between items (or the browser does, with
@@ -29,13 +80,20 @@ const FOCUSABLE_SELECTOR = "a[href], button, input, select, textarea, summary, [
29
80
  *
30
81
  * Owned keys (conventions 1.2): `id`, `popover`, `role`, `aria-labelledby` (token-merged),
31
82
  * `tabIndex`, `focusgroup` (with the flag), `onKeyDown`, `onBlur`, `onMouseDown` (a press on
32
- * non-item space keeps focus on the current item), `onPointerEnter` / `onPointerLeave`, and
33
- * `data-side` / `data-align` unless `side` is `null`.
83
+ * non-item space keeps focus on the current item), `onContextMenu` (no native menu over this
84
+ * one), `onPointerEnter` / `onPointerLeave`, `data-side` / `data-align` unless `side` is `null`
85
+ * or the menu is placed at a point, and `data-context` for an open from a `Menu.ContextTrigger`
86
+ * (placed at the pointer: `--menu-point-x` / `--menu-point-y` inline, and unless `side` is
87
+ * `null`, `left` / `top` at the point with the library's `@position-try` fallbacks, whose rules
88
+ * it renders as a hoisted `<style>`). A top-level menu closes at once on a right- or
89
+ * middle-click outside it.
34
90
  */
35
91
  function MenuPopup({ side: sideProp, align = "start", keepMounted, experimental_focusgroup = false, transition, className, style, children, render, ...rest }) {
36
92
  const menu = useMenuContext("Menu.Popup");
37
93
  const { popover, submenu, loop } = menu;
38
- const side = sideProp === void 0 ? submenu ? "right" : "bottom" : sideProp;
94
+ const sideOption = sideProp === void 0 ? submenu ? "right" : "bottom" : sideProp;
95
+ const point = menu.placement.kind === "point" ? menu.placement : null;
96
+ const side = point !== null ? null : sideOption;
39
97
  const nativeFocusgroup = useSyncExternalStore(subscribeNever, () => experimental_focusgroup && "focusGroup" in HTMLElement.prototype, () => false);
40
98
  const typeahead = useRef({
41
99
  text: "",
@@ -45,7 +103,13 @@ function MenuPopup({ side: sideProp, align = "start", keepMounted, experimental_
45
103
  const { text, at } = typeahead.current;
46
104
  return text !== "" && Date.now() - at <= TYPEAHEAD_RESET_MS;
47
105
  }, []);
48
- const { setElement, visible, wrapContent } = usePopoverPopup(popover, {
106
+ const outside = useRef(null);
107
+ const stopOutside = useCallback(() => {
108
+ outside.current?.();
109
+ outside.current = null;
110
+ }, []);
111
+ useEffect(() => stopOutside, [stopOutside]);
112
+ const { setElement, visible, wrapContent, reshow } = usePopoverPopup(popover, {
49
113
  part: "Menu.Popup",
50
114
  initialFocus: void 0,
51
115
  resolveInitialFocus: (popup) => {
@@ -58,11 +122,33 @@ function MenuPopup({ side: sideProp, align = "start", keepMounted, experimental_
58
122
  } : element;
59
123
  },
60
124
  keepMounted,
61
- transition
125
+ transition,
126
+ source: () => point !== null ? point.source : popover.triggerRef.current,
127
+ onToggle: (event) => {
128
+ if (submenu) return;
129
+ const element = popover.popupRef.current;
130
+ if (event.newState === "open" && element !== null && element.matches(":popover-open")) outside.current ??= listenOutside(element, menu);
131
+ else if (event.newState === "closed") stopOutside();
132
+ }
133
+ });
134
+ const shownKey = useRef(null);
135
+ useLayoutEffect(() => {
136
+ const element = popover.popupRef.current;
137
+ if (element === null) return;
138
+ const key = point !== null ? point.generation : "anchor";
139
+ if (element.matches(":popover-open")) {
140
+ if (shownKey.current !== null && shownKey.current !== key && reshow()) capturePointer(element, point?.pointerId);
141
+ shownKey.current = key;
142
+ } else {
143
+ shownKey.current = null;
144
+ if (!visible && point !== null && !popover.pending && !popover.intended()) menu.resetPlacement(point.generation);
145
+ }
62
146
  });
63
147
  const devRef = useCallback((element) => {
64
148
  if (isDev && element.tagName === "DIALOG") console.error("Menu.Popup: rendered a <dialog>. Render a <div>: a <dialog popover> runs the dialog focusing steps, which fight the menu's item focus.");
65
- return () => {};
149
+ const onPointerDown = (event) => claimPress(event);
150
+ element.addEventListener("pointerdown", onPointerDown);
151
+ return () => element.removeEventListener("pointerdown", onPointerDown);
66
152
  }, []);
67
153
  const ref = useMemo(() => mergeRefs(setElement, devRef), [setElement, devRef]);
68
154
  const listContext = useMemo(() => ({
@@ -78,7 +164,8 @@ function MenuPopup({ side: sideProp, align = "start", keepMounted, experimental_
78
164
  open: popover.committed,
79
165
  side,
80
166
  align,
81
- submenu
167
+ submenu,
168
+ context: point !== null
82
169
  };
83
170
  const runTypeahead = (popup, current, key) => {
84
171
  const now = Date.now();
@@ -160,6 +247,9 @@ function MenuPopup({ side: sideProp, align = "start", keepMounted, experimental_
160
247
  onKeyDown,
161
248
  onBlur,
162
249
  onMouseDown,
250
+ onContextMenu: (event) => {
251
+ event.preventDefault();
252
+ },
163
253
  ...submenu && {
164
254
  onPointerEnter: (event) => {
165
255
  if (event.pointerType !== "touch") menu.timers.cancelClose();
@@ -171,8 +261,14 @@ function MenuPopup({ side: sideProp, align = "start", keepMounted, experimental_
171
261
  ...side !== null && {
172
262
  "data-side": side,
173
263
  "data-align": align
174
- }
264
+ },
265
+ "data-context": point !== null ? "" : void 0
175
266
  };
267
+ const pointRules = point !== null && sideOption !== null ? /* @__PURE__ */ jsx("style", {
268
+ href: MENU_POINT_HREF,
269
+ precedence: FSUI_PRECEDENCE,
270
+ children: MENU_POINT_RULES
271
+ }) : null;
176
272
  const content = (s) => {
177
273
  let node;
178
274
  if (keepMounted === true) node = /* @__PURE__ */ jsx(Activity, {
@@ -180,12 +276,12 @@ function MenuPopup({ side: sideProp, align = "start", keepMounted, experimental_
180
276
  children: wrapContent(resolveStateValue(children, s))
181
277
  });
182
278
  else node = visible ? wrapContent(resolveStateValue(children, s)) : null;
183
- return /* @__PURE__ */ jsx(MenuListContext, {
279
+ return /* @__PURE__ */ jsxs(MenuListContext, {
184
280
  value: listContext,
185
- children: /* @__PURE__ */ jsx(ToolbarContext, {
281
+ children: [pointRules, /* @__PURE__ */ jsx(ToolbarContext, {
186
282
  value: null,
187
283
  children: node
188
- })
284
+ })]
189
285
  });
190
286
  };
191
287
  const element = useRender({
@@ -193,7 +289,10 @@ function MenuPopup({ side: sideProp, align = "start", keepMounted, experimental_
193
289
  render,
194
290
  state,
195
291
  className,
196
- style: side === null ? style : (s) => ({
292
+ style: point !== null ? (s) => ({
293
+ ...pointStyle(point, sideOption === null),
294
+ ...resolveStateValue(style, s)
295
+ }) : side === null ? style : (s) => ({
197
296
  positionArea: positionArea(side, align),
198
297
  positionTryFallbacks: submenu ? "flip-inline" : POSITION_TRY_FALLBACKS,
199
298
  ...resolveStateValue(style, s)
@@ -1 +1 @@
1
- {"version":3,"file":"menu-popup.mjs","names":[],"sources":["../../src/menu/menu-popup.tsx"],"sourcesContent":["\"use client\";\nimport { Activity, useCallback, useMemo, useRef, useSyncExternalStore } from \"react\";\nimport type {\n ComponentPropsWithRef,\n CSSProperties,\n FocusEvent,\n KeyboardEvent,\n MouseEvent,\n PointerEvent,\n ReactNode,\n ViewTransitionClass,\n} from \"react\";\nimport { POSITION_TRY_FALLBACKS, positionArea } from \"../internal/anchor-position.js\";\nimport type { AnchorAlign, AnchorSide } from \"../internal/anchor-position.js\";\nimport { mergeRefs } from \"../internal/merge-props.js\";\nimport { ToolbarContext } from \"../internal/toolbar-context.js\";\nimport { useTransitionBoundary } from \"../internal/transition-boundary.js\";\nimport type { RenderProp, StateValue } from \"../internal/types.js\";\nimport { usePopoverPopup } from \"../internal/use-popover-popup.js\";\nimport { resolveStateValue, useRender } from \"../internal/use-render.js\";\nimport type { DataAttributes } from \"../internal/use-render.js\";\nimport { MenuListContext, useMenuContext } from \"./menu-context.js\";\nimport type { MenuListContextValue } from \"./menu-context.js\";\nimport {\n edgeItem,\n focusItem,\n hasModifier,\n isMenuItem,\n isRtl,\n menuItems,\n typeaheadText,\n} from \"./menu-dom.js\";\nimport { scheduleHoverClose } from \"./menu-hover.js\";\n\nexport type MenuSide = AnchorSide;\nexport type MenuAlign = AnchorAlign;\n\nexport interface MenuPopupState {\n /** Committed open state. Read `:popover-open` for the DOM truth. */\n readonly open: boolean;\n readonly side: MenuSide | null;\n readonly align: MenuAlign;\n /** This popup belongs to a submenu. */\n readonly submenu: boolean;\n}\n\nexport interface MenuPopupProps extends Omit<\n ComponentPropsWithRef<\"div\">,\n | \"id\"\n | \"popover\"\n | \"role\"\n | \"aria-labelledby\"\n | \"onKeyDown\"\n | \"onFocus\"\n | \"onBlur\"\n | \"onMouseDown\"\n | \"onPointerEnter\"\n | \"onPointerLeave\"\n | \"onToggle\"\n | \"onBeforeToggle\"\n | \"className\"\n | \"style\"\n | \"children\"\n> {\n /**\n * Placement against the Trigger through inline `position-area`, with `position-try-fallbacks`\n * flipping it when it does not fit. Default `\"bottom\"` for a top-level menu, `\"right\"` for a\n * submenu (whose fallback is `flip-inline` only). `null` emits no inline positioning (and no\n * `data-side` / `data-align`): the stylesheet owns placement.\n */\n side?: MenuSide | null;\n /** Alignment along the side. Default `\"start\"`. Ignored when `side` is `null`. */\n align?: MenuAlign;\n /**\n * `false` (default): the items are not rendered while the menu is closed; mounting them is part\n * of the opening transition, so items that suspend hold the open (and the Trigger pending)\n * until they are ready. `true`: they stay mounted inside `<Activity mode=\"hidden\">`.\n */\n keepMounted?: boolean;\n /**\n * Experimental (Chromium 150+). Renders `focusgroup=\"menu wrap|nowrap\"` and, where the engine\n * implements `focusGroup`, lets the browser handle ArrowUp / ArrowDown / Home / End: items then\n * render no `tabindex` and the library's arrow-key handling steps aside. Typeahead, activation,\n * submenu keys and Tab stay in JS. No effect in other engines. Default `false`.\n */\n experimental_focusgroup?: boolean;\n /**\n * Forwarded to the `<ViewTransition default>` around the popup element. Animates the opening\n * commit and every library-driven close (an item's activation, the Trigger, ArrowLeft, hover\n * leaving a submenu, controlled `open={false}`) in all engines; dismissals (Escape, a click\n * outside, Tab) animate with CSS only. Without it the part renders no `<ViewTransition>`.\n */\n transition?: ViewTransitionClass;\n className?: StateValue<string | undefined, MenuPopupState>;\n style?: StateValue<CSSProperties | undefined, MenuPopupState>;\n children?: StateValue<ReactNode, MenuPopupState>;\n /** Should render a `<div>`; a `<dialog>` would run the dialog focusing steps. */\n render?: RenderProp<ComponentPropsWithRef<\"div\">, MenuPopupState>;\n}\n\n/** Engine capabilities never change while the page lives. */\nconst subscribeNever = () => () => {};\n\n/** Typeahead buffer lifetime after the last keystroke (design 5.1). */\nconst TYPEAHEAD_RESET_MS = 500;\n\nconst isDev = process.env.NODE_ENV !== \"production\";\n\n/** What a press may focus inside a popup: items, the popup itself, the consumer's controls. */\nconst FOCUSABLE_SELECTOR =\n 'a[href], button, input, select, textarea, summary, [tabindex], [contenteditable]:not([contenteditable=\"false\"])';\n\n/**\n * The menu itself: `<div popover=\"auto\" role=\"menu\" aria-labelledby={trigger} tabindex=\"-1\">`,\n * placed against its Trigger with CSS Anchor Positioning. Owns the keyboard model (design 5):\n * ArrowUp / ArrowDown / Home / End move focus between items (or the browser does, with\n * `experimental_focusgroup` where supported), printable characters run typeahead, ArrowLeft\n * closes a submenu. The top-level popup closes the whole menu at once when focus leaves it (Tab).\n * Style it with `:popover-open`; it emits `data-side` / `data-align` but no `data-open`.\n *\n * Owned keys (conventions 1.2): `id`, `popover`, `role`, `aria-labelledby` (token-merged),\n * `tabIndex`, `focusgroup` (with the flag), `onKeyDown`, `onBlur`, `onMouseDown` (a press on\n * non-item space keeps focus on the current item), `onPointerEnter` / `onPointerLeave`, and\n * `data-side` / `data-align` unless `side` is `null`.\n */\nexport function MenuPopup({\n side: sideProp,\n align = \"start\",\n keepMounted,\n experimental_focusgroup = false,\n transition,\n className,\n style,\n children,\n render,\n ...rest\n}: MenuPopupProps) {\n const menu = useMenuContext(\"Menu.Popup\");\n const { popover, submenu, loop } = menu;\n const side = sideProp === undefined ? (submenu ? \"right\" : \"bottom\") : sideProp;\n\n // As Tabs.List (#43): detected only with the flag, never during SSR or hydration.\n const nativeFocusgroup = useSyncExternalStore(\n subscribeNever,\n () => experimental_focusgroup && \"focusGroup\" in HTMLElement.prototype,\n () => false,\n );\n\n const typeahead = useRef({ text: \"\", at: 0 });\n const typingAhead = useCallback(() => {\n const { text, at } = typeahead.current;\n return text !== \"\" && Date.now() - at <= TYPEAHEAD_RESET_MS;\n }, []);\n\n const { setElement, visible, wrapContent } = usePopoverPopup(popover, {\n part: \"Menu.Popup\",\n initialFocus: undefined,\n // Design 6.1: the first (or last) enabled item by open cause, nothing for a hover open, the\n // popup itself when there are no items.\n // A pointer open focuses without the focus ring where the engine can be told so (H1).\n resolveInitialFocus: (popup) => {\n const { cause, pointer } = menu.takeOpenCause();\n if (cause === \"none\") return null;\n const element = edgeItem(popup, cause) ?? popup;\n return pointer ? { element, focusVisible: false } : element;\n },\n keepMounted,\n transition,\n });\n\n const devRef = useCallback((element: HTMLElement) => {\n if (isDev && element.tagName === \"DIALOG\") {\n console.error(\n \"Menu.Popup: rendered a <dialog>. Render a <div>: a <dialog popover> runs the dialog \" +\n \"focusing steps, which fight the menu's item focus.\",\n );\n }\n return () => {};\n }, []);\n const ref = useMemo(() => mergeRefs(setElement, devRef), [setElement, devRef]);\n\n const listContext = useMemo<MenuListContextValue>(\n () => ({ menu, nativeFocusgroup, typingAhead }),\n [menu, nativeFocusgroup, typingAhead],\n );\n\n const state: MenuPopupState = { open: popover.committed, side, align, submenu };\n\n const runTypeahead = (popup: HTMLElement, current: HTMLElement | null, key: string): boolean => {\n const now = Date.now();\n const buffer = typeahead.current;\n if (buffer.text === \"\" || now - buffer.at > TYPEAHEAD_RESET_MS) buffer.text = \"\";\n if (key === \" \" && buffer.text === \"\") return false;\n buffer.text += key.toLowerCase();\n buffer.at = now;\n const items = menuItems(popup);\n if (items.length === 0) return true;\n // A repeated single character cycles through the items starting with it; a longer buffer\n // keeps the current item while it still matches (design 5.1).\n const repeated = [...buffer.text].every((c) => c === buffer.text[0]);\n const search = repeated ? buffer.text[0]! : buffer.text;\n const index = current === null ? -1 : items.indexOf(current);\n const start = repeated || index < 0 ? index + 1 : index;\n for (let i = 0; i < items.length; i++) {\n const item = items[(start + i) % items.length]!;\n if (typeaheadText(item).startsWith(search)) {\n if (item !== current) focusItem(item);\n break;\n }\n }\n return true;\n };\n\n const onKeyDown = (event: KeyboardEvent<HTMLElement>) => {\n if (event.defaultPrevented || hasModifier(event)) return;\n const popup = event.currentTarget;\n const target = event.target as HTMLElement;\n const onItem = isMenuItem(target, popup);\n if (!onItem && target !== popup) return;\n const key = event.key;\n\n // ArrowLeft (ArrowRight in RTL) closes a submenu; focus returns to its SubmenuTrigger\n // through the popover hook's nested focus rule (design 6.3).\n if (submenu && key === (isRtl(popup) ? \"ArrowRight\" : \"ArrowLeft\")) {\n event.preventDefault();\n popover.requestClose(event.nativeEvent, \"activate\");\n return;\n }\n\n if (key === \"ArrowDown\" || key === \"ArrowUp\" || key === \"Home\" || key === \"End\") {\n if (!onItem) {\n // The popup itself has focus (no items at open, or focus landed on it some other way):\n // into the list from its edge (review M1).\n event.preventDefault();\n const items = menuItems(popup);\n const item = key === \"ArrowDown\" || key === \"Home\" ? items[0] : items.at(-1);\n if (item !== undefined) focusItem(item);\n return;\n }\n // The browser moves focus itself where `focusgroup` is implemented.\n if (nativeFocusgroup) return;\n event.preventDefault();\n const items = menuItems(popup);\n const index = items.indexOf(target);\n const last = items.length - 1;\n let next: number;\n if (key === \"Home\") next = 0;\n else if (key === \"End\") next = last;\n else if (key === \"ArrowDown\") next = index >= last ? (loop ? 0 : last) : index + 1;\n else next = index <= 0 ? (loop ? last : 0) : index - 1;\n const item = items[next];\n if (item !== undefined && item !== target) focusItem(item);\n return;\n }\n\n if (key.length === 1 && runTypeahead(popup, onItem ? target : null, key)) {\n event.preventDefault();\n }\n };\n\n // Tab / Shift+Tab or any focus move out of the tree closes the whole stack at once (design\n // 2.2): submenus are DOM descendants of the top-level popup, the Trigger is outside on purpose.\n // A null `relatedTarget` (window blur, a removed element) closes nothing.\n const onBlur = (event: FocusEvent<HTMLElement>) => {\n if (submenu) return;\n const popup = event.currentTarget;\n const next = event.relatedTarget;\n if (!(next instanceof Node) || popup.contains(next)) return;\n if (popup.matches(\":popover-open\")) popup.hidePopover();\n };\n\n // A press on non-item space (padding, a Separator, a GroupLabel) would focus the popup itself\n // (`tabindex=\"-1\"`) and take focus off the current item; keep it where it is (review M1, the\n // pointer-focus model of design 6.2). Focusable content of the consumer's (a text field) still\n // takes focus.\n const onMouseDown = (event: MouseEvent<HTMLElement>) => {\n if (event.button !== 0) return;\n const target = event.target as Element;\n const focusable = target.closest(FOCUSABLE_SELECTOR);\n if (focusable === null || focusable.getAttribute(\"role\") === \"menu\") event.preventDefault();\n };\n\n const libraryProps: ComponentPropsWithRef<\"div\"> & DataAttributes = {\n id: popover.ids.popup,\n popover: \"auto\",\n role: \"menu\",\n \"aria-labelledby\": menu.triggerId,\n // The popup itself takes focus as the last resort when a menu has no items.\n tabIndex: -1,\n ...(experimental_focusgroup && { focusgroup: `menu ${loop ? \"wrap\" : \"nowrap\"}` }),\n onKeyDown,\n onBlur,\n onMouseDown,\n ...(submenu && {\n onPointerEnter: (event: PointerEvent<HTMLElement>) => {\n if (event.pointerType !== \"touch\") menu.timers.cancelClose();\n },\n onPointerLeave: (event: PointerEvent<HTMLElement>) => {\n if (event.pointerType !== \"touch\") scheduleHoverClose(menu, event.nativeEvent);\n },\n }),\n ...(side !== null && { \"data-side\": side, \"data-align\": align }),\n };\n\n const content = (s: MenuPopupState): ReactNode => {\n let node: ReactNode;\n if (keepMounted === true) {\n node = (\n <Activity mode={visible ? \"visible\" : \"hidden\"}>\n {wrapContent(resolveStateValue(children, s))}\n </Activity>\n );\n } else {\n node = visible ? wrapContent(resolveStateValue(children, s)) : null;\n }\n // Not part of an enclosing Toolbar: a Menu.Trigger or ToggleGroup in here sees no Toolbar.\n return (\n <MenuListContext value={listContext}>\n <ToolbarContext value={null}>{node}</ToolbarContext>\n </MenuListContext>\n );\n };\n\n const element = useRender<ComponentPropsWithRef<\"div\">, MenuPopupState>({\n defaultElement: \"div\",\n render,\n state,\n className,\n // The inline placement sits under the consumer's style: the consumer wins.\n style:\n side === null\n ? style\n : (s) => ({\n positionArea: positionArea(side, align),\n positionTryFallbacks: submenu ? \"flip-inline\" : POSITION_TRY_FALLBACKS,\n ...resolveStateValue(style, s),\n }),\n children: content,\n props: rest,\n libraryProps,\n ref,\n });\n\n return useTransitionBoundary(\"Menu.Popup\", transition, element);\n}\n\nMenuPopup.displayName = \"MenuPopup\";\n"],"mappings":"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"}
1
+ {"version":3,"file":"menu-popup.mjs","names":[],"sources":["../../src/menu/menu-popup.tsx"],"sourcesContent":["\"use client\";\nimport {\n Activity,\n useCallback,\n useEffect,\n useLayoutEffect,\n useMemo,\n useRef,\n useSyncExternalStore,\n} from \"react\";\nimport type {\n ComponentPropsWithRef,\n CSSProperties,\n FocusEvent,\n KeyboardEvent,\n MouseEvent,\n PointerEvent,\n ReactNode,\n ViewTransitionClass,\n} from \"react\";\nimport {\n FSUI_PRECEDENCE,\n MENU_POINT_FALLBACKS,\n MENU_POINT_HREF,\n MENU_POINT_RULES,\n MENU_POINT_X,\n MENU_POINT_Y,\n POSITION_TRY_FALLBACKS,\n positionArea,\n} from \"../internal/anchor-position.js\";\nimport type { AnchorAlign, AnchorSide } from \"../internal/anchor-position.js\";\nimport { mergeRefs } from \"../internal/merge-props.js\";\nimport { ToolbarContext } from \"../internal/toolbar-context.js\";\nimport { useTransitionBoundary } from \"../internal/transition-boundary.js\";\nimport type { RenderProp, StateValue } from \"../internal/types.js\";\nimport { usePopoverPopup } from \"../internal/use-popover-popup.js\";\nimport { resolveStateValue, useRender } from \"../internal/use-render.js\";\nimport type { DataAttributes } from \"../internal/use-render.js\";\nimport { claimPress, isHandledBy, MenuListContext, useMenuContext } from \"./menu-context.js\";\nimport type { MenuContextValue, MenuListContextValue, MenuPlacement } from \"./menu-context.js\";\nimport {\n capturePointer,\n edgeItem,\n focusItem,\n hasModifier,\n isMenuItem,\n isRtl,\n menuItems,\n typeaheadText,\n} from \"./menu-dom.js\";\nimport { scheduleHoverClose } from \"./menu-hover.js\";\n\nexport type MenuSide = AnchorSide;\nexport type MenuAlign = AnchorAlign;\n\nexport interface MenuPopupState {\n /** Committed open state. Read `:popover-open` for the DOM truth. */\n readonly open: boolean;\n /** `null` with `side={null}`, and while the menu is placed at a point (`context`). */\n readonly side: MenuSide | null;\n readonly align: MenuAlign;\n /** This popup belongs to a submenu. */\n readonly submenu: boolean;\n /**\n * The current open came from a `Menu.ContextTrigger`: the popup is placed at the pointer, with\n * `--menu-point-x` / `--menu-point-y` set inline (`data-context`).\n */\n readonly context: boolean;\n}\n\nexport interface MenuPopupProps extends Omit<\n ComponentPropsWithRef<\"div\">,\n | \"id\"\n | \"popover\"\n | \"role\"\n | \"aria-labelledby\"\n | \"onKeyDown\"\n | \"onFocus\"\n | \"onBlur\"\n | \"onMouseDown\"\n | \"onContextMenu\"\n | \"onPointerEnter\"\n | \"onPointerLeave\"\n | \"onToggle\"\n | \"onBeforeToggle\"\n | \"className\"\n | \"style\"\n | \"children\"\n> {\n /**\n * Placement against the Trigger through inline `position-area`, with `position-try-fallbacks`\n * flipping it when it does not fit. Default `\"bottom\"` for a top-level menu, `\"right\"` for a\n * submenu (whose fallback is `flip-inline` only). `null` emits no inline positioning (and no\n * `data-side` / `data-align`): the stylesheet owns placement. Ignored for an open from a\n * `Menu.ContextTrigger`, which is placed at the pointer; there `null` also opts out of the\n * inline point placement and the library's `@position-try` rules, leaving only\n * `--menu-point-x` / `--menu-point-y`.\n */\n side?: MenuSide | null;\n /** Alignment along the side. Default `\"start\"`. Ignored when `side` is `null`. */\n align?: MenuAlign;\n /**\n * `false` (default): the items are not rendered while the menu is closed; mounting them is part\n * of the opening transition, so items that suspend hold the open (and the Trigger pending)\n * until they are ready. `true`: they stay mounted inside `<Activity mode=\"hidden\">`.\n */\n keepMounted?: boolean;\n /**\n * Experimental (Chromium 150+). Renders `focusgroup=\"menu wrap|nowrap\"` and, where the engine\n * implements `focusGroup`, lets the browser handle ArrowUp / ArrowDown / Home / End: items then\n * render no `tabindex` and the library's arrow-key handling steps aside. Typeahead, activation,\n * submenu keys and Tab stay in JS. No effect in other engines. Default `false`.\n */\n experimental_focusgroup?: boolean;\n /**\n * Forwarded to the `<ViewTransition default>` around the popup element. Animates the opening\n * commit and every library-driven close (an item's activation, the Trigger, ArrowLeft, hover\n * leaving a submenu, controlled `open={false}`) in all engines; dismissals (Escape, a click\n * outside, Tab) animate with CSS only. Without it the part renders no `<ViewTransition>`.\n */\n transition?: ViewTransitionClass;\n className?: StateValue<string | undefined, MenuPopupState>;\n style?: StateValue<CSSProperties | undefined, MenuPopupState>;\n children?: StateValue<ReactNode, MenuPopupState>;\n /** Should render a `<div>`; a `<dialog>` would run the dialog focusing steps. */\n render?: RenderProp<ComponentPropsWithRef<\"div\">, MenuPopupState>;\n}\n\n/** Engine capabilities never change while the page lives. */\nconst subscribeNever = () => () => {};\n\n/** Typeahead buffer lifetime after the last keystroke (design 5.1). */\nconst TYPEAHEAD_RESET_MS = 500;\n\nconst isDev = process.env.NODE_ENV !== \"production\";\n\n/** What a press may focus inside a popup: items, the popup itself, the consumer's controls. */\nconst FOCUSABLE_SELECTOR =\n 'a[href], button, input, select, textarea, summary, [tabindex], [contenteditable]:not([contenteditable=\"false\"])';\n\ntype PointPlacement = Extract<MenuPlacement, { kind: \"point\" }>;\n\n/**\n * The inline placement of a point open (context-menu.md 3.3): the point as two custom\n * properties, and unless the stylesheet owns placement, `left` / `top` at it with the other two\n * insets `auto` (longhands, so React never sees a shorthand next to its longhands),\n * `position-area: none` (a consumer's anchored `position-area` would otherwise place the popup\n * inside a grid area) and the library's fallbacks, spelled by generation parity (4.3).\n */\nfunction pointStyle(point: PointPlacement, stylesheetOwned: boolean): CSSProperties {\n const vars = { [MENU_POINT_X]: `${point.x}px`, [MENU_POINT_Y]: `${point.y}px` };\n if (stylesheetOwned) return vars as CSSProperties;\n return {\n ...vars,\n top: `var(${MENU_POINT_Y})`,\n right: \"auto\",\n bottom: \"auto\",\n left: `var(${MENU_POINT_X})`,\n positionArea: \"none\",\n positionTryFallbacks: MENU_POINT_FALLBACKS[point.generation % 2],\n } as CSSProperties;\n}\n\n/**\n * The outside right- and middle-click close of a top-level menu (context-menu.md 5): a\n * `contextmenu` outside the menu tree that this menu's ContextTrigger did not handle, or a\n * middle-button `pointerdown` outside it, hides the root popup at once (`\"dismiss\"` through the\n * `toggle` notification). Chromium's click-based light dismiss ignores those buttons. Returns the\n * cleanup.\n */\nfunction listenOutside(popup: HTMLElement, menu: MenuContextValue): () => void {\n const doc = popup.ownerDocument;\n const inside = (target: EventTarget | null) => target instanceof Node && popup.contains(target);\n const close = () => {\n if (popup.matches(\":popover-open\")) popup.hidePopover();\n };\n // Bubble phase: the region's handler ran first and marked a move.\n const onContextMenu = (event: Event) => {\n if (inside(event.target) || isHandledBy(event, menu)) return;\n close();\n };\n const onPointerDown = (event: Event) => {\n if ((event as globalThis.PointerEvent).button !== 1 || inside(event.target)) return;\n close();\n };\n doc.addEventListener(\"contextmenu\", onContextMenu);\n doc.addEventListener(\"pointerdown\", onPointerDown, true);\n return () => {\n doc.removeEventListener(\"contextmenu\", onContextMenu);\n doc.removeEventListener(\"pointerdown\", onPointerDown, true);\n };\n}\n\n/**\n * The menu itself: `<div popover=\"auto\" role=\"menu\" aria-labelledby={trigger} tabindex=\"-1\">`,\n * placed against its Trigger with CSS Anchor Positioning. Owns the keyboard model (design 5):\n * ArrowUp / ArrowDown / Home / End move focus between items (or the browser does, with\n * `experimental_focusgroup` where supported), printable characters run typeahead, ArrowLeft\n * closes a submenu. The top-level popup closes the whole menu at once when focus leaves it (Tab).\n * Style it with `:popover-open`; it emits `data-side` / `data-align` but no `data-open`.\n *\n * Owned keys (conventions 1.2): `id`, `popover`, `role`, `aria-labelledby` (token-merged),\n * `tabIndex`, `focusgroup` (with the flag), `onKeyDown`, `onBlur`, `onMouseDown` (a press on\n * non-item space keeps focus on the current item), `onContextMenu` (no native menu over this\n * one), `onPointerEnter` / `onPointerLeave`, `data-side` / `data-align` unless `side` is `null`\n * or the menu is placed at a point, and `data-context` for an open from a `Menu.ContextTrigger`\n * (placed at the pointer: `--menu-point-x` / `--menu-point-y` inline, and unless `side` is\n * `null`, `left` / `top` at the point with the library's `@position-try` fallbacks, whose rules\n * it renders as a hoisted `<style>`). A top-level menu closes at once on a right- or\n * middle-click outside it.\n */\nexport function MenuPopup({\n side: sideProp,\n align = \"start\",\n keepMounted,\n experimental_focusgroup = false,\n transition,\n className,\n style,\n children,\n render,\n ...rest\n}: MenuPopupProps) {\n const menu = useMenuContext(\"Menu.Popup\");\n const { popover, submenu, loop } = menu;\n const sideOption = sideProp === undefined ? (submenu ? \"right\" : \"bottom\") : sideProp;\n // A ContextTrigger's open is placed at the point; nothing is anchored (context-menu.md 3.3).\n const point = menu.placement.kind === \"point\" ? menu.placement : null;\n const side = point !== null ? null : sideOption;\n\n // As Tabs.List (#43): detected only with the flag, never during SSR or hydration.\n const nativeFocusgroup = useSyncExternalStore(\n subscribeNever,\n () => experimental_focusgroup && \"focusGroup\" in HTMLElement.prototype,\n () => false,\n );\n\n const typeahead = useRef({ text: \"\", at: 0 });\n const typingAhead = useCallback(() => {\n const { text, at } = typeahead.current;\n return text !== \"\" && Date.now() - at <= TYPEAHEAD_RESET_MS;\n }, []);\n\n // The outside right- / middle-click listeners of a top-level menu, while it is shown.\n const outside = useRef<(() => void) | null>(null);\n const stopOutside = useCallback(() => {\n outside.current?.();\n outside.current = null;\n }, []);\n useEffect(() => stopOutside, [stopOutside]);\n\n const { setElement, visible, wrapContent, reshow } = usePopoverPopup(popover, {\n part: \"Menu.Popup\",\n initialFocus: undefined,\n // Design 6.1: the first (or last) enabled item by open cause, nothing for a hover open, the\n // popup itself when there are no items.\n // A pointer open focuses without the focus ring where the engine can be told so (H1).\n resolveInitialFocus: (popup) => {\n const { cause, pointer } = menu.takeOpenCause();\n if (cause === \"none\") return null;\n const element = edgeItem(popup, cause) ?? popup;\n return pointer ? { element, focusVisible: false } : element;\n },\n keepMounted,\n transition,\n // A point open nests through the region that opened it (context-menu.md 3.1).\n source: () => (point !== null ? point.source : popover.triggerRef.current),\n onToggle: (event) => {\n if (submenu) return;\n const element = popover.popupRef.current;\n if (event.newState === \"open\" && element !== null && element.matches(\":popover-open\")) {\n outside.current ??= listenOutside(element, menu);\n } else if (event.newState === \"closed\") {\n stopOutside();\n }\n },\n });\n\n // A move (context-menu.md 4.3): the popup is shown and the placement changed since it was\n // shown (a new point, or a right-click in the region while a Trigger open is shown). Re-show\n // it so the respelled fallbacks place it afresh, the source is the region and the first item\n // is focused again; then capture the still-pressed pointer again, against the Firefox / WebKit\n // `pointerup` light dismiss. Runs after the hook's apply effect (declared after it).\n // Once the popup is not visible (hidden, exit animation over) and no open is intended or in\n // flight, a point placement goes back to anchored for the next open that is not a\n // ContextTrigger's: after a point open that was shown and closed, and after one that was\n // rejected or refused (review L1).\n const shownKey = useRef<number | \"anchor\" | null>(null);\n useLayoutEffect(() => {\n const element = popover.popupRef.current;\n if (element === null) return;\n const key = point !== null ? point.generation : \"anchor\";\n if (element.matches(\":popover-open\")) {\n if (shownKey.current !== null && shownKey.current !== key && reshow()) {\n capturePointer(element, point?.pointerId);\n }\n shownKey.current = key;\n } else {\n shownKey.current = null;\n if (!visible && point !== null && !popover.pending && !popover.intended()) {\n menu.resetPlacement(point.generation);\n }\n }\n });\n\n const devRef = useCallback((element: HTMLElement) => {\n if (isDev && element.tagName === \"DIALOG\") {\n console.error(\n \"Menu.Popup: rendered a <dialog>. Render a <div>: a <dialog popover> runs the dialog \" +\n \"focusing steps, which fight the menu's item focus.\",\n );\n }\n // A press inside a menu is never a long-press of a Menu.ContextTrigger around it (review M1).\n // Native, so it runs before React's root listener dispatches to the region.\n const onPointerDown = (event: Event) => claimPress(event);\n element.addEventListener(\"pointerdown\", onPointerDown);\n return () => element.removeEventListener(\"pointerdown\", onPointerDown);\n }, []);\n const ref = useMemo(() => mergeRefs(setElement, devRef), [setElement, devRef]);\n\n const listContext = useMemo<MenuListContextValue>(\n () => ({ menu, nativeFocusgroup, typingAhead }),\n [menu, nativeFocusgroup, typingAhead],\n );\n\n const state: MenuPopupState = {\n open: popover.committed,\n side,\n align,\n submenu,\n context: point !== null,\n };\n\n const runTypeahead = (popup: HTMLElement, current: HTMLElement | null, key: string): boolean => {\n const now = Date.now();\n const buffer = typeahead.current;\n if (buffer.text === \"\" || now - buffer.at > TYPEAHEAD_RESET_MS) buffer.text = \"\";\n if (key === \" \" && buffer.text === \"\") return false;\n buffer.text += key.toLowerCase();\n buffer.at = now;\n const items = menuItems(popup);\n if (items.length === 0) return true;\n // A repeated single character cycles through the items starting with it; a longer buffer\n // keeps the current item while it still matches (design 5.1).\n const repeated = [...buffer.text].every((c) => c === buffer.text[0]);\n const search = repeated ? buffer.text[0]! : buffer.text;\n const index = current === null ? -1 : items.indexOf(current);\n const start = repeated || index < 0 ? index + 1 : index;\n for (let i = 0; i < items.length; i++) {\n const item = items[(start + i) % items.length]!;\n if (typeaheadText(item).startsWith(search)) {\n if (item !== current) focusItem(item);\n break;\n }\n }\n return true;\n };\n\n const onKeyDown = (event: KeyboardEvent<HTMLElement>) => {\n if (event.defaultPrevented || hasModifier(event)) return;\n const popup = event.currentTarget;\n const target = event.target as HTMLElement;\n const onItem = isMenuItem(target, popup);\n if (!onItem && target !== popup) return;\n const key = event.key;\n\n // ArrowLeft (ArrowRight in RTL) closes a submenu; focus returns to its SubmenuTrigger\n // through the popover hook's nested focus rule (design 6.3).\n if (submenu && key === (isRtl(popup) ? \"ArrowRight\" : \"ArrowLeft\")) {\n event.preventDefault();\n popover.requestClose(event.nativeEvent, \"activate\");\n return;\n }\n\n if (key === \"ArrowDown\" || key === \"ArrowUp\" || key === \"Home\" || key === \"End\") {\n if (!onItem) {\n // The popup itself has focus (no items at open, or focus landed on it some other way):\n // into the list from its edge (review M1).\n event.preventDefault();\n const items = menuItems(popup);\n const item = key === \"ArrowDown\" || key === \"Home\" ? items[0] : items.at(-1);\n if (item !== undefined) focusItem(item);\n return;\n }\n // The browser moves focus itself where `focusgroup` is implemented.\n if (nativeFocusgroup) return;\n event.preventDefault();\n const items = menuItems(popup);\n const index = items.indexOf(target);\n const last = items.length - 1;\n let next: number;\n if (key === \"Home\") next = 0;\n else if (key === \"End\") next = last;\n else if (key === \"ArrowDown\") next = index >= last ? (loop ? 0 : last) : index + 1;\n else next = index <= 0 ? (loop ? last : 0) : index - 1;\n const item = items[next];\n if (item !== undefined && item !== target) focusItem(item);\n return;\n }\n\n if (key.length === 1 && runTypeahead(popup, onItem ? target : null, key)) {\n event.preventDefault();\n }\n };\n\n // Tab / Shift+Tab or any focus move out of the tree closes the whole stack at once (design\n // 2.2): submenus are DOM descendants of the top-level popup, the Trigger is outside on purpose.\n // A null `relatedTarget` (window blur, a removed element) closes nothing.\n const onBlur = (event: FocusEvent<HTMLElement>) => {\n if (submenu) return;\n const popup = event.currentTarget;\n const next = event.relatedTarget;\n if (!(next instanceof Node) || popup.contains(next)) return;\n if (popup.matches(\":popover-open\")) popup.hidePopover();\n };\n\n // A press on non-item space (padding, a Separator, a GroupLabel) would focus the popup itself\n // (`tabindex=\"-1\"`) and take focus off the current item; keep it where it is (review M1, the\n // pointer-focus model of design 6.2). Focusable content of the consumer's (a text field) still\n // takes focus.\n const onMouseDown = (event: MouseEvent<HTMLElement>) => {\n if (event.button !== 0) return;\n const target = event.target as Element;\n const focusable = target.closest(FOCUSABLE_SELECTOR);\n if (focusable === null || focusable.getAttribute(\"role\") === \"menu\") event.preventDefault();\n };\n\n const libraryProps: ComponentPropsWithRef<\"div\"> & DataAttributes = {\n id: popover.ids.popup,\n popover: \"auto\",\n role: \"menu\",\n \"aria-labelledby\": menu.triggerId,\n // The popup itself takes focus as the last resort when a menu has no items.\n tabIndex: -1,\n ...(experimental_focusgroup && { focusgroup: `menu ${loop ? \"wrap\" : \"nowrap\"}` }),\n onKeyDown,\n onBlur,\n onMouseDown,\n // No native menu over the custom one. The popup is a sibling of any ContextTrigger, so this\n // event never reaches a region as a move; an outer region sees it prevented and stays quiet.\n onContextMenu: (event: MouseEvent<HTMLElement>) => {\n event.preventDefault();\n },\n ...(submenu && {\n onPointerEnter: (event: PointerEvent<HTMLElement>) => {\n if (event.pointerType !== \"touch\") menu.timers.cancelClose();\n },\n onPointerLeave: (event: PointerEvent<HTMLElement>) => {\n if (event.pointerType !== \"touch\") scheduleHoverClose(menu, event.nativeEvent);\n },\n }),\n ...(side !== null && { \"data-side\": side, \"data-align\": align }),\n \"data-context\": point !== null ? \"\" : undefined,\n };\n\n // The shipped `@position-try` rules (conventions 12): hoisted into <head> and deduplicated by\n // React, rendered only by a point open that does not opt out with `side={null}`.\n const pointRules =\n point !== null && sideOption !== null ? (\n <style href={MENU_POINT_HREF} precedence={FSUI_PRECEDENCE}>\n {MENU_POINT_RULES}\n </style>\n ) : null;\n\n const content = (s: MenuPopupState): ReactNode => {\n let node: ReactNode;\n if (keepMounted === true) {\n node = (\n <Activity mode={visible ? \"visible\" : \"hidden\"}>\n {wrapContent(resolveStateValue(children, s))}\n </Activity>\n );\n } else {\n node = visible ? wrapContent(resolveStateValue(children, s)) : null;\n }\n // Not part of an enclosing Toolbar: a Menu.Trigger or ToggleGroup in here sees no Toolbar.\n return (\n <MenuListContext value={listContext}>\n {pointRules}\n <ToolbarContext value={null}>{node}</ToolbarContext>\n </MenuListContext>\n );\n };\n\n const element = useRender<ComponentPropsWithRef<\"div\">, MenuPopupState>({\n defaultElement: \"div\",\n render,\n state,\n className,\n // The inline placement sits under the consumer's style: the consumer wins.\n style:\n point !== null\n ? (s) => ({ ...pointStyle(point, sideOption === null), ...resolveStateValue(style, s) })\n : side === null\n ? style\n : (s) => ({\n positionArea: positionArea(side, align),\n positionTryFallbacks: submenu ? \"flip-inline\" : POSITION_TRY_FALLBACKS,\n ...resolveStateValue(style, s),\n }),\n children: content,\n props: rest,\n libraryProps,\n ref,\n });\n\n return useTransitionBoundary(\"Menu.Popup\", transition, element);\n}\n\nMenuPopup.displayName = \"MenuPopup\";\n"],"mappings":"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"}
@@ -21,6 +21,11 @@ export interface MenuRootState {
21
21
  readonly pending: boolean;
22
22
  /** This Root is a submenu (rendered inside a `Menu.Popup`). */
23
23
  readonly submenu: boolean;
24
+ /**
25
+ * The current open (optimistic) came from a `Menu.ContextTrigger`: the menu is placed at the
26
+ * pointer rather than against the Trigger.
27
+ */
28
+ readonly context: boolean;
24
29
  }
25
30
  export interface MenuRootProps {
26
31
  /** Controlled open state. `undefined` means uncontrolled. */
@@ -1 +1 @@
1
- {"version":3,"file":"menu-root.d.mts","names":[],"sources":["../../src/menu/menu-root.tsx"],"mappings":";;;;iBAYiB,8BAA8B;;;;;;;;;;WAUpC,QAAQ;;iBAGF;;WAEN;;WAEA;;WAEA;;iBAGM;;EAEf;;EAEA;;;;;;;;EAQA,mBAAmB,sBAAsB;;EAEzC;;EAEA;;;;;EAKA;EACA,WAAW,WAAW,WAAW;;wBAenB,WACd,MACA,aACA,kBACA,MACA,OACA,YACA,YACC,gCAAa,IAAA;yBARA"}
1
+ {"version":3,"file":"menu-root.d.mts","names":[],"sources":["../../src/menu/menu-root.tsx"],"mappings":";;;;iBAsBiB,8BAA8B;;;;;;;;;;WAUpC,QAAQ;;iBAGF;;WAEN;;WAEA;;WAEA;;;;;WAKA;;iBAGM;;EAEf;;EAEA;;;;;;;;EAQA,mBAAmB,sBAAsB;;EAEzC;;EAEA;;;;;EAKA;EACA,WAAW,WAAW,WAAW;;wBAenB,WACd,MACA,aACA,kBACA,MACA,OACA,YACA,YACC,gCAAa,IAAA;yBARA"}