@allxsmith/bestax-bulma 5.37.0 → 5.38.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 (83) hide show
  1. package/dist/bestax.css +1 -1
  2. package/dist/bestax.css.map +1 -1
  3. package/dist/esm/components/Card.js +2 -2
  4. package/dist/esm/components/Carousel.js +14 -6
  5. package/dist/esm/components/Carousel.js.map +1 -1
  6. package/dist/esm/components/Dropdown.js +7 -2
  7. package/dist/esm/components/Dropdown.js.map +1 -1
  8. package/dist/esm/components/Loading.js +7 -1
  9. package/dist/esm/components/Loading.js.map +1 -1
  10. package/dist/esm/components/Panel.js +34 -4
  11. package/dist/esm/components/Panel.js.map +1 -1
  12. package/dist/esm/components/Popover.js +7 -2
  13. package/dist/esm/components/Popover.js.map +1 -1
  14. package/dist/esm/components/Sidebar.js +46 -17
  15. package/dist/esm/components/Sidebar.js.map +1 -1
  16. package/dist/esm/form/Autocomplete.js +28 -18
  17. package/dist/esm/form/Autocomplete.js.map +1 -1
  18. package/dist/esm/form/Combobox.js +592 -0
  19. package/dist/esm/form/Combobox.js.map +1 -0
  20. package/dist/esm/form/Field.js +3 -3
  21. package/dist/esm/form/Field.js.map +1 -1
  22. package/dist/esm/form/_pickerInternals/PickerPopover.js +10 -5
  23. package/dist/esm/form/_pickerInternals/PickerPopover.js.map +1 -1
  24. package/dist/esm/form/useAutoLabelId.js.map +1 -1
  25. package/dist/esm/helpers/Theme.js.map +1 -1
  26. package/dist/esm/helpers/useAnchoredPosition.js +35 -6
  27. package/dist/esm/helpers/useAnchoredPosition.js.map +1 -1
  28. package/dist/esm/helpers/useComboboxEngine.js +331 -0
  29. package/dist/esm/helpers/useComboboxEngine.js.map +1 -0
  30. package/dist/esm/helpers/useFocusTrap.js +87 -49
  31. package/dist/esm/helpers/useFocusTrap.js.map +1 -1
  32. package/dist/esm/helpers/useInertSiblings.js +106 -0
  33. package/dist/esm/helpers/useInertSiblings.js.map +1 -0
  34. package/dist/esm/helpers/useTreeView.js +2 -2
  35. package/dist/esm/layout/Container.js +2 -2
  36. package/dist/esm/layout/Container.js.map +1 -1
  37. package/dist/esm/layout/Hero.js +6 -6
  38. package/dist/esm/layout/Hero.js.map +1 -1
  39. package/dist/extras.css +1 -1
  40. package/dist/extras.css.map +1 -1
  41. package/dist/index.cjs +1504 -351
  42. package/dist/index.cjs.map +1 -1
  43. package/dist/index.esm.js +1 -0
  44. package/dist/index.esm.js.map +1 -1
  45. package/dist/types/components/Loading.d.ts +16 -2
  46. package/dist/types/components/Panel.d.ts +45 -5
  47. package/dist/types/components/Sidebar.d.ts +17 -2
  48. package/dist/types/form/Combobox.d.ts +416 -0
  49. package/dist/types/form/Field.d.ts +5 -5
  50. package/dist/types/form/useAutoLabelId.d.ts +3 -2
  51. package/dist/types/helpers/Theme.d.ts +6 -0
  52. package/dist/types/helpers/useAnchoredPosition.d.ts +25 -2
  53. package/dist/types/helpers/useComboboxEngine.d.ts +111 -0
  54. package/dist/types/helpers/useFocusTrap.d.ts +15 -2
  55. package/dist/types/helpers/useInertSiblings.d.ts +30 -0
  56. package/dist/types/index.d.ts +1 -0
  57. package/dist/types/layout/Container.d.ts +9 -1
  58. package/dist/types/layout/Hero.d.ts +27 -2
  59. package/dist/types-cjs/components/Loading.d.ts +16 -2
  60. package/dist/types-cjs/components/Panel.d.ts +45 -5
  61. package/dist/types-cjs/components/Sidebar.d.ts +17 -2
  62. package/dist/types-cjs/form/Combobox.d.ts +416 -0
  63. package/dist/types-cjs/form/Field.d.ts +5 -5
  64. package/dist/types-cjs/form/useAutoLabelId.d.ts +3 -2
  65. package/dist/types-cjs/helpers/Theme.d.ts +6 -0
  66. package/dist/types-cjs/helpers/useAnchoredPosition.d.ts +25 -2
  67. package/dist/types-cjs/helpers/useComboboxEngine.d.ts +111 -0
  68. package/dist/types-cjs/helpers/useFocusTrap.d.ts +15 -2
  69. package/dist/types-cjs/helpers/useInertSiblings.d.ts +30 -0
  70. package/dist/types-cjs/index.d.ts +1 -0
  71. package/dist/types-cjs/layout/Container.d.ts +9 -1
  72. package/dist/types-cjs/layout/Hero.d.ts +27 -2
  73. package/dist/versions/bestax-no-dark-mode.css +1 -1
  74. package/dist/versions/bestax-no-dark-mode.css.map +1 -1
  75. package/dist/versions/bestax-no-helpers-prefixed.css +1 -1
  76. package/dist/versions/bestax-no-helpers-prefixed.css.map +1 -1
  77. package/dist/versions/bestax-no-helpers.css +1 -1
  78. package/dist/versions/bestax-no-helpers.css.map +1 -1
  79. package/dist/versions/bestax-prefixed.css +1 -1
  80. package/dist/versions/bestax-prefixed.css.map +1 -1
  81. package/package.json +1 -1
  82. package/src/scss/form/_combobox.scss +323 -0
  83. package/src/scss/form/_index.scss +1 -0
