@allxsmith/bestax-bulma 5.29.0 → 5.31.0

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.
Files changed (67) hide show
  1. package/dist/bestax.css +1 -1
  2. package/dist/bestax.css.map +1 -1
  3. package/dist/esm/components/ContextMenu.js +244 -0
  4. package/dist/esm/components/ContextMenu.js.map +1 -0
  5. package/dist/esm/components/Dropdown.js +37 -85
  6. package/dist/esm/components/Dropdown.js.map +1 -1
  7. package/dist/esm/components/HoverCard.js +3 -10
  8. package/dist/esm/components/HoverCard.js.map +1 -1
  9. package/dist/esm/components/Tooltip.js +88 -63
  10. package/dist/esm/components/Tooltip.js.map +1 -1
  11. package/dist/esm/elements/Button.js.map +1 -1
  12. package/dist/esm/elements/Buttons.js +2 -0
  13. package/dist/esm/elements/Buttons.js.map +1 -1
  14. package/dist/esm/elements/ToggleButton.js +68 -0
  15. package/dist/esm/elements/ToggleButton.js.map +1 -0
  16. package/dist/esm/form/DateTimeInputBase.js.map +1 -1
  17. package/dist/esm/form/TimeInputBase.js.map +1 -1
  18. package/dist/esm/form/_pickerInternals/pickerTypes.js +14 -6
  19. package/dist/esm/form/_pickerInternals/pickerTypes.js.map +1 -1
  20. package/dist/esm/helpers/useContextMenuTrigger.js +166 -0
  21. package/dist/esm/helpers/useContextMenuTrigger.js.map +1 -0
  22. package/dist/esm/helpers/useHoverOpen.js +40 -6
  23. package/dist/esm/helpers/useHoverOpen.js.map +1 -1
  24. package/dist/esm/helpers/useMenuKeys.js +182 -0
  25. package/dist/esm/helpers/useMenuKeys.js.map +1 -0
  26. package/dist/extras.css +1 -1
  27. package/dist/extras.css.map +1 -1
  28. package/dist/index.cjs +1896 -1261
  29. package/dist/index.cjs.map +1 -1
  30. package/dist/index.esm.js +2 -0
  31. package/dist/index.esm.js.map +1 -1
  32. package/dist/types/components/ContextMenu.d.ts +66 -0
  33. package/dist/types/components/Tooltip.d.ts +63 -9
  34. package/dist/types/elements/Button.d.ts +1 -1
  35. package/dist/types/elements/Buttons.d.ts +1 -0
  36. package/dist/types/elements/ToggleButton.d.ts +100 -0
  37. package/dist/types/form/DateTimeInputBase.d.ts +4 -1
  38. package/dist/types/form/TimeInputBase.d.ts +5 -2
  39. package/dist/types/form/_pickerInternals/pickerTypes.d.ts +16 -1
  40. package/dist/types/helpers/useContextMenuTrigger.d.ts +67 -0
  41. package/dist/types/helpers/useHoverOpen.d.ts +19 -3
  42. package/dist/types/helpers/useMenuKeys.d.ts +57 -0
  43. package/dist/types/index.d.ts +2 -0
  44. package/dist/types-cjs/components/ContextMenu.d.ts +66 -0
  45. package/dist/types-cjs/components/Tooltip.d.ts +63 -9
  46. package/dist/types-cjs/elements/Button.d.ts +1 -1
  47. package/dist/types-cjs/elements/Buttons.d.ts +1 -0
  48. package/dist/types-cjs/elements/ToggleButton.d.ts +100 -0
  49. package/dist/types-cjs/form/DateTimeInputBase.d.ts +4 -1
  50. package/dist/types-cjs/form/TimeInputBase.d.ts +5 -2
  51. package/dist/types-cjs/form/_pickerInternals/pickerTypes.d.ts +16 -1
  52. package/dist/types-cjs/helpers/useContextMenuTrigger.d.ts +67 -0
  53. package/dist/types-cjs/helpers/useHoverOpen.d.ts +19 -3
  54. package/dist/types-cjs/helpers/useMenuKeys.d.ts +57 -0
  55. package/dist/types-cjs/index.d.ts +2 -0
  56. package/dist/versions/bestax-no-dark-mode.css +1 -1
  57. package/dist/versions/bestax-no-dark-mode.css.map +1 -1
  58. package/dist/versions/bestax-no-helpers-prefixed.css +1 -1
  59. package/dist/versions/bestax-no-helpers-prefixed.css.map +1 -1
  60. package/dist/versions/bestax-no-helpers.css +1 -1
  61. package/dist/versions/bestax-no-helpers.css.map +1 -1
  62. package/dist/versions/bestax-prefixed.css +1 -1
  63. package/dist/versions/bestax-prefixed.css.map +1 -1
  64. package/package.json +1 -1
  65. package/src/scss/components/_contextmenu.scss +50 -0
  66. package/src/scss/components/_index.scss +1 -0
  67. package/src/scss/components/_tooltip.scss +76 -1
@@ -0,0 +1,166 @@
1
+ /* @allxsmith/bestax-bulma — AI agents: see AGENTS.md in the package root, or https://bestax.io/llms.txt */
2
+ import { useRef, useEffect } from 'react';
3
+
4
+ // INTERNAL: deliberately not exported from src/index.ts.
5
+ /** How long a touch or a pen has to stay down to open the menu, in milliseconds. */
6
+ const LONG_PRESS_DELAY = 500;
7
+ /** How far a long press can move before it counts as something else, in CSS pixels. */
8
+ const LONG_PRESS_TOLERANCE = 10;
9
+ /**
10
+ * The last press no trigger is to start a long press for: one a trigger
11
+ * took, or another pointer coming down while a press is held. A press
12
+ * reaches every trigger it bubbles through, the innermost first and all
13
+ * before the next press, so this is how a trigger around another one knows
14
+ * the inner one took it, and how a trigger a second finger lands on knows to
15
+ * leave it.
16
+ */
17
+ let claimed = null;
18
+ /** Whether the event happened in the element its handler is on, rather than in content portaled out of it. */
19
+ const inTrigger = (event) => event.currentTarget.contains(event.target);
20
+ /**
21
+ * Where a `contextmenu` asks to open: at its point when that is on the
22
+ * element it was fired at, which it is for the pointer, and at that
23
+ * element's bottom-left corner otherwise, which is where the browser puts
24
+ * its own menu for a key.
25
+ */
26
+ function contextMenuPoint(event) {
27
+ const { clientX: x, clientY: y } = event;
28
+ const rect = event.target.getBoundingClientRect();
29
+ return x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom
30
+ ? { x, y }
31
+ : { x: rect.left, y: rect.bottom };
32
+ }
33
+ /**
34
+ * Decides when a context menu asks to open, and where. It renders nothing,
35
+ * and knows nothing about ARIA or the menu.
36
+ *
37
+ * - **`contextmenu`:** a right-click, and Shift+F10 or the context menu key
38
+ * where the browser fires the event for them, asks to open at the point
39
+ * `contextMenuPoint` works out, and prevents the browser's own menu. Each
40
+ * one asks again, so a right-click elsewhere on the trigger moves it.
41
+ * - **Long press:** a touch or a pen held still on the trigger for
42
+ * `LONG_PRESS_DELAY` fires a `contextmenu` at what it landed on, so it
43
+ * takes the same path, handlers on the way included. Moving more than
44
+ * `LONG_PRESS_TOLERANCE`, lifting, another pointer coming down anywhere in
45
+ * the document, or `pointercancel`, which is the browser taking the
46
+ * gesture for a scroll or a zoom, cancels it, and a mouse never starts
47
+ * one. A pointer that comes down while another is held starts no long
48
+ * press of its own, on this trigger or another, so two fingers on two rows
49
+ * open nothing. While the finger is down, and only then, a listener on the
50
+ * document hears other pointers coming down. Where the
51
+ * browser fires `contextmenu` for a long press itself, the first of the two
52
+ * opens it, and the end of a touch that opened it is prevented, so lifting
53
+ * doesn't click what is under the finger.
54
+ * - **Who opens:** a handler that calls `preventDefault()` on the
55
+ * `contextmenu` before this one keeps it from asking, which is how a
56
+ * trigger's own handler opts out and how the innermost of nested triggers
57
+ * wins. A long press goes to the innermost trigger that takes it. Events
58
+ * from content portaled out of the trigger, which React brings here
59
+ * anyway, are ignored.
60
+ * - **`disabled`:** nothing asks, and the browser's menu shows.
61
+ *
62
+ * Nothing it scheduled runs after the component unmounts.
63
+ *
64
+ * @function useContextMenuTrigger
65
+ * @param options - How to ask to open, and whether to.
66
+ * @returns Handlers for the trigger element.
67
+ */
68
+ function useContextMenuTrigger({ onOpen, disabled, }) {
69
+ const pressRef = useRef(null);
70
+ /** A long press opened the menu, and the touch that made it hasn't ended. */
71
+ const heldRef = useRef(false);
72
+ const cancel = () => {
73
+ clearTimeout(pressRef.current?.timer);
74
+ pressRef.current?.stop();
75
+ pressRef.current = null;
76
+ };
77
+ useEffect(() => () => {
78
+ clearTimeout(pressRef.current?.timer);
79
+ pressRef.current?.stop();
80
+ pressRef.current = null;
81
+ }, []);
82
+ const isPress = (event) => pressRef.current?.pointerId === event.pointerId;
83
+ return {
84
+ onContextMenu: event => {
85
+ if (disabled || event.defaultPrevented || !inTrigger(event))
86
+ return;
87
+ event.preventDefault();
88
+ const press = pressRef.current;
89
+ if (press) {
90
+ if (press.opened)
91
+ return;
92
+ clearTimeout(press.timer);
93
+ press.opened = true;
94
+ heldRef.current = true;
95
+ }
96
+ onOpen(contextMenuPoint(event), event.currentTarget);
97
+ },
98
+ onPointerDown: event => {
99
+ const native = event.nativeEvent;
100
+ if (disabled ||
101
+ (event.pointerType !== 'touch' && event.pointerType !== 'pen') ||
102
+ claimed === native ||
103
+ !inTrigger(event)) {
104
+ return;
105
+ }
106
+ claimed = native;
107
+ heldRef.current = false;
108
+ const target = event.target;
109
+ const doc = target.ownerDocument;
110
+ // Another pointer coming down anywhere makes it a pinch or a second
111
+ // touch, not a long press. In the capture phase, so it's heard before
112
+ // any trigger it lands on, which then leaves it. The press's own
113
+ // pointerdown has passed the document already, so it isn't heard.
114
+ const another = (other) => {
115
+ claimed = other;
116
+ cancel();
117
+ };
118
+ doc.addEventListener('pointerdown', another, true);
119
+ const press = {
120
+ pointerId: event.pointerId,
121
+ target,
122
+ x: event.clientX,
123
+ y: event.clientY,
124
+ opened: false,
125
+ stop: () => doc.removeEventListener('pointerdown', another, true),
126
+ };
127
+ press.timer = setTimeout(() => {
128
+ const view = press.target.ownerDocument.defaultView;
129
+ press.target.dispatchEvent(new view.MouseEvent('contextmenu', {
130
+ bubbles: true,
131
+ cancelable: true,
132
+ composed: true,
133
+ clientX: press.x,
134
+ clientY: press.y,
135
+ button: 2,
136
+ }));
137
+ }, LONG_PRESS_DELAY);
138
+ pressRef.current = press;
139
+ },
140
+ onPointerMove: event => {
141
+ const press = pressRef.current;
142
+ if (!press || !isPress(event) || press.opened)
143
+ return;
144
+ const moved = Math.hypot(event.clientX - press.x, event.clientY - press.y);
145
+ if (moved > LONG_PRESS_TOLERANCE)
146
+ cancel();
147
+ },
148
+ onPointerUp: event => {
149
+ if (isPress(event))
150
+ cancel();
151
+ },
152
+ onPointerCancel: event => {
153
+ if (isPress(event))
154
+ cancel();
155
+ },
156
+ onTouchEnd: event => {
157
+ if (!heldRef.current)
158
+ return;
159
+ heldRef.current = false;
160
+ event.preventDefault();
161
+ },
162
+ };
163
+ }
164
+
165
+ export { LONG_PRESS_DELAY, LONG_PRESS_TOLERANCE, useContextMenuTrigger };
166
+ //# sourceMappingURL=useContextMenuTrigger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useContextMenuTrigger.js","sources":["../../../src/helpers/useContextMenuTrigger.ts"],"sourcesContent":["// INTERNAL: deliberately not exported from src/index.ts.\nimport { useEffect, useRef } from 'react';\nimport type { MouseEvent, PointerEvent, TouchEvent } from 'react';\n\n/** A point in the viewport, in CSS pixels. */\nexport interface ContextMenuPoint {\n x: number;\n y: number;\n}\n\n/**\n * Options for `useContextMenuTrigger`.\n */\nexport interface UseContextMenuTriggerOptions {\n /**\n * Asks to open at `point`. `trigger` is the element the handlers are on,\n * for its document.\n */\n onOpen: (point: ContextMenuPoint, trigger: HTMLElement) => void;\n /** Leave the browser's own menu alone, and start no long press. */\n disabled: boolean;\n}\n\n/** Handlers for the trigger element. */\nexport interface ContextMenuTriggerHandlers {\n onContextMenu: (event: MouseEvent<HTMLElement>) => void;\n onPointerDown: (event: PointerEvent<HTMLElement>) => void;\n onPointerMove: (event: PointerEvent<HTMLElement>) => void;\n onPointerUp: (event: PointerEvent<HTMLElement>) => void;\n onPointerCancel: (event: PointerEvent<HTMLElement>) => void;\n onTouchEnd: (event: TouchEvent<HTMLElement>) => void;\n}\n\n/** How long a touch or a pen has to stay down to open the menu, in milliseconds. */\nexport const LONG_PRESS_DELAY = 500;\n\n/** How far a long press can move before it counts as something else, in CSS pixels. */\nexport const LONG_PRESS_TOLERANCE = 10;\n\n/** A touch or a pen held on the trigger. */\ninterface Press {\n pointerId: number;\n /** What it landed on, which its `contextmenu` goes to. */\n target: Element;\n x: number;\n y: number;\n timer?: ReturnType<typeof setTimeout>;\n /** A `contextmenu` arrived while it was down, so the menu opened. */\n opened: boolean;\n /** Stops listening for another pointer coming down in its document. */\n stop: () => void;\n}\n\n/**\n * The last press no trigger is to start a long press for: one a trigger\n * took, or another pointer coming down while a press is held. A press\n * reaches every trigger it bubbles through, the innermost first and all\n * before the next press, so this is how a trigger around another one knows\n * the inner one took it, and how a trigger a second finger lands on knows to\n * leave it.\n */\nlet claimed: Event | null = null;\n\n/** Whether the event happened in the element its handler is on, rather than in content portaled out of it. */\nconst inTrigger = (event: { currentTarget: Element; target: EventTarget }) =>\n event.currentTarget.contains(event.target as Node);\n\n/**\n * Where a `contextmenu` asks to open: at its point when that is on the\n * element it was fired at, which it is for the pointer, and at that\n * element's bottom-left corner otherwise, which is where the browser puts\n * its own menu for a key.\n */\nfunction contextMenuPoint(event: MouseEvent<HTMLElement>): ContextMenuPoint {\n const { clientX: x, clientY: y } = event;\n const rect = (event.target as Element).getBoundingClientRect();\n return x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom\n ? { x, y }\n : { x: rect.left, y: rect.bottom };\n}\n\n/**\n * Decides when a context menu asks to open, and where. It renders nothing,\n * and knows nothing about ARIA or the menu.\n *\n * - **`contextmenu`:** a right-click, and Shift+F10 or the context menu key\n * where the browser fires the event for them, asks to open at the point\n * `contextMenuPoint` works out, and prevents the browser's own menu. Each\n * one asks again, so a right-click elsewhere on the trigger moves it.\n * - **Long press:** a touch or a pen held still on the trigger for\n * `LONG_PRESS_DELAY` fires a `contextmenu` at what it landed on, so it\n * takes the same path, handlers on the way included. Moving more than\n * `LONG_PRESS_TOLERANCE`, lifting, another pointer coming down anywhere in\n * the document, or `pointercancel`, which is the browser taking the\n * gesture for a scroll or a zoom, cancels it, and a mouse never starts\n * one. A pointer that comes down while another is held starts no long\n * press of its own, on this trigger or another, so two fingers on two rows\n * open nothing. While the finger is down, and only then, a listener on the\n * document hears other pointers coming down. Where the\n * browser fires `contextmenu` for a long press itself, the first of the two\n * opens it, and the end of a touch that opened it is prevented, so lifting\n * doesn't click what is under the finger.\n * - **Who opens:** a handler that calls `preventDefault()` on the\n * `contextmenu` before this one keeps it from asking, which is how a\n * trigger's own handler opts out and how the innermost of nested triggers\n * wins. A long press goes to the innermost trigger that takes it. Events\n * from content portaled out of the trigger, which React brings here\n * anyway, are ignored.\n * - **`disabled`:** nothing asks, and the browser's menu shows.\n *\n * Nothing it scheduled runs after the component unmounts.\n *\n * @function useContextMenuTrigger\n * @param options - How to ask to open, and whether to.\n * @returns Handlers for the trigger element.\n */\nexport function useContextMenuTrigger({\n onOpen,\n disabled,\n}: UseContextMenuTriggerOptions): ContextMenuTriggerHandlers {\n const pressRef = useRef<Press | null>(null);\n /** A long press opened the menu, and the touch that made it hasn't ended. */\n const heldRef = useRef(false);\n\n const cancel = () => {\n clearTimeout(pressRef.current?.timer);\n pressRef.current?.stop();\n pressRef.current = null;\n };\n\n useEffect(\n () => () => {\n clearTimeout(pressRef.current?.timer);\n pressRef.current?.stop();\n pressRef.current = null;\n },\n []\n );\n const isPress = (event: PointerEvent<HTMLElement>) =>\n pressRef.current?.pointerId === event.pointerId;\n\n return {\n onContextMenu: event => {\n if (disabled || event.defaultPrevented || !inTrigger(event)) return;\n event.preventDefault();\n const press = pressRef.current;\n if (press) {\n if (press.opened) return;\n clearTimeout(press.timer);\n press.opened = true;\n heldRef.current = true;\n }\n onOpen(contextMenuPoint(event), event.currentTarget);\n },\n onPointerDown: event => {\n const native = event.nativeEvent;\n if (\n disabled ||\n (event.pointerType !== 'touch' && event.pointerType !== 'pen') ||\n claimed === native ||\n !inTrigger(event)\n ) {\n return;\n }\n claimed = native;\n heldRef.current = false;\n const target = event.target as Element;\n const doc = target.ownerDocument;\n // Another pointer coming down anywhere makes it a pinch or a second\n // touch, not a long press. In the capture phase, so it's heard before\n // any trigger it lands on, which then leaves it. The press's own\n // pointerdown has passed the document already, so it isn't heard.\n const another = (other: Event) => {\n claimed = other;\n cancel();\n };\n doc.addEventListener('pointerdown', another, true);\n const press: Press = {\n pointerId: event.pointerId,\n target,\n x: event.clientX,\n y: event.clientY,\n opened: false,\n stop: () => doc.removeEventListener('pointerdown', another, true),\n };\n press.timer = setTimeout(() => {\n const view = press.target.ownerDocument.defaultView as typeof window;\n press.target.dispatchEvent(\n new view.MouseEvent('contextmenu', {\n bubbles: true,\n cancelable: true,\n composed: true,\n clientX: press.x,\n clientY: press.y,\n button: 2,\n })\n );\n }, LONG_PRESS_DELAY);\n pressRef.current = press;\n },\n onPointerMove: event => {\n const press = pressRef.current;\n if (!press || !isPress(event) || press.opened) return;\n const moved = Math.hypot(\n event.clientX - press.x,\n event.clientY - press.y\n );\n if (moved > LONG_PRESS_TOLERANCE) cancel();\n },\n onPointerUp: event => {\n if (isPress(event)) cancel();\n },\n onPointerCancel: event => {\n if (isPress(event)) cancel();\n },\n onTouchEnd: event => {\n if (!heldRef.current) return;\n heldRef.current = false;\n event.preventDefault();\n },\n };\n}\n"],"names":[],"mappings":";;;AAAA;AAiCA;AACO,MAAM,gBAAgB,GAAG;AAEhC;AACO,MAAM,oBAAoB,GAAG;AAgBpC;;;;;;;AAOG;AACH,IAAI,OAAO,GAAiB,IAAI;AAEhC;AACA,MAAM,SAAS,GAAG,CAAC,KAAsD,KACvE,KAAK,CAAC,aAAa,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAc,CAAC;AAEpD;;;;;AAKG;AACH,SAAS,gBAAgB,CAAC,KAA8B,EAAA;IACtD,MAAM,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,GAAG,KAAK;IACxC,MAAM,IAAI,GAAI,KAAK,CAAC,MAAkB,CAAC,qBAAqB,EAAE;IAC9D,OAAO,CAAC,IAAI,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,IAAI,CAAC,KAAK,IAAI,CAAC,IAAI,IAAI,CAAC,GAAG,IAAI,CAAC,IAAI,IAAI,CAAC;AACrE,UAAE,EAAE,CAAC,EAAE,CAAC;AACR,UAAE,EAAE,CAAC,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,EAAE,IAAI,CAAC,MAAM,EAAE;AACtC;AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkCG;SACa,qBAAqB,CAAC,EACpC,MAAM,EACN,QAAQ,GACqB,EAAA;AAC7B,IAAA,MAAM,QAAQ,GAAG,MAAM,CAAe,IAAI,CAAC;;AAE3C,IAAA,MAAM,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC;IAE7B,MAAM,MAAM,GAAG,MAAK;AAClB,QAAA,YAAY,CAAC,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC;AACrC,QAAA,QAAQ,CAAC,OAAO,EAAE,IAAI,EAAE;AACxB,QAAA,QAAQ,CAAC,OAAO,GAAG,IAAI;AACzB,IAAA,CAAC;AAED,IAAA,SAAS,CACP,MAAM,MAAK;AACT,QAAA,YAAY,CAAC,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC;AACrC,QAAA,QAAQ,CAAC,OAAO,EAAE,IAAI,EAAE;AACxB,QAAA,QAAQ,CAAC,OAAO,GAAG,IAAI;IACzB,CAAC,EACD,EAAE,CACH;AACD,IAAA,MAAM,OAAO,GAAG,CAAC,KAAgC,KAC/C,QAAQ,CAAC,OAAO,EAAE,SAAS,KAAK,KAAK,CAAC,SAAS;IAEjD,OAAO;QACL,aAAa,EAAE,KAAK,IAAG;YACrB,IAAI,QAAQ,IAAI,KAAK,CAAC,gBAAgB,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC;gBAAE;YAC7D,KAAK,CAAC,cAAc,EAAE;AACtB,YAAA,MAAM,KAAK,GAAG,QAAQ,CAAC,OAAO;YAC9B,IAAI,KAAK,EAAE;gBACT,IAAI,KAAK,CAAC,MAAM;oBAAE;AAClB,gBAAA,YAAY,CAAC,KAAK,CAAC,KAAK,CAAC;AACzB,gBAAA,KAAK,CAAC,MAAM,GAAG,IAAI;AACnB,gBAAA,OAAO,CAAC,OAAO,GAAG,IAAI;YACxB;YACA,MAAM,CAAC,gBAAgB,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;QACtD,CAAC;QACD,aAAa,EAAE,KAAK,IAAG;AACrB,YAAA,MAAM,MAAM,GAAG,KAAK,CAAC,WAAW;AAChC,YAAA,IACE,QAAQ;iBACP,KAAK,CAAC,WAAW,KAAK,OAAO,IAAI,KAAK,CAAC,WAAW,KAAK,KAAK,CAAC;AAC9D,gBAAA,OAAO,KAAK,MAAM;AAClB,gBAAA,CAAC,SAAS,CAAC,KAAK,CAAC,EACjB;gBACA;YACF;YACA,OAAO,GAAG,MAAM;AAChB,YAAA,OAAO,CAAC,OAAO,GAAG,KAAK;AACvB,YAAA,MAAM,MAAM,GAAG,KAAK,CAAC,MAAiB;AACtC,YAAA,MAAM,GAAG,GAAG,MAAM,CAAC,aAAa;;;;;AAKhC,YAAA,MAAM,OAAO,GAAG,CAAC,KAAY,KAAI;gBAC/B,OAAO,GAAG,KAAK;AACf,gBAAA,MAAM,EAAE;AACV,YAAA,CAAC;YACD,GAAG,CAAC,gBAAgB,CAAC,aAAa,EAAE,OAAO,EAAE,IAAI,CAAC;AAClD,YAAA,MAAM,KAAK,GAAU;gBACnB,SAAS,EAAE,KAAK,CAAC,SAAS;gBAC1B,MAAM;gBACN,CAAC,EAAE,KAAK,CAAC,OAAO;gBAChB,CAAC,EAAE,KAAK,CAAC,OAAO;AAChB,gBAAA,MAAM,EAAE,KAAK;AACb,gBAAA,IAAI,EAAE,MAAM,GAAG,CAAC,mBAAmB,CAAC,aAAa,EAAE,OAAO,EAAE,IAAI,CAAC;aAClE;AACD,YAAA,KAAK,CAAC,KAAK,GAAG,UAAU,CAAC,MAAK;gBAC5B,MAAM,IAAI,GAAG,KAAK,CAAC,MAAM,CAAC,aAAa,CAAC,WAA4B;gBACpE,KAAK,CAAC,MAAM,CAAC,aAAa,CACxB,IAAI,IAAI,CAAC,UAAU,CAAC,aAAa,EAAE;AACjC,oBAAA,OAAO,EAAE,IAAI;AACb,oBAAA,UAAU,EAAE,IAAI;AAChB,oBAAA,QAAQ,EAAE,IAAI;oBACd,OAAO,EAAE,KAAK,CAAC,CAAC;oBAChB,OAAO,EAAE,KAAK,CAAC,CAAC;AAChB,oBAAA,MAAM,EAAE,CAAC;AACV,iBAAA,CAAC,CACH;YACH,CAAC,EAAE,gBAAgB,CAAC;AACpB,YAAA,QAAQ,CAAC,OAAO,GAAG,KAAK;QAC1B,CAAC;QACD,aAAa,EAAE,KAAK,IAAG;AACrB,YAAA,MAAM,KAAK,GAAG,QAAQ,CAAC,OAAO;YAC9B,IAAI,CAAC,KAAK,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,MAAM;gBAAE;YAC/C,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CACtB,KAAK,CAAC,OAAO,GAAG,KAAK,CAAC,CAAC,EACvB,KAAK,CAAC,OAAO,GAAG,KAAK,CAAC,CAAC,CACxB;YACD,IAAI,KAAK,GAAG,oBAAoB;AAAE,gBAAA,MAAM,EAAE;QAC5C,CAAC;QACD,WAAW,EAAE,KAAK,IAAG;YACnB,IAAI,OAAO,CAAC,KAAK,CAAC;AAAE,gBAAA,MAAM,EAAE;QAC9B,CAAC;QACD,eAAe,EAAE,KAAK,IAAG;YACvB,IAAI,OAAO,CAAC,KAAK,CAAC;AAAE,gBAAA,MAAM,EAAE;QAC9B,CAAC;QACD,UAAU,EAAE,KAAK,IAAG;YAClB,IAAI,CAAC,OAAO,CAAC,OAAO;gBAAE;AACtB,YAAA,OAAO,CAAC,OAAO,GAAG,KAAK;YACvB,KAAK,CAAC,cAAc,EAAE;QACxB,CAAC;KACF;AACH;;"}
@@ -4,6 +4,16 @@ import { useClientLayoutEffect } from './useClientLayoutEffect.js';
4
4
  import { isEventInside, getDeepestActiveElement } from './shadowDom.js';