@@ -0,0 +1,331 @@
1
+ /* @allxsmith/bestax-bulma — AI agents: see AGENTS.md in the package root, or https://bestax.io/llms.txt */
2
+ import { useId, useCallback, useRef, useState, useReducer, useEffect } from 'react';
3
+ import { useClientLayoutEffect } from './useClientLayoutEffect.js';
4
+ import { isEventInside, getActiveElementInTree } from './shadowDom.js';
5
+
6
+ // INTERNAL: deliberately not exported from src/index.ts.
7
+ /** An IME is composing, so the key belongs to it (`keyCode` 229 is Safari's word for it). */
8
+ const isComposing$1 = (event) => event.nativeEvent.isComposing || event.nativeEvent.keyCode === 229;
9
+ const sameValues = (a, b) => a.length === b.length && a.every((value, i) => value === b[i]);
10
+ /**
11
+ * The keys, the open state, the highlight and the ARIA of an editable
12
+ * combobox whose list the app renders and filters (#1083). DOM focus stays
13
+ * in the field the whole time: the highlight is `aria-activedescendant`, as
14
+ * the ARIA Authoring Practices ask of a combobox, where `useMenuKeys` moves
15
+ * real focus, as a menu should.
16
+ *
17
+ * The options are read from the listbox in document order whenever they're
18
+ * needed, so options in groups and options that come and go are found, and
19
+ * the highlight is remembered by value, so it survives a re-render. The text
20
+ * in the field and any value are the app's: a pick only calls
21
+ * `onSelect`.
22
+ *
23
+ * @function useComboboxEngine
24
+ * @param options - The open state, the pick callback and the shape.
25
+ * @returns The state and handlers the parts wire up.
26
+ */
27
+ function useComboboxEngine({ open: controlledOpen, defaultOpen, onOpenChange, onSelect, onSettle, keepFirst, multiple, inline, deferred, }) {
28
+ const baseId = useId();
29
+ const listboxId = `${baseId}-listbox`;
30
+ const itemPrefix = `${baseId}-item-`;
31
+ // The value goes into the id encoded, so a value with a space in it still
32
+ // makes one id, not two.
33
+ const itemId = useCallback((value) => itemPrefix + encodeURIComponent(value), [itemPrefix]);
34
+ const rootRef = useRef(null);
35
+ const inputRef = useRef(null);
36
+ const listboxRef = useRef(null);
37
+ const popupRef = useRef(null);
38
+ const insidePressRef = useRef(null);
39
+ const optionsRef = useRef(new Map());
40
+ const landingRef = useRef(null);
41
+ const wasOpenRef = useRef(false);
42
+ const shownValuesRef = useRef([]);
43
+ // Whether the next highlight change is one to scroll to: a key's or a
44
+ // landing's. The pointer's isn't, since the pointer is already on that
45
+ // option, and scrolling would move the list out from under it.
46
+ const scrollToHighlightRef = useRef(false);
47
+ const isControlled = controlledOpen !== undefined;
48
+ const [internalOpen, setInternalOpen] = useState(defaultOpen);
49
+ const open = inline || (controlledOpen ?? internalOpen);
50
+ const [highlighted, setHighlighted] = useState(null);
51
+ const [typed, setTyped] = useState(false);
52
+ // Whether a listbox with an option in it is on the page, which only the
53
+ // page can say, so it's read once the open list has rendered.
54
+ const [listShown, setListShown] = useState(null);
55
+ // Options register from their own effects, which needn't render the
56
+ // component that holds this hook, so they ask for a render to be read.
57
+ const [, refresh] = useReducer((n) => n + 1, 0);
58
+ // Closing forgets the highlight, the typing and what was on the page,
59
+ // during render, React's pattern for adjusting state when other state
60
+ // changes.
61
+ if (!open) {
62
+ if (highlighted !== null)
63
+ setHighlighted(null);
64
+ if (typed)
65
+ setTyped(false);
66
+ if (listShown !== null)
67
+ setListShown(null);
68
+ }
69
+ const expanded = open && (listShown ?? !deferred);
70
+ /** The options on the page, in document order, groups included. */
71
+ const shownOptions = useCallback(() => {
72
+ const listbox = listboxRef.current;
73
+ if (!listbox)
74
+ return [];
75
+ const shown = [];
76
+ listbox
77
+ .querySelectorAll('[role="option"]')
78
+ .forEach(element => {
79
+ // Only the Combobox's own options: an element given the role by
80
+ // hand isn't one.
81
+ const state = optionsRef.current.get(element);
82
+ if (state)
83
+ shown.push({ element, ...state });
84
+ });
85
+ return shown;
86
+ }, []);
87
+ const requestOpen = useCallback((next, landing = 'open') => {
88
+ if (inline || next === open)
89
+ return;
90
+ landingRef.current = next ? landing : null;
91
+ onOpenChange?.(next);
92
+ if (!isControlled)
93
+ setInternalOpen(next);
94
+ }, [inline, open, onOpenChange, isControlled]);
95
+ const close = useCallback(() => requestOpen(false), [requestOpen]);
96
+ // After every render, read the list: whether it has options, and where
97
+ // the highlight goes now. A highlighted option that has left takes the
98
+ // highlight with it, or hands it to the first enabled one under
99
+ // `keepFirst`, which also moves it there whenever the options change.
100
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- after every render on purpose
101
+ useClientLayoutEffect(() => {
102
+ const opened = open && !wasOpenRef.current;
103
+ wasOpenRef.current = open;
104
+ if (!open) {
105
+ landingRef.current = null;
106
+ shownValuesRef.current = [];
107
+ return;
108
+ }
109
+ const options = shownOptions();
110
+ const values = options.map(option => option.value);
111
+ const changed = !sameValues(values, shownValuesRef.current);
112
+ shownValuesRef.current = values;
113
+ setListShown(values.length > 0);
114
+ const landing = landingRef.current ?? (opened ? 'open' : null);
115
+ landingRef.current = null;
116
+ const enabled = options.filter(option => !option.disabled);
117
+ const checked = enabled.find(option => option.checked)?.value;
118
+ const first = enabled.length > 0 ? enabled[0].value : null;
119
+ const last = enabled.length > 0 ? enabled[enabled.length - 1].value : null;
120
+ let next = highlighted;
121
+ if (landing === 'first')
122
+ next = checked ?? first;
123
+ else if (landing === 'last')
124
+ next = checked ?? last;
125
+ else if (landing === 'open')
126
+ next = checked ?? (keepFirst ? first : null);
127
+ else if (landing === 'typed')
128
+ next = keepFirst ? first : null;
129
+ else if (landing === 'none')
130
+ next = null;
131
+ else if (keepFirst && changed)
132
+ next = first;
133
+ // Without `keepFirst`, which moved it above, a highlighted option
134
+ // that left takes the highlight with it.
135
+ else if (next !== null && !values.includes(next))
136
+ next = null;
137
+ // Only a change is set. StrictMode runs this twice on mount, and the
138
+ // second run, with the landing used up, works out the rendered highlight
139
+ // again, so it leaves the first run's landing in place.
140
+ if (next !== highlighted) {
141
+ scrollToHighlightRef.current = true;
142
+ setHighlighted(next);
143
+ }
144
+ });
145
+ // A browser scrolls to what has focus, not to an active descendant, so
146
+ // the highlighted option is brought into view here, when a key or a
147
+ // landing moved it there.
148
+ useClientLayoutEffect(() => {
149
+ if (highlighted === null || !scrollToHighlightRef.current)
150
+ return;
151
+ scrollToHighlightRef.current = false;
152
+ const option = shownOptions().find(shown => shown.value === highlighted);
153
+ option?.element.scrollIntoView?.({ block: 'nearest' });
154
+ }, [highlighted, shownOptions]);
155
+ // A press outside the field and the list closes it. The path is checked
156
+ // rather than the target, so it works in a shadow root.
157
+ useEffect(() => {
158
+ const root = rootRef.current;
159
+ if (!open || inline || !root)
160
+ return undefined;
161
+ const onPress = (event) => {
162
+ if (event === insidePressRef.current ||
163
+ isEventInside(event, rootRef.current) ||
164
+ isEventInside(event, popupRef.current)) {
165
+ return;
166
+ }
167
+ close();
168
+ };
169
+ // Its document, an iframe's when the tree renders into one.
170
+ const doc = root.ownerDocument;
171
+ doc.addEventListener('pointerdown', onPress);
172
+ return () => doc.removeEventListener('pointerdown', onPress);
173
+ }, [open, inline, close]);
174
+ const submit = useCallback((value) => {
175
+ onSelect?.(value);
176
+ if (!multiple)
177
+ close();
178
+ }, [onSelect, multiple, close]);
179
+ const onKeyDown = (event) => {
180
+ // The caller's handler ran first and can keep a key, and an IME that's
181
+ // composing owns every key.
182
+ if (event.defaultPrevented || isComposing$1(event))
183
+ return;
184
+ const { key, altKey } = event;
185
+ if (!open) {
186
+ if (key === 'ArrowDown' || (key === 'ArrowUp' && !altKey)) {
187
+ event.preventDefault();
188
+ requestOpen(true, altKey ? 'none' : key === 'ArrowDown' ? 'first' : 'last');
189
+ }
190
+ return;
191
+ }
192
+ switch (key) {
193
+ case 'ArrowDown':
194
+ case 'ArrowUp': {
195
+ if (altKey) {
196
+ if (key === 'ArrowUp' && !inline) {
197
+ event.preventDefault();
198
+ close();
199
+ }
200
+ return;
201
+ }
202
+ event.preventDefault();
203
+ // Disabled options too, so a screen reader user finds them.
204
+ const options = shownOptions();
205
+ if (options.length === 0)
206
+ return;
207
+ const at = options.findIndex(option => option.value === highlighted);
208
+ const step = key === 'ArrowDown' ? 1 : -1;
209
+ const index = at === -1
210
+ ? step === 1
211
+ ? 0
212
+ : options.length - 1
213
+ : (at + step + options.length) % options.length;
214
+ scrollToHighlightRef.current = true;
215
+ setHighlighted(options[index].value);
216
+ return;
217
+ }
218
+ case 'Enter': {
219
+ const option = shownOptions().find(shown => shown.value === highlighted);
220
+ if (option) {
221
+ event.preventDefault();
222
+ if (!option.disabled)
223
+ submit(option.value);
224
+ return;
225
+ }
226
+ // Nothing to pick: the key goes on, so a form submits.
227
+ onSettle?.();
228
+ close();
229
+ return;
230
+ }
231
+ case 'Escape':
232
+ if (inline)
233
+ return;
234
+ // It stops here only when there was something to close, so a
235
+ // Popover or Modal around it keeps its own Escape otherwise. Nothing
236
+ // stops it going on: a handler further out reads `defaultPrevented`.
237
+ if (popupRef.current)
238
+ event.preventDefault();
239
+ close();
240
+ return;
241
+ case 'Tab':
242
+ close();
243
+ return;
244
+ case 'Home':
245
+ case 'End':
246
+ case 'ArrowLeft':
247
+ case 'ArrowRight':
248
+ // The field's own keys, which take the field back from the list.
249
+ setHighlighted(null);
250
+ return;
251
+ }
252
+ };
253
+ const onChange = () => {
254
+ setTyped(true);
255
+ if (open) {
256
+ landingRef.current = 'typed';
257
+ setHighlighted(null);
258
+ refresh();
259
+ }
260
+ else {
261
+ requestOpen(true, 'typed');
262
+ }
263
+ };
264
+ const onClick = () => {
265
+ requestOpen(true, 'open');
266
+ };
267
+ const onBlur = (event) => {
268
+ const next = event.relatedTarget;
269
+ if (next &&
270
+ (rootRef.current?.contains(next) || popupRef.current?.contains(next))) {
271
+ return;
272
+ }
273
+ close();
274
+ };
275
+ const hover = (value) => {
276
+ scrollToHighlightRef.current = false;
277
+ setHighlighted(value);
278
+ };
279
+ const pick = (value) => {
280
+ submit(value);
281
+ // A press in the list keeps focus in the field, but a browser that
282
+ // ignores that, or a tap, can leave it elsewhere.
283
+ const input = inputRef.current;
284
+ if (input && input.isConnected && getActiveElementInTree(input) !== input) {
285
+ input.focus();
286
+ }
287
+ };
288
+ const keepFocus = (event) => event.preventDefault();
289
+ const markInside = (event) => {
290
+ insidePressRef.current = event.nativeEvent;
291
+ };
292
+ const registerPopup = useCallback((element) => {
293
+ popupRef.current = element;
294
+ return () => {
295
+ popupRef.current = null;
296
+ };
297
+ }, []);
298
+ const registerItem = useCallback((element, state) => {
299
+ const options = optionsRef.current;
300
+ options.set(element, state);
301
+ refresh();
302
+ return () => {
303
+ options.delete(element);
304
+ refresh();
305
+ };
306
+ }, []);
307
+ return {
308
+ open,
309
+ expanded,
310
+ highlighted,
311
+ typed,
312
+ listboxId,
313
+ itemId,
314
+ rootRef,
315
+ inputRef,
316
+ listboxRef,
317
+ registerPopup,
318
+ registerItem,
319
+ onKeyDown,
320
+ onChange,
321
+ onClick,
322
+ onBlur,
323
+ hover,
324
+ pick,
325
+ keepFocus,
326
+ markInside,
327
+ };
328
+ }
329
+
330
+ export { useComboboxEngine };
331
+ //# sourceMappingURL=useComboboxEngine.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useComboboxEngine.js","sources":["../../../src/helpers/useComboboxEngine.ts"],"sourcesContent":["// INTERNAL: deliberately not exported from src/index.ts.\nimport {\n useCallback,\n useEffect,\n useId,\n useReducer,\n useRef,\n useState,\n} from 'react';\nimport type React from 'react';\nimport { useClientLayoutEffect } from './useClientLayoutEffect';\nimport { getActiveElementInTree, isEventInside } from './shadowDom';\n\n/** What an item tells the engine about itself. */\nexport interface ComboboxItemState {\n /** What a pick hands back. Unique within the combobox. */\n value: string;\n /** Reachable with the arrows, never picked. */\n disabled: boolean;\n /** The value, or one of them. */\n checked: boolean;\n}\n\n/** An option on the page, with its element. */\ninterface ShownOption extends ComboboxItemState {\n element: HTMLElement;\n}\n\n/**\n * Where the highlight lands once an opened list has rendered, or once typing\n * has changed what's in it:\n *\n * - `first` and `last` (Down and Up Arrow): the checked option, otherwise the\n * first or last enabled one;\n * - `open` (a click, or any other way of opening): the checked option,\n * otherwise the first enabled one with `keepFirst`, otherwise nothing;\n * - `typed`: the first enabled option with `keepFirst`, otherwise nothing;\n * - `none` (Alt+Down Arrow): nothing.\n *\n * Only an enabled option takes it, so a disabled checked one is passed over.\n */\ntype Landing = 'first' | 'last' | 'open' | 'typed' | 'none';\n\n/** Options for `useComboboxEngine`. */\nexport interface ComboboxEngineOptions {\n /** Controlled open state. When set, opening and closing only report through `onOpenChange`. */\n open?: boolean;\n /** Initial open state when uncontrolled. */\n defaultOpen: boolean;\n /** Called with the open state asked for, when it differs from the current one. */\n onOpenChange?: (open: boolean) => void;\n /** Called with an item's `value` when it's picked. */\n onSelect?: (value: string) => void;\n /**\n * Called when Enter goes on to the form with nothing to pick, before the\n * list closes, so an owner that shows something other than the typing can\n * put it back before the form reads the field. An `inline` list doesn't\n * close, so this is the one word it gets.\n */\n onSettle?: () => void;\n /** Highlights the first enabled item whenever the list opens or its items change. */\n keepFirst: boolean;\n /** Several values: a pick keeps the list open. */\n multiple: boolean;\n /** The list is always on screen, so nothing opens or closes it. */\n inline: boolean;\n /**\n * The list's panel is portaled, so it has no server markup and shows only\n * from the commit after hydration. Until it has rendered, an open list\n * isn't taken to be on the page, so no `aria-controls` names it early.\n */\n deferred: boolean;\n}\n\n/** What `useComboboxEngine` hands the parts of a combobox. */\nexport interface ComboboxEngine {\n /** Whether the list is open. Always, with `inline`. */\n open: boolean;\n /**\n * The field's `aria-expanded`: open, with a listbox that has an option in\n * it on the page. Until the open list has rendered, it's taken to have\n * one, so server markup for an open list in place says it's expanded. A\n * `deferred` list isn't in server markup, so it's taken to have none.\n */\n expanded: boolean;\n /** The highlighted option's value, if any. */\n highlighted: string | null;\n /** Whether the user has typed since the list opened. */\n typed: boolean;\n /** The listbox's id. */\n listboxId: string;\n /** An item's id, which follows from its value. */\n itemId: (value: string) => string;\n /** For the element around the field and, unless it's portaled, the list. */\n rootRef: React.MutableRefObject<HTMLDivElement | null>;\n /** For the field. */\n inputRef: React.MutableRefObject<HTMLInputElement | null>;\n /** For the listbox. */\n listboxRef: React.MutableRefObject<HTMLDivElement | null>;\n /**\n * Records the floating panel while it's on screen, which makes it count as\n * inside, and makes an Escape that closes it stop there. Returns the\n * function that forgets it.\n */\n registerPopup: (element: HTMLElement) => () => void;\n /** Records an item's element. Returns the function that forgets it. */\n registerItem: (element: HTMLElement, state: ComboboxItemState) => () => void;\n /** The field's keys, to run after the caller's own handler. */\n onKeyDown: (event: React.KeyboardEvent<HTMLInputElement>) => void;\n /** A change to the field's value: the user typed. */\n onChange: () => void;\n /** A click in the field. */\n onClick: () => void;\n /** Focus leaving the field. */\n onBlur: (event: React.FocusEvent<HTMLInputElement>) => void;\n /** The pointer moved over an enabled option. */\n hover: (value: string) => void;\n /** A click on an enabled option. Focus ends up in the field. */\n pick: (value: string) => void;\n /** For anything in the list: a press there doesn't take focus from the field. */\n keepFocus: (event: React.MouseEvent) => void;\n /**\n * For the floating panel: marks a press as inside, which React reports from\n * content the panel portals elsewhere as well.\n */\n markInside: (event: React.PointerEvent) => void;\n}\n\n/** An IME is composing, so the key belongs to it (`keyCode` 229 is Safari's word for it). */\nconst isComposing = (event: React.KeyboardEvent): boolean =>\n event.nativeEvent.isComposing || event.nativeEvent.keyCode === 229;\n\nconst sameValues = (a: string[], b: string[]): boolean =>\n a.length === b.length && a.every((value, i) => value === b[i]);\n\n/**\n * The keys, the open state, the highlight and the ARIA of an editable\n * combobox whose list the app renders and filters (#1083). DOM focus stays\n * in the field the whole time: the highlight is `aria-activedescendant`, as\n * the ARIA Authoring Practices ask of a combobox, where `useMenuKeys` moves\n * real focus, as a menu should.\n *\n * The options are read from the listbox in document order whenever they're\n * needed, so options in groups and options that come and go are found, and\n * the highlight is remembered by value, so it survives a re-render. The text\n * in the field and any value are the app's: a pick only calls\n * `onSelect`.\n *\n * @function useComboboxEngine\n * @param options - The open state, the pick callback and the shape.\n * @returns The state and handlers the parts wire up.\n */\nexport function useComboboxEngine({\n open: controlledOpen,\n defaultOpen,\n onOpenChange,\n onSelect,\n onSettle,\n keepFirst,\n multiple,\n inline,\n deferred,\n}: ComboboxEngineOptions): ComboboxEngine {\n const baseId = useId();\n const listboxId = `${baseId}-listbox`;\n const itemPrefix = `${baseId}-item-`;\n // The value goes into the id encoded, so a value with a space in it still\n // makes one id, not two.\n const itemId = useCallback(\n (value: string) => itemPrefix + encodeURIComponent(value),\n [itemPrefix]\n );\n\n const rootRef = useRef<HTMLDivElement | null>(null);\n const inputRef = useRef<HTMLInputElement | null>(null);\n const listboxRef = useRef<HTMLDivElement | null>(null);\n const popupRef = useRef<HTMLElement | null>(null);\n const insidePressRef = useRef<Event | null>(null);\n const optionsRef = useRef(new Map<HTMLElement, ComboboxItemState>());\n const landingRef = useRef<Landing | null>(null);\n const wasOpenRef = useRef(false);\n const shownValuesRef = useRef<string[]>([]);\n // Whether the next highlight change is one to scroll to: a key's or a\n // landing's. The pointer's isn't, since the pointer is already on that\n // option, and scrolling would move the list out from under it.\n const scrollToHighlightRef = useRef(false);\n\n const isControlled = controlledOpen !== undefined;\n const [internalOpen, setInternalOpen] = useState(defaultOpen);\n const open = inline || (controlledOpen ?? internalOpen);\n\n const [highlighted, setHighlighted] = useState<string | null>(null);\n const [typed, setTyped] = useState(false);\n // Whether a listbox with an option in it is on the page, which only the\n // page can say, so it's read once the open list has rendered.\n const [listShown, setListShown] = useState<boolean | null>(null);\n // Options register from their own effects, which needn't render the\n // component that holds this hook, so they ask for a render to be read.\n const [, refresh] = useReducer((n: number) => n + 1, 0);\n\n // Closing forgets the highlight, the typing and what was on the page,\n // during render, React's pattern for adjusting state when other state\n // changes.\n if (!open) {\n if (highlighted !== null) setHighlighted(null);\n if (typed) setTyped(false);\n if (listShown !== null) setListShown(null);\n }\n const expanded = open && (listShown ?? !deferred);\n\n /** The options on the page, in document order, groups included. */\n const shownOptions = useCallback((): ShownOption[] => {\n const listbox = listboxRef.current;\n if (!listbox) return [];\n const shown: ShownOption[] = [];\n listbox\n .querySelectorAll<HTMLElement>('[role=\"option\"]')\n .forEach(element => {\n // Only the Combobox's own options: an element given the role by\n // hand isn't one.\n const state = optionsRef.current.get(element);\n if (state) shown.push({ element, ...state });\n });\n return shown;\n }, []);\n\n const requestOpen = useCallback(\n (next: boolean, landing: Landing = 'open') => {\n if (inline || next === open) return;\n landingRef.current = next ? landing : null;\n onOpenChange?.(next);\n if (!isControlled) setInternalOpen(next);\n },\n [inline, open, onOpenChange, isControlled]\n );\n const close = useCallback(() => requestOpen(false), [requestOpen]);\n\n // After every render, read the list: whether it has options, and where\n // the highlight goes now. A highlighted option that has left takes the\n // highlight with it, or hands it to the first enabled one under\n // `keepFirst`, which also moves it there whenever the options change.\n // eslint-disable-next-line react-hooks/exhaustive-deps -- after every render on purpose\n useClientLayoutEffect(() => {\n const opened = open && !wasOpenRef.current;\n wasOpenRef.current = open;\n if (!open) {\n landingRef.current = null;\n shownValuesRef.current = [];\n return;\n }\n const options = shownOptions();\n const values = options.map(option => option.value);\n const changed = !sameValues(values, shownValuesRef.current);\n shownValuesRef.current = values;\n setListShown(values.length > 0);\n\n const landing = landingRef.current ?? (opened ? 'open' : null);\n landingRef.current = null;\n const enabled = options.filter(option => !option.disabled);\n const checked = enabled.find(option => option.checked)?.value;\n const first = enabled.length > 0 ? enabled[0].value : null;\n const last = enabled.length > 0 ? enabled[enabled.length - 1].value : null;\n let next = highlighted;\n if (landing === 'first') next = checked ?? first;\n else if (landing === 'last') next = checked ?? last;\n else if (landing === 'open') next = checked ?? (keepFirst ? first : null);\n else if (landing === 'typed') next = keepFirst ? first : null;\n else if (landing === 'none') next = null;\n else if (keepFirst && changed) next = first;\n // Without `keepFirst`, which moved it above, a highlighted option\n // that left takes the highlight with it.\n else if (next !== null && !values.includes(next)) next = null;\n // Only a change is set. StrictMode runs this twice on mount, and the\n // second run, with the landing used up, works out the rendered highlight\n // again, so it leaves the first run's landing in place.\n if (next !== highlighted) {\n scrollToHighlightRef.current = true;\n setHighlighted(next);\n }\n });\n\n // A browser scrolls to what has focus, not to an active descendant, so\n // the highlighted option is brought into view here, when a key or a\n // landing moved it there.\n useClientLayoutEffect(() => {\n if (highlighted === null || !scrollToHighlightRef.current) return;\n scrollToHighlightRef.current = false;\n const option = shownOptions().find(shown => shown.value === highlighted);\n option?.element.scrollIntoView?.({ block: 'nearest' });\n }, [highlighted, shownOptions]);\n\n // A press outside the field and the list closes it. The path is checked\n // rather than the target, so it works in a shadow root.\n useEffect(() => {\n const root = rootRef.current;\n if (!open || inline || !root) return undefined;\n const onPress = (event: PointerEvent) => {\n if (\n event === insidePressRef.current ||\n isEventInside(event, rootRef.current) ||\n isEventInside(event, popupRef.current)\n ) {\n return;\n }\n close();\n };\n // Its document, an iframe's when the tree renders into one.\n const doc = root.ownerDocument;\n doc.addEventListener('pointerdown', onPress);\n return () => doc.removeEventListener('pointerdown', onPress);\n }, [open, inline, close]);\n\n const submit = useCallback(\n (value: string) => {\n onSelect?.(value);\n if (!multiple) close();\n },\n [onSelect, multiple, close]\n );\n\n const onKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {\n // The caller's handler ran first and can keep a key, and an IME that's\n // composing owns every key.\n if (event.defaultPrevented || isComposing(event)) return;\n const { key, altKey } = event;\n\n if (!open) {\n if (key === 'ArrowDown' || (key === 'ArrowUp' && !altKey)) {\n event.preventDefault();\n requestOpen(\n true,\n altKey ? 'none' : key === 'ArrowDown' ? 'first' : 'last'\n );\n }\n return;\n }\n\n switch (key) {\n case 'ArrowDown':\n case 'ArrowUp': {\n if (altKey) {\n if (key === 'ArrowUp' && !inline) {\n event.preventDefault();\n close();\n }\n return;\n }\n event.preventDefault();\n // Disabled options too, so a screen reader user finds them.\n const options = shownOptions();\n if (options.length === 0) return;\n const at = options.findIndex(option => option.value === highlighted);\n const step = key === 'ArrowDown' ? 1 : -1;\n const index =\n at === -1\n ? step === 1\n ? 0\n : options.length - 1\n : (at + step + options.length) % options.length;\n scrollToHighlightRef.current = true;\n setHighlighted(options[index].value);\n return;\n }\n case 'Enter': {\n const option = shownOptions().find(\n shown => shown.value === highlighted\n );\n if (option) {\n event.preventDefault();\n if (!option.disabled) submit(option.value);\n return;\n }\n // Nothing to pick: the key goes on, so a form submits.\n onSettle?.();\n close();\n return;\n }\n case 'Escape':\n if (inline) return;\n // It stops here only when there was something to close, so a\n // Popover or Modal around it keeps its own Escape otherwise. Nothing\n // stops it going on: a handler further out reads `defaultPrevented`.\n if (popupRef.current) event.preventDefault();\n close();\n return;\n case 'Tab':\n close();\n return;\n case 'Home':\n case 'End':\n case 'ArrowLeft':\n case 'ArrowRight':\n // The field's own keys, which take the field back from the list.\n setHighlighted(null);\n return;\n default:\n }\n };\n\n const onChange = () => {\n setTyped(true);\n if (open) {\n landingRef.current = 'typed';\n setHighlighted(null);\n refresh();\n } else {\n requestOpen(true, 'typed');\n }\n };\n\n const onClick = () => {\n requestOpen(true, 'open');\n };\n\n const onBlur = (event: React.FocusEvent<HTMLInputElement>) => {\n const next = event.relatedTarget as Node | null;\n if (\n next &&\n (rootRef.current?.contains(next) || popupRef.current?.contains(next))\n ) {\n return;\n }\n close();\n };\n\n const hover = (value: string) => {\n scrollToHighlightRef.current = false;\n setHighlighted(value);\n };\n\n const pick = (value: string) => {\n submit(value);\n // A press in the list keeps focus in the field, but a browser that\n // ignores that, or a tap, can leave it elsewhere.\n const input = inputRef.current;\n if (input && input.isConnected && getActiveElementInTree(input) !== input) {\n input.focus();\n }\n };\n\n const keepFocus = (event: React.MouseEvent) => event.preventDefault();\n\n const markInside = (event: React.PointerEvent) => {\n insidePressRef.current = event.nativeEvent;\n };\n\n const registerPopup = useCallback((element: HTMLElement) => {\n popupRef.current = element;\n return () => {\n popupRef.current = null;\n };\n }, []);\n\n const registerItem = useCallback(\n (element: HTMLElement, state: ComboboxItemState) => {\n const options = optionsRef.current;\n options.set(element, state);\n refresh();\n return () => {\n options.delete(element);\n refresh();\n };\n },\n []\n );\n\n return {\n open,\n expanded,\n highlighted,\n typed,\n listboxId,\n itemId,\n rootRef,\n inputRef,\n listboxRef,\n registerPopup,\n registerItem,\n onKeyDown,\n onChange,\n onClick,\n onBlur,\n hover,\n pick,\n keepFocus,\n markInside,\n };\n}\n"],"names":["isComposing"],"mappings":"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"}
@@ -288,8 +288,8 @@ function findTabStops(container) {
288
288
  /**
289
289
  * The first tab stop inside `container`, found the way the trap finds it, or
290
290
  * `null` when there is none. For a component that moves focus into a panel
291
- * without trapping it there. `src/index.ts` names its exports from this
292
- * module, so this one stays internal.
291
+ * itself rather than through the hook. `src/index.ts` names its exports from
292
+ * this module, so this one stays internal.
293
293
  */
294
294
  function firstTabStop(container) {
295
295
  return findTabStops(container)?.first ?? null;
@@ -303,6 +303,88 @@ function firstTabStop(container) {
303
303
  * was inside a shadow root that was removed, so this falls back for it.
304
304
  */
305
305
  const focusedElement = (doc) => getDeepestActiveElement(doc) ?? doc.body;
306
+ /**
307
+ * Handles a Tab for a trap on `container`. When the browser has no tab stop
308
+ * left in that direction inside the container, focus goes to the stop at the
309
+ * other end and the key is marked used, and with no stops at all the
310
+ * container takes focus. Focus outside the container goes in at that end
311
+ * too. Otherwise the browser moves focus, and this returns the end to send it
312
+ * to should that Tab take it out anyway.
313
+ */
314
+ function wrapTab(e, container) {
315
+ const stops = findTabStops(container);
316
+ if (!stops) {
317
+ e.preventDefault();
318
+ container.focus();
319
+ return null;
320
+ }
321
+ const focused = focusedElement(container.ownerDocument);
322
+ const current = stops.stopFor(focused);
323
+ const end = e.shiftKey ? stops.last : stops.first;
324
+ // Wrap whenever the browser has no stop left in that direction inside the
325
+ // container. Comparing positions rather than testing `=== first` also
326
+ // covers focus resting on the container or on an element outside the tab
327
+ // order, which the browser would otherwise walk straight out of.
328
+ if (!inside(container, focused) ||
329
+ (e.shiftKey
330
+ ? !follows(container, stops.first, current)
331
+ : !follows(container, current, stops.last))) {
332
+ e.preventDefault();
333
+ end.focus();
334
+ return null;
335
+ }
336
+ return end;
337
+ }
338
+ /**
339
+ * Holds Tab inside `container` until the returned function is called. Tab
340
+ * and Shift+Tab heard on `keyTarget` wrap at the ends, unless something
341
+ * already marked the key used, and if a Tab the trap let through still takes
342
+ * focus out, it sends focus back to the other end.
343
+ *
344
+ * `holds` is asked on every Tab, and the trap leaves the key alone while it
345
+ * says no. Internal like `firstTabStop`. It's for an overlay that holds Tab
346
+ * from a listener of its own on `window` and asks the overlay stack first,
347
+ * so it stands aside while another overlay is open over it, which a trap
348
+ * listening on the container can't do.
349
+ */
350
+ function trapTab(container, keyTarget = container, holds = () => true) {
351
+ const doc = container.ownerDocument;
352
+ // Where focus goes if a Tab the trap let the browser handle takes it out,
353
+ // held until that Tab has finished moving focus.
354
+ let wrapTo = null;
355
+ let wrapTimer;
356
+ // `keyTarget` is any target, so the listener takes the base event type.
357
+ const handleKeyDown = (event) => {
358
+ const e = event;
359
+ // A trap nested inside this one has already wrapped focus.
360
+ if (e.key !== 'Tab' || e.defaultPrevented || !holds())
361
+ return;
362
+ const end = wrapTab(e, container);
363
+ if (!end)
364
+ return;
365
+ wrapTo = end;
366
+ clearTimeout(wrapTimer);
367
+ wrapTimer = setTimeout(() => {
368
+ wrapTo = null;
369
+ });
370
+ };
371
+ // The browser moves focus as soon as the keydown is over, so focus that
372
+ // lands outside while `wrapTo` is set got there by that Tab.
373
+ const handleFocusIn = (e) => {
374
+ if (!wrapTo || inside(container, e.composedPath()[0]))
375
+ return;
376
+ const target = wrapTo;
377
+ wrapTo = null;
378
+ target.focus();
379
+ };
380
+ keyTarget.addEventListener('keydown', handleKeyDown);
381
+ doc.addEventListener('focusin', handleFocusIn, true);
382
+ return () => {
383
+ keyTarget.removeEventListener('keydown', handleKeyDown);
384
+ doc.removeEventListener('focusin', handleFocusIn, true);
385
+ clearTimeout(wrapTimer);
386
+ };
387
+ }
306
388
  /**
307
389
  * Keeps keyboard focus inside a container while it is active: focus moves in
308
390
  * when the trap turns on, Tab and Shift+Tab wrap between the first and last
@@ -381,53 +463,9 @@ function useFocusTrap(containerRef, options = {}) {
381
463
  if (!inTrap || !inside(container, focusedElement(doc))) {
382
464
  (findTabStops(container)?.first ?? container).focus();
383
465
  }
384
- // Where focus goes if a Tab the trap let the browser handle takes it out,
385
- // held until that Tab has finished moving focus.
386
- let wrapTo = null;
387
- let wrapTimer;
388
- const handleKeyDown = (e) => {
389
- // A trap nested inside this one has already wrapped focus.
390
- if (e.key !== 'Tab' || e.defaultPrevented)
391
- return;
392
- const stops = findTabStops(container);
393
- if (!stops) {
394
- e.preventDefault();
395
- container.focus();
396
- return;
397
- }
398
- const current = stops.stopFor(focusedElement(doc));
399
- // Wrap whenever the browser has no stop left in that direction inside
400
- // the container. Comparing positions rather than testing `=== first`
401
- // also covers focus resting on the container or on an element outside
402
- // the tab order, which the browser would otherwise walk straight out of.
403
- if (e.shiftKey
404
- ? !follows(container, stops.first, current)
405
- : !follows(container, current, stops.last)) {
406
- e.preventDefault();
407
- (e.shiftKey ? stops.last : stops.first).focus();
408
- return;
409
- }
410
- wrapTo = e.shiftKey ? stops.last : stops.first;
411
- clearTimeout(wrapTimer);
412
- wrapTimer = setTimeout(() => {
413
- wrapTo = null;
414
- });
415
- };
416
- // The browser moves focus as soon as the keydown is over, so focus that
417
- // lands outside while `wrapTo` is set got there by that Tab.
418
- const handleFocusIn = (e) => {
419
- if (!wrapTo || inside(container, e.composedPath()[0]))
420
- return;
421
- const target = wrapTo;
422
- wrapTo = null;
423
- target.focus();
424
- };
425
- container.addEventListener('keydown', handleKeyDown);
426
- doc.addEventListener('focusin', handleFocusIn, true);
466
+ const releaseTab = trapTab(container);
427
467
  return () => {
428
- container.removeEventListener('keydown', handleKeyDown);
429
- doc.removeEventListener('focusin', handleFocusIn, true);
430
- clearTimeout(wrapTimer);
468
+ releaseTab();
431
469
  let target = restoreFocus ? opener : null;
432
470
  // `null` is an object too, and a caller without types can pass it.
433
471
  if (restoreFocus && typeof restoreFocus === 'object') {
@@ -467,5 +505,5 @@ function useFocusTrap(containerRef, options = {}) {
467
505
  });
468
506
  }
469
507
 
470
- export { firstTabStop, isTabbableKind, useFocusTrap };
508
+ export { firstTabStop, isTabbableKind, trapTab, useFocusTrap };
471
509
  //# sourceMappingURL=useFocusTrap.js.map