5
5
 
6
6
  // INTERNAL: deliberately not exported from src/index.ts.
7
+ /**
8
+ * Runs the caller's own handler on the element that holds the trigger, then
9
+ * the hook's, so neither replaces the other.
10
+ */
11
+ function chain$1(own, ours) {
12
+ return event => {
13
+ own?.(event);
14
+ ours(event);
15
+ };
16
+ }
7
17
  /**
8
18
  * Whether focus that just arrived on `el` is keyboard focus, as the browser
9
19
  * decides it for `:focus-visible`: focus from a key or from the browser's
@@ -45,7 +55,8 @@ let openings = 0;
45
55
  *
46
56
  * The keypress belongs to whatever has focus. When that is one of the open
47
57
  * floating elements or their triggers, or nothing, the listener stops it in
48
- * the capture phase, before anything around (a Popover, a Modal) sees it.
58
+ * the capture phase, before anything around (a Popover, a Modal) sees it,
59
+ * unless the one it closes was set not to consume Escape.
49
60
  * When focus is anywhere else, such as a combobox or a dialog of its own,
50
61
  * the floating element still closes, since hover put it there without
51
62
  * focus, and the key goes on to that owner.
@@ -79,7 +90,8 @@ function wantEscape(doc, entry) {
79
90
  }
80
91
  if (!top)
81
92
  return;
82
- if (holding.length > 0 || !focused || focused === doc.body) {
93
+ if (top.consumes() &&
94
+ (holding.length > 0 || !focused || focused === doc.body)) {
83
95
  event.preventDefault();
84
96
  event.stopPropagation();
85
97
  }
@@ -111,7 +123,8 @@ function wantEscape(doc, entry) {
111
123
  * - The pointer entering, or keyboard focus arriving on the trigger, asks to
112
124
  * open after `openDelay`. A touch pointer doesn't count. Neither does focus
113
125
  * that a press brings to the trigger: it counts when the browser shows it
114
- * as `:focus-visible`.
126
+ * as `:focus-visible`. The pointer arriving from outside a shadow root the
127
+ * trigger renders into counts too, though React sends no enter for it.
115
128
  * - Focus in the floating element holds it open however it got there,
116
129
  * including focus in content it portals elsewhere, such as a nested card.
117
130
  * Whether a click focuses what it lands on is up to the browser: Safari
@@ -122,8 +135,9 @@ function wantEscape(doc, entry) {
122
135
  * else closes it on a timer.
123
136
  * - Escape closes it at once, wherever focus is, and stops there when focus
124
137
  * is in it, on its trigger or nowhere, so a `Popover` or `Modal` around it
125
- * keeps its own. With focus somewhere else, the key goes on after closing
126
- * it. Inside another one, the inner one closes first. Escape before it
138
+ * keeps its own. With focus somewhere else, or with `consumeEscape` off,
139
+ * the key goes on after closing it. Inside another one, the inner one
140
+ * closes first. Escape before it
127
141
  * opens takes the opening back and goes on its way. One key listener per
128
142
  * document serves every hook on it, there only while one of them is open
129
143
  * or on its way to open.
@@ -199,6 +213,7 @@ function useHoverOpen(options) {
199
213
  clearTimeout(stateRef.current.timer);
200
214
  latestRef.current.onOpenChange(false);
201
215
  },
216
+ consumes: () => latestRef.current.consumeEscape !== false,
202
217
  });
203
218
  // Asks to open or to close. Escape can take an ask to open back until it is
204
219
  // answered: by the commit that follows it, or, when the caller keeps it
@@ -330,6 +345,25 @@ function useHoverOpen(options) {
330
345
  stateRef.current.pointerIn = true;
331
346
  settle();
332
347
  },
348
+ onPointerOver: event => {
349
+ // React makes the enter out of the pointerout on the element the
350
+ // pointer left, and skips this pointerover when that element is its
351
+ // own. Leaving page content for a trigger inside a shadow root, that
352
+ // pointerout names only the shadow host, so no enter reaches here.
353
+ // A pointerover from outside the holder and the floating element is
354
+ // the pointer arriving, then. One from inside them is the pointer
355
+ // moving about within, which doesn't bring back a closed one.
356
+ const s = stateRef.current;
357
+ if (s.pointerIn || event.pointerType === 'touch')
358
+ return;
359
+ const from = event.relatedTarget;
360
+ if (within(event.currentTarget, from) ||
361
+ within(latestRef.current.floatingRef.current, from)) {
362
+ return;
363
+ }
364
+ s.pointerIn = true;
365
+ settle();
366
+ },
333
367
  onPointerLeave: event => {
334
368
  // A touch pointer never set pointerIn, so its leave has nothing to
335
369
  // clear, and on a device with a mouse too it would close a card the
@@ -386,5 +420,5 @@ function useHoverOpen(options) {
386
420
  }, [settle, ask]);
387
421
  }
388
422
 
389
- export { useHoverOpen };
423
+ export { chain$1 as chain, useHoverOpen };
390
424
  //# sourceMappingURL=useHoverOpen.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"useHoverOpen.js","sources":["../../../src/helpers/useHoverOpen.ts"],"sourcesContent":["// INTERNAL: deliberately not exported from src/index.ts.\nimport { useCallback, useMemo, useRef } from 'react';\nimport type { FocusEvent, PointerEvent, RefObject } from 'react';\nimport { useClientLayoutEffect } from './useClientLayoutEffect';\nimport { getDeepestActiveElement, isEventInside } from './shadowDom';\n\n/**\n * Options for `useHoverOpen`.\n */\nexport interface UseHoverOpenOptions {\n /** Whether the floating element is open now, controlled or not. */\n open: boolean;\n /**\n * Asks for the floating element to open or close: when a delay runs out,\n * and at once on Escape. It isn't asked for the state it is already in,\n * except that an Escape between asking to open and the open showing takes\n * the ask back by asking to close.\n */\n onOpenChange: (open: boolean) => void;\n /** How long hover or keyboard focus waits before asking to open, in milliseconds. */\n openDelay: number;\n /**\n * How long it waits once the pointer and focus have both left before\n * asking to close, in milliseconds. Coming back in that time keeps it open,\n * which is how the pointer crosses a gap to the floating element.\n */\n closeDelay: number;\n /**\n * The element that opens it, which has to take focus itself. The Escape\n * listener goes on its document, an iframe's when it renders into one, and\n * focus left in the floating element comes back here when the floating\n * element closes.\n */\n triggerRef: RefObject<HTMLElement | null>;\n /** The floating element, while it is open. */\n floatingRef: RefObject<HTMLElement | null>;\n /**\n * Whether a tap on the trigger opens it at once, with the next tap closing\n * it. Without it, a touch pointer never opens it. While a tap holds it\n * open, no hover timer closes it, and a press outside the trigger and the\n * floating element does, as Escape does. The trigger's own tap action\n * still runs.\n * @defaultValue false\n */\n openOnTap?: boolean;\n}\n\n/**\n * Pointer and focus handlers for the element that holds the trigger. The\n * floating element has to be inside it in React's tree, in place or through\n * a portal, since React's events reach it from there either way, and it\n * should hold nothing else: focus inside it that isn't on the trigger counts\n * as focus in the floating element.\n */\nexport interface HoverOpenHandlers {\n onPointerEnter: (event: PointerEvent<HTMLElement>) => void;\n onPointerLeave: (event: PointerEvent<HTMLElement>) => void;\n onFocus: (event: FocusEvent<HTMLElement>) => void;\n onBlur: (event: FocusEvent<HTMLElement>) => void;\n onPointerDown: (event: PointerEvent<HTMLElement>) => void;\n onPointerUp: (event: PointerEvent<HTMLElement>) => void;\n}\n\n/**\n * Whether focus that just arrived on `el` is keyboard focus, as the browser\n * decides it for `:focus-visible`: focus from a key or from the browser's\n * own controls is, focus a press brings isn't, and focus a script moves\n * follows whichever came last. A browser that can't match the selector\n * throws, and then every focus counts, so keyboard users can still open it.\n */\nfunction isKeyboardFocus(el: Element): boolean {\n try {\n return el.matches(':focus-visible');\n } catch {\n return true;\n }\n}\n\n/** Whether `node` is `container` or inside it, when there are both. */\nconst within = (container: Node | null, node: Node | null): boolean =>\n Boolean(container && node && container.contains(node));\n\n/** A hook that wants Escape: its floating element is open, or an open is on its way. */\ninterface EscapeEntry {\n /** When its floating element opened, counted across the page, or 0 while it is closed. */\n openedAt: number;\n trigger: () => HTMLElement | null;\n floating: () => HTMLElement | null;\n /** Takes back an open that is on its way: waiting on its delay, or asked for and not showing yet. */\n cancel: () => void;\n /** Asks to close. */\n close: () => void;\n}\n\n/**\n * Per document, the hooks that want Escape and the one listener they share\n * while there are any, so a card inside another card doesn't add a second\n * listener that closes both on one keypress.\n */\nconst escapeListeners = new Map<\n Document,\n { entries: Set<EscapeEntry>; stop: () => void }\n>();\n\n/** Counts openings across the page, so the most recent one is known. */\nlet openings = 0;\n\n/**\n * Adds `entry` to the hooks that want Escape on `doc`, starting the shared\n * listener when it is the first.\n *\n * Escape takes back every open that is on its way, so nothing appears right\n * after it, and closes one open floating element, chosen by these rules in\n * turn:\n *\n * 1. One that focus is in, on its trigger or inside it, when there is one.\n * 2. The innermost: one whose floating element holds no other's trigger.\n * That follows nesting whether either one is portaled, since an inner\n * trigger sits in the outer floating element either way.\n * 3. The one that opened last.\n *\n * The keypress belongs to whatever has focus. When that is one of the open\n * floating elements or their triggers, or nothing, the listener stops it in\n * the capture phase, before anything around (a Popover, a Modal) sees it.\n * When focus is anywhere else, such as a combobox or a dialog of its own,\n * the floating element still closes, since hover put it there without\n * focus, and the key goes on to that owner.\n *\n * @returns What to call when the hook stops wanting Escape.\n */\nfunction wantEscape(doc: Document, entry: EscapeEntry): () => void {\n let listener = escapeListeners.get(doc);\n if (!listener) {\n const entries = new Set<EscapeEntry>();\n const handleKeyDown = (event: KeyboardEvent) => {\n if (event.key !== 'Escape') return;\n const open: EscapeEntry[] = [];\n // A copy, since taking an open back can take its hook out of the set.\n for (const each of [...entries]) {\n if (each.openedAt) open.push(each);\n else each.cancel();\n }\n const focused = getDeepestActiveElement(doc);\n const holding = open.filter(\n each =>\n within(each.trigger(), focused) || within(each.floating(), focused)\n );\n const pool = holding.length > 0 ? holding : open;\n let top: EscapeEntry | undefined;\n for (const each of pool) {\n const holdsAnother = pool.some(\n other => other !== each && within(each.floating(), other.trigger())\n );\n if (!holdsAnother && (!top || each.openedAt > top.openedAt)) {\n top = each;\n }\n }\n if (!top) return;\n if (holding.length > 0 || !focused || focused === doc.body) {\n event.preventDefault();\n event.stopPropagation();\n }\n top.close();\n };\n doc.addEventListener('keydown', handleKeyDown, true);\n listener = {\n entries,\n stop: () => {\n doc.removeEventListener('keydown', handleKeyDown, true);\n escapeListeners.delete(doc);\n },\n };\n escapeListeners.set(doc, listener);\n }\n const shared = listener;\n shared.entries.add(entry);\n return () => {\n shared.entries.delete(entry);\n if (shared.entries.size === 0) shared.stop();\n };\n}\n\ninterface HoverOpenState {\n /** The pointer, other than a touch, is over the trigger or the floating element. */\n pointerIn: boolean;\n /** Focus holds it open: any focus in the floating element, or keyboard focus on the trigger. */\n focusIn: boolean;\n /** Focus, however it got there, is in the floating element. */\n focusInFloating: boolean;\n /** Whether the pointer or focus was holding it open when last settled. */\n held: boolean;\n /** An open is waiting on its delay. */\n opening: boolean;\n /**\n * An open was asked for and isn't showing yet: no commit has answered it,\n * and the event that asked isn't over.\n */\n asked: boolean;\n /** Focus is on its way back to the trigger, which isn't focus arriving. */\n restoring: boolean;\n timer?: ReturnType<typeof setTimeout>;\n /** Ends an ask to open that no commit answered, once the event that made it is over. */\n askTimer?: ReturnType<typeof setTimeout>;\n /** Takes this hook out of the ones that want Escape, while it is in. */\n releaseEscape?: () => void;\n /**\n * The document of the tap that opened it, or asked to: a press outside in\n * that document closes it while it is open.\n */\n tappedIn?: Document;\n /** The last press React brought to the holder: on the trigger or in the floating element's tree. */\n pressedInside?: Event;\n /** Takes the listener for a press outside off its document, while it is on. */\n stopPress?: () => void;\n}\n\n/**\n * Decides when a floating element that opens on hover or keyboard focus asks\n * to open and to close. It renders nothing, and leaves roles, ARIA and\n * placement to the component that calls it.\n *\n * - The pointer entering, or keyboard focus arriving on the trigger, asks to\n * open after `openDelay`. A touch pointer doesn't count. Neither does focus\n * that a press brings to the trigger: it counts when the browser shows it\n * as `:focus-visible`.\n * - Focus in the floating element holds it open however it got there,\n * including focus in content it portals elsewhere, such as a nested card.\n * Whether a click focuses what it lands on is up to the browser: Safari\n * and Firefox on macOS don't focus a button on click, so there a clicked\n * button doesn't hold it once the pointer leaves.\n * - Once the pointer and focus have both left, it asks to close after\n * `closeDelay`, and either coming back before then keeps it open. Nothing\n * else closes it on a timer.\n * - Escape closes it at once, wherever focus is, and stops there when focus\n * is in it, on its trigger or nowhere, so a `Popover` or `Modal` around it\n * keeps its own. With focus somewhere else, the key goes on after closing\n * it. Inside another one, the inner one closes first. Escape before it\n * opens takes the opening back and goes on its way. One key listener per\n * document serves every hook on it, there only while one of them is open\n * or on its way to open.\n * - With `openOnTap`, a tap on the trigger opens it at once and the next tap\n * on the trigger closes it. While a tap holds it open, a press outside the\n * trigger and the floating element closes it, from a listener on the\n * document that is there only then. Taps never count as hover, and mouse\n * and pen work the same with it or without.\n * - Once it closes, for any reason, whatever was holding it open has to\n * leave and come back to open it again.\n * - When the floating element closes with focus inside it, focus goes back\n * to the trigger, unless the same update put it somewhere else. Handing it\n * back doesn't count as focus arriving.\n *\n * Nothing it scheduled runs after the component unmounts.\n *\n * @function useHoverOpen\n * @param options - The open state, how to ask for another, the delays, and the two elements.\n * @returns Handlers for the element that holds the trigger.\n */\nexport function useHoverOpen(options: UseHoverOpenOptions): HoverOpenHandlers {\n const latestRef = useRef(options);\n const stateRef = useRef<HoverOpenState>({\n pointerIn: false,\n focusIn: false,\n focusInFloating: false,\n held: false,\n opening: false,\n asked: false,\n restoring: false,\n });\n\n // In the hooks that want Escape from the moment an open is on its way until\n // the floating element closes, and out otherwise. Stamped with the order it\n // opened in.\n const listenForEscape = useCallback(function listen() {\n const s = stateRef.current;\n const entry = entryRef.current;\n const { open, triggerRef } = latestRef.current;\n if (!open) entry.openedAt = 0;\n else if (!entry.openedAt) {\n openings += 1;\n entry.openedAt = openings;\n }\n const wanted = open || s.opening || s.asked;\n if (wanted === Boolean(s.releaseEscape)) return;\n if (s.releaseEscape) {\n s.releaseEscape();\n s.releaseEscape = undefined;\n return;\n }\n const doc = triggerRef.current?.ownerDocument ?? document;\n s.releaseEscape = wantEscape(doc, entry);\n }, []);\n\n const entryRef = useRef<EscapeEntry>({\n openedAt: 0,\n trigger: () => latestRef.current.triggerRef.current,\n floating: () => latestRef.current.floatingRef.current,\n // Only ever called while an open is on its way, so it is one or the\n // other: still waiting on its delay, or asked for already.\n cancel: () => {\n const s = stateRef.current;\n clearTimeout(s.timer);\n // An ask already made is taken back by asking the other way.\n if (s.asked) latestRef.current.onOpenChange(false);\n s.opening = false;\n s.asked = false;\n listenForEscape();\n },\n close: () => {\n clearTimeout(stateRef.current.timer);\n latestRef.current.onOpenChange(false);\n },\n });\n\n // Asks to open or to close. Escape can take an ask to open back until it is\n // answered: by the commit that follows it, or, when the caller keeps it\n // closed without rendering at all, once the event that asked is over, by\n // which time React has rendered any answer the caller set.\n const ask = useCallback(\n (next: boolean) => {\n const s = stateRef.current;\n s.asked = next;\n latestRef.current.onOpenChange(next);\n listenForEscape();\n if (!next) return;\n clearTimeout(s.askTimer);\n s.askTimer = setTimeout(() => {\n // Still closed: the caller turned it down.\n if (latestRef.current.open) return;\n s.asked = false;\n s.tappedIn = undefined;\n listenForEscape();\n });\n },\n [listenForEscape]\n );\n\n // Asks for what the pointer and focus call for, after its delay, when that\n // changed since the last time and isn't the state already.\n const settle = useCallback(() => {\n const s = stateRef.current;\n const held = s.pointerIn || s.focusIn;\n if (held !== s.held) {\n s.held = held;\n clearTimeout(s.timer);\n const { open, openDelay, closeDelay } = latestRef.current;\n s.opening = held && !open;\n if (held !== open) {\n s.timer = setTimeout(\n () => {\n s.opening = false;\n if (latestRef.current.open !== held) ask(held);\n else listenForEscape();\n },\n held ? openDelay : closeDelay\n );\n }\n }\n listenForEscape();\n }, [listenForEscape, ask]);\n\n // While a tap holds the floating element open, a press outside the trigger\n // and the floating element closes it. Set up again each time it opens or\n // closes.\n const listenForPress = useCallback(() => {\n const s = stateRef.current;\n s.stopPress?.();\n s.stopPress = undefined;\n const doc = latestRef.current.open ? s.tappedIn : undefined;\n if (!doc) return;\n const handlePointerDown = (event: Event) => {\n // Inside is a press React brought to the holder, which covers content\n // the floating element portals elsewhere, or one whose path runs\n // through the trigger or the floating element, which covers a press\n // that something inside stopped before React got it to the holder.\n const { triggerRef, floatingRef } = latestRef.current;\n if (\n event === s.pressedInside ||\n isEventInside(event, triggerRef.current) ||\n isEventInside(event, floatingRef.current)\n ) {\n return;\n }\n latestRef.current.onOpenChange(false);\n };\n doc.addEventListener('pointerdown', handlePointerDown);\n s.stopPress = () =>\n doc.removeEventListener('pointerdown', handlePointerDown);\n }, []);\n\n useClientLayoutEffect(() => {\n latestRef.current = options;\n // The commit after an ask to open answers it, open or still closed. A\n // tap that asked and still finds it closed was turned down.\n const s = stateRef.current;\n if (s.asked) {\n s.asked = false;\n if (!options.open) s.tappedIn = undefined;\n listenForEscape();\n }\n });\n\n useClientLayoutEffect(() => {\n const s = stateRef.current;\n return () => {\n clearTimeout(s.timer);\n clearTimeout(s.askTimer);\n s.opening = false;\n s.asked = false;\n s.releaseEscape?.();\n s.releaseEscape = undefined;\n s.stopPress?.();\n s.stopPress = undefined;\n };\n }, []);\n\n const { open } = options;\n // Before paint, so a pointer or a key arriving next finds this settled.\n useClientLayoutEffect(() => {\n const s = stateRef.current;\n if (!open) {\n // The floating element left from under the pointer, if it was there,\n // and no leave event says so. Focus resting on the trigger has done\n // its job too, so neither opens it again until it comes back.\n s.pointerIn = false;\n s.focusIn = false;\n s.tappedIn = undefined;\n if (s.focusInFloating) {\n s.focusInFloating = false;\n const trigger = latestRef.current.triggerRef.current;\n // Focus that went down with the floating element sits on <body>.\n // Focus somewhere else was put there by the same update, so it stays.\n if (\n trigger &&\n trigger.ownerDocument.activeElement === trigger.ownerDocument.body\n ) {\n s.restoring = true;\n trigger.focus();\n s.restoring = false;\n }\n }\n }\n settle();\n listenForPress();\n }, [open, settle, listenForPress]);\n\n return useMemo<HoverOpenHandlers>(() => {\n const inTrigger = (node: EventTarget | null) =>\n within(latestRef.current.triggerRef.current, node as Node | null);\n return {\n onPointerEnter: event => {\n if (event.pointerType === 'touch') return;\n stateRef.current.pointerIn = true;\n settle();\n },\n onPointerLeave: event => {\n // A touch pointer never set pointerIn, so its leave has nothing to\n // clear, and on a device with a mouse too it would close a card the\n // mouse is still holding open.\n if (event.pointerType === 'touch') return;\n stateRef.current.pointerIn = false;\n settle();\n },\n onFocus: event => {\n const s = stateRef.current;\n // React brings focus here from the trigger and from the floating\n // element's whole tree, portals included, so focus not in the\n // trigger is in the floating element, wherever its DOM is.\n s.focusInFloating = !inTrigger(event.target);\n if (s.restoring) return;\n // Any focus in the floating element holds it. On the trigger, only\n // keyboard focus does, so a click there opens nothing.\n s.focusIn = s.focusInFloating || isKeyboardFocus(event.target);\n settle();\n },\n onBlur: event => {\n const s = stateRef.current;\n const next = event.relatedTarget as Node | null;\n // Where focus goes next, as far as the DOM can say. Focus going into\n // content the floating element portals elsewhere reads as leaving,\n // and the focus event that follows straight after brings it back.\n s.focusInFloating = within(latestRef.current.floatingRef.current, next);\n // Moving between the trigger and the floating element isn't leaving.\n if (s.focusInFloating || within(event.currentTarget, next)) return;\n s.focusIn = false;\n settle();\n },\n onPointerDown: event => {\n stateRef.current.pressedInside = event.nativeEvent;\n },\n onPointerUp: event => {\n const now = latestRef.current;\n // With openOnTap, a tap on the trigger opens it at once, and the\n // next one closes it. A tap anywhere else does nothing here.\n if (\n !now.openOnTap ||\n event.pointerType !== 'touch' ||\n !inTrigger(event.target)\n ) {\n return;\n }\n stateRef.current.tappedIn = now.open\n ? undefined\n : (event.target as Element).ownerDocument;\n ask(!now.open);\n },\n };\n }, [settle, ask]);\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"useHoverOpen.js","sources":["../../../src/helpers/useHoverOpen.ts"],"sourcesContent":["// INTERNAL: deliberately not exported from src/index.ts.\nimport { useCallback, useMemo, useRef } from 'react';\nimport type { FocusEvent, PointerEvent, RefObject } from 'react';\nimport { useClientLayoutEffect } from './useClientLayoutEffect';\nimport { getDeepestActiveElement, isEventInside } from './shadowDom';\n\n/**\n * Options for `useHoverOpen`.\n */\nexport interface UseHoverOpenOptions {\n /** Whether the floating element is open now, controlled or not. */\n open: boolean;\n /**\n * Asks for the floating element to open or close: when a delay runs out,\n * and at once on Escape. It isn't asked for the state it is already in,\n * except that an Escape between asking to open and the open showing takes\n * the ask back by asking to close.\n */\n onOpenChange: (open: boolean) => void;\n /** How long hover or keyboard focus waits before asking to open, in milliseconds. */\n openDelay: number;\n /**\n * How long it waits once the pointer and focus have both left before\n * asking to close, in milliseconds. Coming back in that time keeps it open,\n * which is how the pointer crosses a gap to the floating element.\n */\n closeDelay: number;\n /**\n * The element that opens it, which has to take focus itself. The Escape\n * listener goes on its document, an iframe's when it renders into one, and\n * focus left in the floating element comes back here when the floating\n * element closes.\n */\n triggerRef: RefObject<HTMLElement | null>;\n /** The floating element, while it is open. */\n floatingRef: RefObject<HTMLElement | null>;\n /**\n * Whether a tap on the trigger opens it at once, with the next tap closing\n * it. Without it, a touch pointer never opens it. While a tap holds it\n * open, no hover timer closes it, and a press outside the trigger and the\n * floating element does, as Escape does. The trigger's own tap action\n * still runs.\n * @defaultValue false\n */\n openOnTap?: boolean;\n /**\n * Whether an Escape that closes it stops there when focus is on the\n * trigger, in the floating element or nowhere, so a `Popover` or `Modal`\n * around it keeps its own. Off, Escape still closes it and always goes on\n * to whatever has focus, as suits content that only adds to the trigger.\n * @defaultValue true\n */\n consumeEscape?: boolean;\n}\n\n/**\n * Pointer and focus handlers for the element that holds the trigger. The\n * floating element has to be inside it in React's tree, in place or through\n * a portal, since React's events reach it from there either way, and it\n * should hold nothing else: focus inside it that isn't on the trigger counts\n * as focus in the floating element.\n */\nexport interface HoverOpenHandlers {\n onPointerEnter: (event: PointerEvent<HTMLElement>) => void;\n onPointerOver: (event: PointerEvent<HTMLElement>) => void;\n onPointerLeave: (event: PointerEvent<HTMLElement>) => void;\n onFocus: (event: FocusEvent<HTMLElement>) => void;\n onBlur: (event: FocusEvent<HTMLElement>) => void;\n onPointerDown: (event: PointerEvent<HTMLElement>) => void;\n onPointerUp: (event: PointerEvent<HTMLElement>) => void;\n}\n\n/**\n * Runs the caller's own handler on the element that holds the trigger, then\n * the hook's, so neither replaces the other.\n */\nexport function chain<E>(\n own: ((event: E) => void) | undefined,\n ours: (event: E) => void\n): (event: E) => void {\n return event => {\n own?.(event);\n ours(event);\n };\n}\n\n/**\n * Whether focus that just arrived on `el` is keyboard focus, as the browser\n * decides it for `:focus-visible`: focus from a key or from the browser's\n * own controls is, focus a press brings isn't, and focus a script moves\n * follows whichever came last. A browser that can't match the selector\n * throws, and then every focus counts, so keyboard users can still open it.\n */\nfunction isKeyboardFocus(el: Element): boolean {\n try {\n return el.matches(':focus-visible');\n } catch {\n return true;\n }\n}\n\n/** Whether `node` is `container` or inside it, when there are both. */\nconst within = (container: Node | null, node: Node | null): boolean =>\n Boolean(container && node && container.contains(node));\n\n/** A hook that wants Escape: its floating element is open, or an open is on its way. */\ninterface EscapeEntry {\n /** When its floating element opened, counted across the page, or 0 while it is closed. */\n openedAt: number;\n trigger: () => HTMLElement | null;\n floating: () => HTMLElement | null;\n /** Takes back an open that is on its way: waiting on its delay, or asked for and not showing yet. */\n cancel: () => void;\n /** Asks to close. */\n close: () => void;\n /** Whether an Escape that closes it can stop there. */\n consumes: () => boolean;\n}\n\n/**\n * Per document, the hooks that want Escape and the one listener they share\n * while there are any, so a card inside another card doesn't add a second\n * listener that closes both on one keypress.\n */\nconst escapeListeners = new Map<\n Document,\n { entries: Set<EscapeEntry>; stop: () => void }\n>();\n\n/** Counts openings across the page, so the most recent one is known. */\nlet openings = 0;\n\n/**\n * Adds `entry` to the hooks that want Escape on `doc`, starting the shared\n * listener when it is the first.\n *\n * Escape takes back every open that is on its way, so nothing appears right\n * after it, and closes one open floating element, chosen by these rules in\n * turn:\n *\n * 1. One that focus is in, on its trigger or inside it, when there is one.\n * 2. The innermost: one whose floating element holds no other's trigger.\n * That follows nesting whether either one is portaled, since an inner\n * trigger sits in the outer floating element either way.\n * 3. The one that opened last.\n *\n * The keypress belongs to whatever has focus. When that is one of the open\n * floating elements or their triggers, or nothing, the listener stops it in\n * the capture phase, before anything around (a Popover, a Modal) sees it,\n * unless the one it closes was set not to consume Escape.\n * When focus is anywhere else, such as a combobox or a dialog of its own,\n * the floating element still closes, since hover put it there without\n * focus, and the key goes on to that owner.\n *\n * @returns What to call when the hook stops wanting Escape.\n */\nfunction wantEscape(doc: Document, entry: EscapeEntry): () => void {\n let listener = escapeListeners.get(doc);\n if (!listener) {\n const entries = new Set<EscapeEntry>();\n const handleKeyDown = (event: KeyboardEvent) => {\n if (event.key !== 'Escape') return;\n const open: EscapeEntry[] = [];\n // A copy, since taking an open back can take its hook out of the set.\n for (const each of [...entries]) {\n if (each.openedAt) open.push(each);\n else each.cancel();\n }\n const focused = getDeepestActiveElement(doc);\n const holding = open.filter(\n each =>\n within(each.trigger(), focused) || within(each.floating(), focused)\n );\n const pool = holding.length > 0 ? holding : open;\n let top: EscapeEntry | undefined;\n for (const each of pool) {\n const holdsAnother = pool.some(\n other => other !== each && within(each.floating(), other.trigger())\n );\n if (!holdsAnother && (!top || each.openedAt > top.openedAt)) {\n top = each;\n }\n }\n if (!top) return;\n if (\n top.consumes() &&\n (holding.length > 0 || !focused || focused === doc.body)\n ) {\n event.preventDefault();\n event.stopPropagation();\n }\n top.close();\n };\n doc.addEventListener('keydown', handleKeyDown, true);\n listener = {\n entries,\n stop: () => {\n doc.removeEventListener('keydown', handleKeyDown, true);\n escapeListeners.delete(doc);\n },\n };\n escapeListeners.set(doc, listener);\n }\n const shared = listener;\n shared.entries.add(entry);\n return () => {\n shared.entries.delete(entry);\n if (shared.entries.size === 0) shared.stop();\n };\n}\n\ninterface HoverOpenState {\n /** The pointer, other than a touch, is over the trigger or the floating element. */\n pointerIn: boolean;\n /** Focus holds it open: any focus in the floating element, or keyboard focus on the trigger. */\n focusIn: boolean;\n /** Focus, however it got there, is in the floating element. */\n focusInFloating: boolean;\n /** Whether the pointer or focus was holding it open when last settled. */\n held: boolean;\n /** An open is waiting on its delay. */\n opening: boolean;\n /**\n * An open was asked for and isn't showing yet: no commit has answered it,\n * and the event that asked isn't over.\n */\n asked: boolean;\n /** Focus is on its way back to the trigger, which isn't focus arriving. */\n restoring: boolean;\n timer?: ReturnType<typeof setTimeout>;\n /** Ends an ask to open that no commit answered, once the event that made it is over. */\n askTimer?: ReturnType<typeof setTimeout>;\n /** Takes this hook out of the ones that want Escape, while it is in. */\n releaseEscape?: () => void;\n /**\n * The document of the tap that opened it, or asked to: a press outside in\n * that document closes it while it is open.\n */\n tappedIn?: Document;\n /** The last press React brought to the holder: on the trigger or in the floating element's tree. */\n pressedInside?: Event;\n /** Takes the listener for a press outside off its document, while it is on. */\n stopPress?: () => void;\n}\n\n/**\n * Decides when a floating element that opens on hover or keyboard focus asks\n * to open and to close. It renders nothing, and leaves roles, ARIA and\n * placement to the component that calls it.\n *\n * - The pointer entering, or keyboard focus arriving on the trigger, asks to\n * open after `openDelay`. A touch pointer doesn't count. Neither does focus\n * that a press brings to the trigger: it counts when the browser shows it\n * as `:focus-visible`. The pointer arriving from outside a shadow root the\n * trigger renders into counts too, though React sends no enter for it.\n * - Focus in the floating element holds it open however it got there,\n * including focus in content it portals elsewhere, such as a nested card.\n * Whether a click focuses what it lands on is up to the browser: Safari\n * and Firefox on macOS don't focus a button on click, so there a clicked\n * button doesn't hold it once the pointer leaves.\n * - Once the pointer and focus have both left, it asks to close after\n * `closeDelay`, and either coming back before then keeps it open. Nothing\n * else closes it on a timer.\n * - Escape closes it at once, wherever focus is, and stops there when focus\n * is in it, on its trigger or nowhere, so a `Popover` or `Modal` around it\n * keeps its own. With focus somewhere else, or with `consumeEscape` off,\n * the key goes on after closing it. Inside another one, the inner one\n * closes first. Escape before it\n * opens takes the opening back and goes on its way. One key listener per\n * document serves every hook on it, there only while one of them is open\n * or on its way to open.\n * - With `openOnTap`, a tap on the trigger opens it at once and the next tap\n * on the trigger closes it. While a tap holds it open, a press outside the\n * trigger and the floating element closes it, from a listener on the\n * document that is there only then. Taps never count as hover, and mouse\n * and pen work the same with it or without.\n * - Once it closes, for any reason, whatever was holding it open has to\n * leave and come back to open it again.\n * - When the floating element closes with focus inside it, focus goes back\n * to the trigger, unless the same update put it somewhere else. Handing it\n * back doesn't count as focus arriving.\n *\n * Nothing it scheduled runs after the component unmounts.\n *\n * @function useHoverOpen\n * @param options - The open state, how to ask for another, the delays, and the two elements.\n * @returns Handlers for the element that holds the trigger.\n */\nexport function useHoverOpen(options: UseHoverOpenOptions): HoverOpenHandlers {\n const latestRef = useRef(options);\n const stateRef = useRef<HoverOpenState>({\n pointerIn: false,\n focusIn: false,\n focusInFloating: false,\n held: false,\n opening: false,\n asked: false,\n restoring: false,\n });\n\n // In the hooks that want Escape from the moment an open is on its way until\n // the floating element closes, and out otherwise. Stamped with the order it\n // opened in.\n const listenForEscape = useCallback(function listen() {\n const s = stateRef.current;\n const entry = entryRef.current;\n const { open, triggerRef } = latestRef.current;\n if (!open) entry.openedAt = 0;\n else if (!entry.openedAt) {\n openings += 1;\n entry.openedAt = openings;\n }\n const wanted = open || s.opening || s.asked;\n if (wanted === Boolean(s.releaseEscape)) return;\n if (s.releaseEscape) {\n s.releaseEscape();\n s.releaseEscape = undefined;\n return;\n }\n const doc = triggerRef.current?.ownerDocument ?? document;\n s.releaseEscape = wantEscape(doc, entry);\n }, []);\n\n const entryRef = useRef<EscapeEntry>({\n openedAt: 0,\n trigger: () => latestRef.current.triggerRef.current,\n floating: () => latestRef.current.floatingRef.current,\n // Only ever called while an open is on its way, so it is one or the\n // other: still waiting on its delay, or asked for already.\n cancel: () => {\n const s = stateRef.current;\n clearTimeout(s.timer);\n // An ask already made is taken back by asking the other way.\n if (s.asked) latestRef.current.onOpenChange(false);\n s.opening = false;\n s.asked = false;\n listenForEscape();\n },\n close: () => {\n clearTimeout(stateRef.current.timer);\n latestRef.current.onOpenChange(false);\n },\n consumes: () => latestRef.current.consumeEscape !== false,\n });\n\n // Asks to open or to close. Escape can take an ask to open back until it is\n // answered: by the commit that follows it, or, when the caller keeps it\n // closed without rendering at all, once the event that asked is over, by\n // which time React has rendered any answer the caller set.\n const ask = useCallback(\n (next: boolean) => {\n const s = stateRef.current;\n s.asked = next;\n latestRef.current.onOpenChange(next);\n listenForEscape();\n if (!next) return;\n clearTimeout(s.askTimer);\n s.askTimer = setTimeout(() => {\n // Still closed: the caller turned it down.\n if (latestRef.current.open) return;\n s.asked = false;\n s.tappedIn = undefined;\n listenForEscape();\n });\n },\n [listenForEscape]\n );\n\n // Asks for what the pointer and focus call for, after its delay, when that\n // changed since the last time and isn't the state already.\n const settle = useCallback(() => {\n const s = stateRef.current;\n const held = s.pointerIn || s.focusIn;\n if (held !== s.held) {\n s.held = held;\n clearTimeout(s.timer);\n const { open, openDelay, closeDelay } = latestRef.current;\n s.opening = held && !open;\n if (held !== open) {\n s.timer = setTimeout(\n () => {\n s.opening = false;\n if (latestRef.current.open !== held) ask(held);\n else listenForEscape();\n },\n held ? openDelay : closeDelay\n );\n }\n }\n listenForEscape();\n }, [listenForEscape, ask]);\n\n // While a tap holds the floating element open, a press outside the trigger\n // and the floating element closes it. Set up again each time it opens or\n // closes.\n const listenForPress = useCallback(() => {\n const s = stateRef.current;\n s.stopPress?.();\n s.stopPress = undefined;\n const doc = latestRef.current.open ? s.tappedIn : undefined;\n if (!doc) return;\n const handlePointerDown = (event: Event) => {\n // Inside is a press React brought to the holder, which covers content\n // the floating element portals elsewhere, or one whose path runs\n // through the trigger or the floating element, which covers a press\n // that something inside stopped before React got it to the holder.\n const { triggerRef, floatingRef } = latestRef.current;\n if (\n event === s.pressedInside ||\n isEventInside(event, triggerRef.current) ||\n isEventInside(event, floatingRef.current)\n ) {\n return;\n }\n latestRef.current.onOpenChange(false);\n };\n doc.addEventListener('pointerdown', handlePointerDown);\n s.stopPress = () =>\n doc.removeEventListener('pointerdown', handlePointerDown);\n }, []);\n\n useClientLayoutEffect(() => {\n latestRef.current = options;\n // The commit after an ask to open answers it, open or still closed. A\n // tap that asked and still finds it closed was turned down.\n const s = stateRef.current;\n if (s.asked) {\n s.asked = false;\n if (!options.open) s.tappedIn = undefined;\n listenForEscape();\n }\n });\n\n useClientLayoutEffect(() => {\n const s = stateRef.current;\n return () => {\n clearTimeout(s.timer);\n clearTimeout(s.askTimer);\n s.opening = false;\n s.asked = false;\n s.releaseEscape?.();\n s.releaseEscape = undefined;\n s.stopPress?.();\n s.stopPress = undefined;\n };\n }, []);\n\n const { open } = options;\n // Before paint, so a pointer or a key arriving next finds this settled.\n useClientLayoutEffect(() => {\n const s = stateRef.current;\n if (!open) {\n // The floating element left from under the pointer, if it was there,\n // and no leave event says so. Focus resting on the trigger has done\n // its job too, so neither opens it again until it comes back.\n s.pointerIn = false;\n s.focusIn = false;\n s.tappedIn = undefined;\n if (s.focusInFloating) {\n s.focusInFloating = false;\n const trigger = latestRef.current.triggerRef.current;\n // Focus that went down with the floating element sits on <body>.\n // Focus somewhere else was put there by the same update, so it stays.\n if (\n trigger &&\n trigger.ownerDocument.activeElement === trigger.ownerDocument.body\n ) {\n s.restoring = true;\n trigger.focus();\n s.restoring = false;\n }\n }\n }\n settle();\n listenForPress();\n }, [open, settle, listenForPress]);\n\n return useMemo<HoverOpenHandlers>(() => {\n const inTrigger = (node: EventTarget | null) =>\n within(latestRef.current.triggerRef.current, node as Node | null);\n return {\n onPointerEnter: event => {\n if (event.pointerType === 'touch') return;\n stateRef.current.pointerIn = true;\n settle();\n },\n onPointerOver: event => {\n // React makes the enter out of the pointerout on the element the\n // pointer left, and skips this pointerover when that element is its\n // own. Leaving page content for a trigger inside a shadow root, that\n // pointerout names only the shadow host, so no enter reaches here.\n // A pointerover from outside the holder and the floating element is\n // the pointer arriving, then. One from inside them is the pointer\n // moving about within, which doesn't bring back a closed one.\n const s = stateRef.current;\n if (s.pointerIn || event.pointerType === 'touch') return;\n const from = event.relatedTarget as Node | null;\n if (\n within(event.currentTarget, from) ||\n within(latestRef.current.floatingRef.current, from)\n ) {\n return;\n }\n s.pointerIn = true;\n settle();\n },\n onPointerLeave: event => {\n // A touch pointer never set pointerIn, so its leave has nothing to\n // clear, and on a device with a mouse too it would close a card the\n // mouse is still holding open.\n if (event.pointerType === 'touch') return;\n stateRef.current.pointerIn = false;\n settle();\n },\n onFocus: event => {\n const s = stateRef.current;\n // React brings focus here from the trigger and from the floating\n // element's whole tree, portals included, so focus not in the\n // trigger is in the floating element, wherever its DOM is.\n s.focusInFloating = !inTrigger(event.target);\n if (s.restoring) return;\n // Any focus in the floating element holds it. On the trigger, only\n // keyboard focus does, so a click there opens nothing.\n s.focusIn = s.focusInFloating || isKeyboardFocus(event.target);\n settle();\n },\n onBlur: event => {\n const s = stateRef.current;\n const next = event.relatedTarget as Node | null;\n // Where focus goes next, as far as the DOM can say. Focus going into\n // content the floating element portals elsewhere reads as leaving,\n // and the focus event that follows straight after brings it back.\n s.focusInFloating = within(latestRef.current.floatingRef.current, next);\n // Moving between the trigger and the floating element isn't leaving.\n if (s.focusInFloating || within(event.currentTarget, next)) return;\n s.focusIn = false;\n settle();\n },\n onPointerDown: event => {\n stateRef.current.pressedInside = event.nativeEvent;\n },\n onPointerUp: event => {\n const now = latestRef.current;\n // With openOnTap, a tap on the trigger opens it at once, and the\n // next one closes it. A tap anywhere else does nothing here.\n if (\n !now.openOnTap ||\n event.pointerType !== 'touch' ||\n !inTrigger(event.target)\n ) {\n return;\n }\n stateRef.current.tappedIn = now.open\n ? undefined\n : (event.target as Element).ownerDocument;\n ask(!now.open);\n },\n };\n }, [settle, ask]);\n}\n"],"names":["chain"],"mappings":"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"}
@@ -0,0 +1,182 @@
1
+ /* @allxsmith/bestax-bulma — AI agents: see AGENTS.md in the package root, or https://bestax.io/llms.txt */
2
+ import { useRef, useEffect, useMemo } from 'react';
3
+ import { getActiveElementInTree } from './shadowDom.js';
4
+
5
+ // INTERNAL: deliberately not exported from src/index.ts.
6
+ /**
7
+ * Every menu-item role, so an item a caller gives `menuitemcheckbox` or
8
+ * `menuitemradio` (a filter or sort menu) stays reachable.
9
+ */
10
+ const MENU_ITEM_SELECTOR = '[role="menuitem"], [role="menuitemcheckbox"], [role="menuitemradio"]';
11
+ /**
12
+ * Whether a menu item can be reached and activated: it isn't disabled, as a
13
+ * native control or through `aria-disabled="true"`.
14
+ */
15
+ function isEnabledMenuItem(item) {
16
+ return (!item.hasAttribute('disabled') &&
17
+ item.getAttribute('aria-disabled') !== 'true');
18
+ }
19
+ /** Whether `node` is one of `menu`'s items, enabled or not. */
20
+ const isItemOf = (menu, node) => Boolean(node && menu.contains(node) && node.matches(MENU_ITEM_SELECTOR));
21
+ /** The items in `menu` the keys reach, in document order. */
22
+ function enabledMenuItems(menu) {
23
+ return Array.from(menu.querySelectorAll(MENU_ITEM_SELECTOR)).filter(isEnabledMenuItem);
24
+ }
25
+ /** How long typed characters keep adding up to one search, in milliseconds. */
26
+ const TYPEAHEAD_TIMEOUT = 500;
27
+ /**
28
+ * Whether the browser activates `item` on this key by itself: a `<button>`
29
+ * answers Enter and Space, and a link with an `href` answers Enter.
30
+ */
31
+ function activatesItself(item, key) {
32
+ if (item.tagName === 'BUTTON')
33
+ return true;
34
+ return key === 'Enter' && item.tagName === 'A' && item.hasAttribute('href');
35
+ }
36
+ /**
37
+ * The text typing finds an item by: what it says, without what's hidden from
38
+ * assistive technology, such as an icon whose glyph is a ligature's name.
39
+ */
40
+ function label(item) {
41
+ let text = '';
42
+ const read = (node) => {
43
+ node.childNodes.forEach(child => {
44
+ if (child.nodeType === 3)
45
+ text += child.textContent;
46
+ else if (child.nodeType === 1 &&
47
+ child.getAttribute('aria-hidden') !== 'true') {
48
+ read(child);
49
+ }
50
+ });
51
+ };
52
+ read(item);
53
+ return text.trim().toLowerCase();
54
+ }
55
+ /**
56
+ * The item typing `search` moves to, or none. A single character looks from
57
+ * the item after the focused one, so typing the same letter again cycles
58
+ * through the items that start with it, and so does a run of one letter
59
+ * ("dd"). A longer search looks from the focused item, so typing more of
60
+ * its own text keeps it.
61
+ */
62
+ function typeaheadMatch(items, search, current) {
63
+ const repeated = Array.from(search).every(char => char === search[0]);
64
+ const wanted = repeated ? search[0] : search;
65
+ const start = current < 0 ? 0 : current + (wanted.length === 1 ? 1 : 0);
66
+ for (let i = 0; i < items.length; i += 1) {
67
+ const item = items[(start + i) % items.length];
68
+ if (label(item).startsWith(wanted))
69
+ return item;
70
+ }
71
+ return undefined;
72
+ }
73
+ /**
74
+ * Moves through and activates the items of a `role="menu"` element from the
75
+ * keyboard. Put the `onKeyDown` it returns on that element. It reads the
76
+ * items from the element each time, so items that come and go are found,
77
+ * and it renders nothing.
78
+ *
79
+ * It acts only while focus is on the menu element itself or on one of its
80
+ * items, so anything else focusable inside the menu, such as a filter
81
+ * field, keeps its own keys.
82
+ *
83
+ * - Down and Up move to the next and previous enabled item, wrapping, and
84
+ * Home and End go to the first and last. From no item, Down goes to the
85
+ * first and Up to the last.
86
+ * - Enter and Space activate the focused item. A `<button>`, and a link with
87
+ * an `href` for Enter, get the browser's own click, and anything else gets
88
+ * one from here, so its `onClick` runs either way. A key held down
89
+ * activates it once. A key handler on the item that called
90
+ * `preventDefault()` has claimed the key, and nothing else runs it.
91
+ * - A printable character moves to the next enabled item whose text starts
92
+ * with what has been typed. Characters typed within `TYPEAHEAD_TIMEOUT` of
93
+ * each other add up to one search, until `reset` starts a new one.
94
+ *
95
+ * Items are `MENU_ITEM_SELECTOR` matches, and disabled ones are skipped by
96
+ * all of the above. Escape and Tab are left to the menu's owner, since what
97
+ * closing means is up to it, and so is any other key.
98
+ *
99
+ * @function useMenuKeys
100
+ * @returns The `keydown` handler for the menu element, and a way to forget
101
+ * what has been typed.
102
+ */
103
+ function useMenuKeys() {
104
+ const typedRef = useRef({ text: '' });
105
+ useEffect(() => {
106
+ const typed = typedRef.current;
107
+ return () => clearTimeout(typed.timer);
108
+ }, []);
109
+ return useMemo(() => {
110
+ const reset = () => {
111
+ const typed = typedRef.current;
112
+ clearTimeout(typed.timer);
113
+ typed.text = '';
114
+ };
115
+ const onKeyDown = (event) => {
116
+ const menu = event.currentTarget;
117
+ const focused = getActiveElementInTree(menu);
118
+ if (focused !== menu && !isItemOf(menu, focused))
119
+ return;
120
+ const items = enabledMenuItems(menu);
121
+ if (!items.length)
122
+ return;
123
+ const current = items.indexOf(focused);
124
+ switch (event.key) {
125
+ case 'ArrowDown':
126
+ event.preventDefault();
127
+ items[current >= 0 ? (current + 1) % items.length : 0].focus();
128
+ return;
129
+ case 'ArrowUp':
130
+ event.preventDefault();
131
+ items[current >= 0
132
+ ? (current - 1 + items.length) % items.length
133
+ : items.length - 1].focus();
134
+ return;
135
+ case 'Home':
136
+ event.preventDefault();
137
+ items[0].focus();
138
+ return;
139
+ case 'End':
140
+ event.preventDefault();
141
+ items[items.length - 1].focus();
142
+ return;
143
+ case 'Enter':
144
+ case ' ': {
145
+ if (current < 0 || event.defaultPrevented)
146
+ return;
147
+ const item = items[current];
148
+ if (activatesItself(item, event.key)) {
149
+ // The browser clicks on Enter's keydown, and a held key sends one
150
+ // per auto-repeat. Space clicks on keyup, once however long it's
151
+ // held.
152
+ if (event.repeat && event.key === 'Enter')
153
+ event.preventDefault();
154
+ return;
155
+ }
156
+ // Space would scroll the page, and a held key repeats.
157
+ event.preventDefault();
158
+ if (!event.repeat)
159
+ item.click();
160
+ return;
161
+ }
162
+ }
163
+ if (event.key.length !== 1 ||
164
+ event.ctrlKey ||
165
+ event.metaKey ||
166
+ event.altKey ||
167
+ event.defaultPrevented) {
168
+ return;
169
+ }
170
+ event.preventDefault();
171
+ const typed = typedRef.current;
172
+ typed.text += event.key.toLowerCase();
173
+ clearTimeout(typed.timer);
174
+ typed.timer = setTimeout(reset, TYPEAHEAD_TIMEOUT);
175
+ typeaheadMatch(items, typed.text, current)?.focus();
176
+ };
177
+ return { onKeyDown, reset };
178
+ }, []);
179
+ }
180
+
181
+ export { MENU_ITEM_SELECTOR, TYPEAHEAD_TIMEOUT, enabledMenuItems, isEnabledMenuItem, useMenuKeys };
182
+ //# sourceMappingURL=useMenuKeys.js.map