@octanejs/aria 0.0.3 → 0.0.5

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 (93) hide show
  1. package/package.json +7 -5
  2. package/src/autocomplete/useAutocomplete.ts +656 -0
  3. package/src/collections/BaseCollection.ts +374 -0
  4. package/src/collections/CollectionBuilder.ts +343 -0
  5. package/src/collections/Document.ts +451 -0
  6. package/src/collections/Hidden.ts +84 -0
  7. package/src/collections/useCachedChildren.ts +103 -0
  8. package/src/components/Autocomplete.ts +124 -0
  9. package/src/components/Breadcrumbs.ts +170 -0
  10. package/src/components/Button.ts +209 -0
  11. package/src/components/Checkbox.ts +593 -0
  12. package/src/components/Collection.ts +301 -0
  13. package/src/components/ComboBox.ts +477 -0
  14. package/src/components/Dialog.ts +195 -0
  15. package/src/components/Disclosure.ts +322 -0
  16. package/src/components/DragAndDrop.ts +177 -0
  17. package/src/components/FieldError.ts +79 -0
  18. package/src/components/Form.ts +65 -0
  19. package/src/components/GridList.ts +1099 -0
  20. package/src/components/Group.ts +146 -0
  21. package/src/components/Header.ts +32 -0
  22. package/src/components/Heading.ts +45 -0
  23. package/src/components/Input.ts +139 -0
  24. package/src/components/Keyboard.ts +23 -0
  25. package/src/components/Label.ts +30 -0
  26. package/src/components/Link.ts +137 -0
  27. package/src/components/ListBox.ts +886 -0
  28. package/src/components/Menu.ts +685 -0
  29. package/src/components/Meter.ts +100 -0
  30. package/src/components/Modal.ts +352 -0
  31. package/src/components/NumberField.ts +221 -0
  32. package/src/components/OverlayArrow.ts +117 -0
  33. package/src/components/Popover.ts +385 -0
  34. package/src/components/ProgressBar.ts +112 -0
  35. package/src/components/RadioGroup.ts +542 -0
  36. package/src/components/SearchField.ts +208 -0
  37. package/src/components/Select.ts +505 -0
  38. package/src/components/SelectionIndicator.ts +52 -0
  39. package/src/components/Separator.ts +97 -0
  40. package/src/components/SharedElementTransition.ts +209 -0
  41. package/src/components/Slider.ts +472 -0
  42. package/src/components/Switch.ts +449 -0
  43. package/src/components/Tabs.ts +675 -0
  44. package/src/components/TagGroup.ts +443 -0
  45. package/src/components/Text.ts +27 -0
  46. package/src/components/TextArea.ts +92 -0
  47. package/src/components/TextField.ts +200 -0
  48. package/src/components/ToggleButton.ts +147 -0
  49. package/src/components/ToggleButtonGroup.ts +106 -0
  50. package/src/components/Toolbar.ts +74 -0
  51. package/src/components/Tooltip.ts +249 -0
  52. package/src/components/index.ts +302 -0
  53. package/src/components/useDragAndDrop.ts +164 -0
  54. package/src/components/utils.ts +521 -0
  55. package/src/intl/autocomplete/ar-AE.json +3 -0
  56. package/src/intl/autocomplete/bg-BG.json +3 -0
  57. package/src/intl/autocomplete/cs-CZ.json +3 -0
  58. package/src/intl/autocomplete/da-DK.json +3 -0
  59. package/src/intl/autocomplete/de-DE.json +3 -0
  60. package/src/intl/autocomplete/el-GR.json +3 -0
  61. package/src/intl/autocomplete/en-US.json +3 -0
  62. package/src/intl/autocomplete/es-ES.json +3 -0
  63. package/src/intl/autocomplete/et-EE.json +3 -0
  64. package/src/intl/autocomplete/fi-FI.json +3 -0
  65. package/src/intl/autocomplete/fr-FR.json +3 -0
  66. package/src/intl/autocomplete/he-IL.json +3 -0
  67. package/src/intl/autocomplete/hr-HR.json +3 -0
  68. package/src/intl/autocomplete/hu-HU.json +3 -0
  69. package/src/intl/autocomplete/index.ts +75 -0
  70. package/src/intl/autocomplete/it-IT.json +3 -0
  71. package/src/intl/autocomplete/ja-JP.json +3 -0
  72. package/src/intl/autocomplete/ko-KR.json +3 -0
  73. package/src/intl/autocomplete/lt-LT.json +3 -0
  74. package/src/intl/autocomplete/lv-LV.json +3 -0
  75. package/src/intl/autocomplete/nb-NO.json +3 -0
  76. package/src/intl/autocomplete/nl-NL.json +3 -0
  77. package/src/intl/autocomplete/pl-PL.json +3 -0
  78. package/src/intl/autocomplete/pt-BR.json +3 -0
  79. package/src/intl/autocomplete/pt-PT.json +3 -0
  80. package/src/intl/autocomplete/ro-RO.json +3 -0
  81. package/src/intl/autocomplete/ru-RU.json +3 -0
  82. package/src/intl/autocomplete/sk-SK.json +3 -0
  83. package/src/intl/autocomplete/sl-SI.json +3 -0
  84. package/src/intl/autocomplete/sr-SP.json +3 -0
  85. package/src/intl/autocomplete/sv-SE.json +3 -0
  86. package/src/intl/autocomplete/tr-TR.json +3 -0
  87. package/src/intl/autocomplete/uk-UA.json +3 -0
  88. package/src/intl/autocomplete/zh-CN.json +3 -0
  89. package/src/intl/autocomplete/zh-TW.json +3 -0
  90. package/src/stately/autocomplete/useAutocompleteState.ts +85 -0
  91. package/src/utils/animation.ts +149 -0
  92. package/src/utils/useLoadMoreSentinel.ts +78 -0
  93. package/src/utils/useViewportSize.ts +123 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@octanejs/aria",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "engines": {
@@ -35,7 +35,8 @@
35
35
  ],
36
36
  "exports": {
37
37
  ".": "./src/index.ts",
38
- "./stately": "./src/stately/index.ts"
38
+ "./stately": "./src/stately/index.ts",
39
+ "./components": "./src/components/index.ts"
39
40
  },
40
41
  "dependencies": {
41
42
  "@internationalized/date": "^3.12.2",
@@ -45,17 +46,18 @@
45
46
  "clsx": "^2.1.1"
46
47
  },
47
48
  "peerDependencies": {
48
- "octane": "0.1.9"
49
+ "octane": "0.1.11"
49
50
  },
50
51
  "devDependencies": {
51
- "@tsrx/react": "^0.2.37",
52
+ "@tsrx/react": "^0.2.44",
52
53
  "esbuild": "^0.28.1",
53
54
  "react": "^19.2.0",
54
55
  "react-aria": "3.50.0",
56
+ "react-aria-components": "1.19.0",
55
57
  "react-dom": "^19.2.0",
56
58
  "react-stately": "3.48.0",
57
59
  "vitest": "^4.1.9",
58
- "octane": "0.1.9"
60
+ "octane": "0.1.11"
59
61
  },
60
62
  "scripts": {
61
63
  "test": "vitest run"
@@ -0,0 +1,656 @@
1
+ // Ported from react-aria (source: .react-spectrum/packages/react-aria/src/autocomplete/useAutocomplete.ts).
2
+ // octane adaptations:
3
+ // - The autocomplete input is a TEXT input: `inputProps.onChange` stays the VALUE-level
4
+ // callback exactly like upstream — the consuming field hook (useTextField et al) wires it
5
+ // onto octane's NATIVE `input` event. No synthetic onChange is added anywhere.
6
+ // - `onKeyDown` receives the ported BaseEvent Proxy (createEventHandler via the field hook's
7
+ // useKeyboard). `.nativeEvent.isComposing` → `e.isComposing`; `.nativeEvent.type` /
8
+ // `.nativeEvent` used as event-constructor init → `e.type` / the Proxy itself (property
9
+ // reads forward to the live native event). Upstream's `'continuePropagation' in e` probe
10
+ // becomes a `typeof` probe — the Proxy exposes overrides via `get`, not `has`.
11
+ // - React FocusEvent/KeyboardEvent/PointerEvent types → native DOM events.
12
+ // - The Parcel glob intl import becomes the generated '../intl/autocomplete' dictionary index.
13
+ // - Public-hook slot threading (splitSlot/subSlot) per the binding convention; explicit
14
+ // dependency arrays are preserved verbatim (including the empty ones).
15
+ import type {
16
+ AriaLabelingProps,
17
+ DOMProps,
18
+ FocusableElement,
19
+ Node,
20
+ RefObject,
21
+ ValueBase,
22
+ } from '@react-types/shared';
23
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'octane';
24
+
25
+ import type { AriaTextFieldProps } from '../textfield/useTextField';
26
+ import type {
27
+ AutocompleteProps,
28
+ AutocompleteState,
29
+ } from '../stately/autocomplete/useAutocompleteState';
30
+ import { CLEAR_FOCUS_EVENT, FOCUS_EVENT } from '../utils/constants';
31
+ import {
32
+ dispatchVirtualBlur,
33
+ dispatchVirtualFocus,
34
+ getVirtuallyFocusedElement,
35
+ moveVirtualFocus,
36
+ } from '../focus/virtualFocus';
37
+ import { getActiveElement, getEventTarget } from '../utils/shadowdom/DOMFunctions';
38
+ import { getInteractionModality, getPointerType } from '../interactions/useFocusVisible';
39
+ // octane adaptation: the ported NATIVE-event handler bags (upstream: FocusEvents/
40
+ // KeyboardEvents from '@react-types/shared', typed over React synthetic events).
41
+ import type { FocusEvents } from '../interactions/useFocus';
42
+ import type { KeyboardEvents } from '../interactions/useKeyboard';
43
+ import { getOwnerDocument } from '../utils/domHelpers';
44
+ import intlMessages from '../intl/autocomplete';
45
+ import { isAndroid, isIOS } from '../utils/platform';
46
+ import { isCtrlKeyPressed } from '../utils/keyboard';
47
+ import { mergeProps } from '../utils/mergeProps';
48
+ import { mergeRefs } from '../utils/mergeRefs';
49
+ import { useEffectEvent } from '../utils/useEffectEvent';
50
+ import { useEvent } from '../utils/useEvent';
51
+ import { useId } from '../utils/useId';
52
+ import { useLabels } from '../utils/useLabels';
53
+ import { useLayoutEffect } from '../utils/useLayoutEffect';
54
+ import { useLocalizedStringFormatter } from '../i18n/useLocalizedStringFormatter';
55
+ import { useObjectRef } from '../utils/useObjectRef';
56
+
57
+ import { S, splitSlot, subSlot } from '../internal';
58
+
59
+ export interface CollectionOptions extends DOMProps, AriaLabelingProps {
60
+ /** Whether the collection items should use virtual focus instead of being focused directly. */
61
+ shouldUseVirtualFocus: boolean;
62
+ /** Whether typeahead is disabled. */
63
+ disallowTypeAhead: boolean;
64
+ }
65
+
66
+ export interface InputProps<T = FocusableElement>
67
+ extends
68
+ DOMProps,
69
+ FocusEvents<T>,
70
+ KeyboardEvents,
71
+ Pick<ValueBase<string>, 'onChange' | 'value'>,
72
+ Pick<
73
+ AriaTextFieldProps,
74
+ | 'enterKeyHint'
75
+ | 'aria-controls'
76
+ | 'aria-autocomplete'
77
+ | 'aria-activedescendant'
78
+ | 'spellCheck'
79
+ | 'autoCorrect'
80
+ | 'autoComplete'
81
+ > {}
82
+
83
+ export interface AriaAutocompleteProps<T> extends AutocompleteProps {
84
+ /**
85
+ * An optional filter function used to determine if a option should be included in the
86
+ * autocomplete list. Include this if the items you are providing to your wrapped collection
87
+ * aren't filtered by default.
88
+ */
89
+ filter?: (textValue: string, inputValue: string, node: Node<T>) => boolean;
90
+
91
+ /**
92
+ * Whether or not to focus the first item in the collection after a filter is performed. Note this
93
+ * is only applicable if virtual focus behavior is not turned off via `disableVirtualFocus`.
94
+ *
95
+ * @default false
96
+ */
97
+ disableAutoFocusFirst?: boolean;
98
+
99
+ /**
100
+ * Whether the autocomplete should disable virtual focus, instead making the wrapped collection
101
+ * directly tabbable.
102
+ *
103
+ * @default false
104
+ */
105
+ disableVirtualFocus?: boolean;
106
+ }
107
+
108
+ export interface AriaAutocompleteOptions<T> extends Omit<AriaAutocompleteProps<T>, 'children'> {
109
+ /** The ref for the wrapped collection element. */
110
+ inputRef: RefObject<HTMLInputElement | null>;
111
+ /** The ref for the wrapped collection element. */
112
+ collectionRef: RefObject<HTMLElement | null>;
113
+ }
114
+
115
+ export interface AutocompleteAria<T> {
116
+ /**
117
+ * Props for the autocomplete input element. These should be passed to the input's aria hooks
118
+ * (e.g. useTextField/useSearchField/etc) respectively.
119
+ */
120
+ inputProps: InputProps;
121
+ /** Props for the collection, to be passed to collection's respective aria hook (e.g. useMenu). */
122
+ collectionProps: CollectionOptions;
123
+ /** Ref to attach to the wrapped collection. */
124
+ collectionRef: RefObject<HTMLElement | null>;
125
+ /**
126
+ * A filter function that returns if the provided collection node should be filtered out of the
127
+ * collection.
128
+ */
129
+ filter?: (nodeTextValue: string, node: Node<T>) => boolean;
130
+ }
131
+
132
+ /**
133
+ * Provides the behavior and accessibility implementation for an autocomplete component. An
134
+ * autocomplete combines a text input with a collection, allowing users to filter the collection's
135
+ * contents match a query.
136
+ *
137
+ * @param props - Props for the autocomplete.
138
+ * @param state - State for the autocomplete, as returned by `useAutocompleteState`.
139
+ */
140
+ export function useAutocomplete<T>(
141
+ props: AriaAutocompleteOptions<T>,
142
+ state: AutocompleteState,
143
+ ): AutocompleteAria<T>;
144
+ // Slot-threading form: sibling ported hooks pass their derived sub-slot as the trailing arg.
145
+ export function useAutocomplete<T>(
146
+ props: AriaAutocompleteOptions<T>,
147
+ state: AutocompleteState,
148
+ slot: symbol | undefined,
149
+ ): AutocompleteAria<T>;
150
+ export function useAutocomplete(...args: any[]): AutocompleteAria<any> {
151
+ const [user, slotArg] = splitSlot(args);
152
+ const slot = slotArg ?? S('useAutocomplete');
153
+ const props = user[0] as AriaAutocompleteOptions<any>;
154
+ const state = user[1] as AutocompleteState;
155
+
156
+ let {
157
+ inputRef,
158
+ collectionRef,
159
+ filter,
160
+ disableAutoFocusFirst = false,
161
+ disableVirtualFocus = false,
162
+ } = props;
163
+
164
+ let collectionId = useId(subSlot(slot, 'collectionId'));
165
+ let timeout = useRef<ReturnType<typeof setTimeout> | undefined>(
166
+ undefined,
167
+ subSlot(slot, 'timeout'),
168
+ );
169
+ let delayNextActiveDescendant = useRef(false, subSlot(slot, 'delayNextActiveDescendant'));
170
+ let queuedActiveDescendant = useRef<string | null>(null, subSlot(slot, 'queuedActiveDescendant'));
171
+
172
+ // For mobile screen readers, we don't want virtual focus, instead opting to disable FocusScope's restoreFocus and manually
173
+ // moving focus back to the subtriggers
174
+ let isMobileScreenReader = getInteractionModality() === 'virtual' && (isIOS() || isAndroid());
175
+ let [shouldUseVirtualFocus, setShouldUseVirtualFocus] = useState(
176
+ !isMobileScreenReader && !disableVirtualFocus,
177
+ subSlot(slot, 'virtualFocus'),
178
+ );
179
+ // Tracks if a collection has been connected to the autocomplete. If false, we don't want to add various attributes to the autocomplete input
180
+ // since it isn't attached to a filterable collection (e.g. Tabs)
181
+ let [hasCollection, setHasCollection] = useState(false, subSlot(slot, 'hasCollection'));
182
+ let [autoFocusOnMount, setAutoFocusOnMount] = useState(false, subSlot(slot, 'autoFocusOnMount'));
183
+
184
+ useEffect(
185
+ () => {
186
+ return () => clearTimeout(timeout.current);
187
+ },
188
+ [],
189
+ subSlot(slot, 'clearTimeout'),
190
+ );
191
+
192
+ let updateActiveDescendantEvent = useEffectEvent(
193
+ (e: Event) => {
194
+ // Ensure input is focused if the user clicks on the collection directly.
195
+ // don't trigger on touch so that mobile keyboard doesnt appear when tapping on options
196
+ if (
197
+ !e.isTrusted &&
198
+ shouldUseVirtualFocus &&
199
+ inputRef.current &&
200
+ getActiveElement(getOwnerDocument(inputRef.current)) !== inputRef.current &&
201
+ getPointerType() !== 'touch'
202
+ ) {
203
+ inputRef.current.focus();
204
+ }
205
+
206
+ let target = getEventTarget(e) as Element | null;
207
+ if (e.isTrusted || !target || queuedActiveDescendant.current === target.id) {
208
+ return;
209
+ }
210
+
211
+ clearTimeout(timeout.current);
212
+ if (target !== collectionRef.current) {
213
+ if (delayNextActiveDescendant.current) {
214
+ queuedActiveDescendant.current = target.id;
215
+ timeout.current = setTimeout(() => {
216
+ state.setFocusedNodeId(target.id);
217
+ }, 500);
218
+ } else {
219
+ queuedActiveDescendant.current = target.id;
220
+ state.setFocusedNodeId(target.id);
221
+ }
222
+ } else if (
223
+ queuedActiveDescendant.current &&
224
+ !document.getElementById(queuedActiveDescendant.current)
225
+ ) {
226
+ // If we recieve a focus event refocusing the collection, either we have newly refocused the input and are waiting for the
227
+ // wrapped collection to refocus the previously focused node if any OR
228
+ // we are in a state where we've filtered to such a point that there aren't any matching items in the collection to focus.
229
+ // In this case we want to clear tracked item if any and clear active descendant
230
+ queuedActiveDescendant.current = null;
231
+ state.setFocusedNodeId(null);
232
+ }
233
+
234
+ delayNextActiveDescendant.current = false;
235
+ },
236
+ subSlot(slot, 'updateActiveDescendant'),
237
+ );
238
+
239
+ let [collectionNode, setCollectionNode] = useState<HTMLElement | null>(
240
+ null,
241
+ subSlot(slot, 'collectionNode'),
242
+ );
243
+ let callbackRef = useCallback(
244
+ (node) => {
245
+ setCollectionNode(node);
246
+ if (node != null) {
247
+ // If useSelectableCollection isn't passed shouldUseVirtualFocus even when useAutocomplete provides it
248
+ // that means the collection doesn't support it (e.g. Table). If that is the case, we need to disable it here regardless
249
+ // of what the user's provided so that the input doesn't recieve the onKeyDown and autocomplete props.
250
+ if (node.getAttribute('tabindex') != null) {
251
+ setShouldUseVirtualFocus(false);
252
+ }
253
+ setHasCollection(true);
254
+ } else {
255
+ setHasCollection(false);
256
+ }
257
+ },
258
+ [],
259
+ subSlot(slot, 'callbackRef'),
260
+ );
261
+ useLayoutEffect(
262
+ () => {
263
+ if (collectionNode != null) {
264
+ // When typing forward, we want to delay the setting of active descendant to not interrupt the native screen reader announcement
265
+ // of the letter you just typed. If we recieve another focus event then we clear the queued update
266
+ collectionNode.addEventListener('focusin', updateActiveDescendantEvent);
267
+ }
268
+ return () => {
269
+ collectionNode?.removeEventListener('focusin', updateActiveDescendantEvent);
270
+ };
271
+ },
272
+ [collectionNode],
273
+ subSlot(slot, 'focusinListener'),
274
+ );
275
+
276
+ // Make sure to memo so that React doesn't keep registering a new event listeners on every rerender of the wrapped collection
277
+ let mergedCollectionRef = useObjectRef(
278
+ useMemo(
279
+ () => mergeRefs(collectionRef, callbackRef),
280
+ [collectionRef, callbackRef],
281
+ subSlot(slot, 'mergeRefs'),
282
+ ),
283
+ subSlot(slot, 'objectRef'),
284
+ );
285
+
286
+ let focusFirstItem = useCallback(
287
+ () => {
288
+ if (!collectionRef.current) {
289
+ setAutoFocusOnMount(true);
290
+ return;
291
+ }
292
+
293
+ delayNextActiveDescendant.current = true;
294
+ collectionRef.current?.dispatchEvent(
295
+ new CustomEvent(FOCUS_EVENT, {
296
+ cancelable: true,
297
+ bubbles: true,
298
+ detail: {
299
+ focusStrategy: 'first',
300
+ },
301
+ }),
302
+ );
303
+ },
304
+ [collectionRef],
305
+ subSlot(slot, 'focusFirstItem'),
306
+ );
307
+
308
+ let clearVirtualFocus = useCallback(
309
+ (clearFocusKey?: boolean) => {
310
+ setAutoFocusOnMount(false);
311
+ moveVirtualFocus(getActiveElement());
312
+ queuedActiveDescendant.current = null;
313
+ state.setFocusedNodeId(null);
314
+ let clearFocusEvent = new CustomEvent(CLEAR_FOCUS_EVENT, {
315
+ cancelable: true,
316
+ bubbles: true,
317
+ detail: {
318
+ clearFocusKey,
319
+ },
320
+ });
321
+ clearTimeout(timeout.current);
322
+ delayNextActiveDescendant.current = false;
323
+ collectionRef.current?.dispatchEvent(clearFocusEvent);
324
+ },
325
+ [collectionRef, state],
326
+ subSlot(slot, 'clearVirtualFocus'),
327
+ );
328
+
329
+ let lastInputType = useRef('', subSlot(slot, 'lastInputType'));
330
+ useEvent(
331
+ inputRef,
332
+ 'beforeinput',
333
+ (e) => {
334
+ let { inputType } = e as InputEvent;
335
+ lastInputType.current = inputType;
336
+ },
337
+ subSlot(slot, 'beforeinput'),
338
+ );
339
+
340
+ let onChange = (value: string) => {
341
+ // Tell wrapped collection to focus the first element in the list when typing forward and to clear focused key when modifying the text via
342
+ // copy paste/backspacing/undo/redo for screen reader announcements
343
+ if (
344
+ (lastInputType.current === 'insertText' ||
345
+ // IME composition (e.g. CJK input) reports 'insertCompositionText'/'insertFromComposition'
346
+ // instead of 'insertText'. Treat these as forward typing so the first item gets virtual focus.
347
+ lastInputType.current === 'insertCompositionText' ||
348
+ lastInputType.current === 'insertFromComposition') &&
349
+ !disableAutoFocusFirst
350
+ ) {
351
+ focusFirstItem();
352
+ } else if (
353
+ lastInputType.current &&
354
+ (lastInputType.current.includes('insert') ||
355
+ lastInputType.current.includes('delete') ||
356
+ lastInputType.current.includes('history'))
357
+ ) {
358
+ clearVirtualFocus(true);
359
+
360
+ // If onChange was triggered before the timeout actually updated the activedescendant, we need to fire
361
+ // our own dispatchVirtualFocus so focusVisible gets reapplied on the input
362
+ if (getVirtuallyFocusedElement(document) === inputRef.current) {
363
+ dispatchVirtualFocus(inputRef.current!, null);
364
+ }
365
+ }
366
+
367
+ state.setInputValue(value);
368
+ };
369
+
370
+ let keyDownTarget = useRef<Element | null>(null, subSlot(slot, 'keyDownTarget'));
371
+ // For textfield specific keydown operations
372
+ // octane adaptation: `e` is the ported BaseEvent Proxy over the native KeyboardEvent
373
+ // (upstream types it BaseEvent<ReactKeyboardEvent>); typed `any` like the useComboBox
374
+ // port so the handler slots into AriaTextFieldProps' native-event handler types.
375
+ let onKeyDown = (e: any) => {
376
+ keyDownTarget.current = getEventTarget(e) as Element;
377
+ // octane adaptation: `.nativeEvent.isComposing` → `e.isComposing` (the BaseEvent Proxy
378
+ // forwards the read to the live native event).
379
+ if (e.isComposing) {
380
+ return;
381
+ }
382
+
383
+ let focusedNodeId = queuedActiveDescendant.current;
384
+ if (
385
+ focusedNodeId !== null &&
386
+ getOwnerDocument(inputRef.current).getElementById(focusedNodeId) == null
387
+ ) {
388
+ // if the focused id doesn't exist in document, then we need to clear the tracked focused node, otherwise
389
+ // we will be attempting to fire key events on a non-existing node instead of trying to focus the newly swapped wrapped collection.
390
+ // This can happen if you are swapping out the Autocomplete wrapped collection component like in the docs search.
391
+ queuedActiveDescendant.current = null;
392
+ focusedNodeId = null;
393
+ }
394
+
395
+ switch (e.key) {
396
+ case 'a':
397
+ if (isCtrlKeyPressed(e)) {
398
+ return;
399
+ }
400
+ break;
401
+ case 'Escape':
402
+ // Early return for Escape here so it doesn't leak the Escape event from the simulated collection event below and
403
+ // close the dialog prematurely. Ideally that should be up to the discretion of the input element hence the check
404
+ // for isPropagationStopped
405
+ if (e.isDefaultPrevented()) {
406
+ return;
407
+ }
408
+ break;
409
+ case ' ':
410
+ // Space shouldn't trigger onAction so early return.
411
+ return;
412
+ case 'Tab':
413
+ // Don't propogate Tab down to the collection, otherwise we will try to focus the collection via useSelectableCollection's Tab handler (aka shift tab logic)
414
+ // We want FocusScope to handle Tab if one exists (aka sub dialog), so special casepropogate
415
+ // octane adaptation: the BaseEvent Proxy exposes its overrides via `get`, so
416
+ // upstream's `'continuePropagation' in e` probe becomes a typeof probe.
417
+ if (typeof e.continuePropagation === 'function') {
418
+ e.continuePropagation();
419
+ }
420
+ return;
421
+ case 'Home':
422
+ case 'End':
423
+ case 'PageDown':
424
+ case 'PageUp':
425
+ case 'ArrowUp':
426
+ case 'ArrowDown':
427
+ case 'ArrowRight':
428
+ case 'ArrowLeft': {
429
+ if ((e.key === 'Home' || e.key === 'End') && focusedNodeId == null && e.shiftKey) {
430
+ return;
431
+ }
432
+
433
+ // If there is text within the input field, we'll want continue propagating events down
434
+ // to the wrapped collection if there is a focused node so that a user can continue moving the
435
+ // virtual focus. However, if the user doesn't have a focus in the collection, just move the text
436
+ // cursor instead. They can move focus down into the collection via down/up arrow if need be
437
+ if (e.key === 'ArrowRight' || e.key === 'ArrowLeft') {
438
+ if (focusedNodeId == null) {
439
+ if (!e.isPropagationStopped()) {
440
+ e.stopPropagation();
441
+ }
442
+ return;
443
+ }
444
+
445
+ break;
446
+ }
447
+
448
+ // Prevent these keys from moving the text cursor in the input
449
+ e.preventDefault();
450
+ // Move virtual focus into the wrapped collection
451
+ let focusCollection = new CustomEvent(FOCUS_EVENT, {
452
+ cancelable: true,
453
+ bubbles: true,
454
+ });
455
+
456
+ collectionRef.current?.dispatchEvent(focusCollection);
457
+ break;
458
+ }
459
+ }
460
+
461
+ // Emulate the keyboard events that happen in the input field in the wrapped collection. This is for triggering things like onAction via Enter
462
+ // or moving focus from one item to another. Stop propagation on the input event if it isn't already stopped so it doesn't leak out. For events
463
+ // like ESC, the dispatched event below will bubble out of the collection and be stopped if handled by useSelectableCollection, otherwise will bubble
464
+ // as expected
465
+ if (!e.isPropagationStopped()) {
466
+ e.stopPropagation();
467
+ }
468
+
469
+ let shouldPerformDefaultAction = true;
470
+ if (collectionRef.current !== null) {
471
+ if (focusedNodeId == null) {
472
+ // octane adaptation: `new KeyboardEvent(e.nativeEvent.type, e.nativeEvent)` — the
473
+ // Proxy forwards init-dict property reads to the live native event.
474
+ shouldPerformDefaultAction =
475
+ collectionRef.current?.dispatchEvent(new KeyboardEvent(e.type, e)) || false;
476
+ } else {
477
+ let item = document.getElementById(focusedNodeId);
478
+ if (item) {
479
+ shouldPerformDefaultAction = item?.dispatchEvent(new KeyboardEvent(e.type, e)) || false;
480
+ }
481
+ }
482
+ }
483
+
484
+ if (shouldPerformDefaultAction) {
485
+ switch (e.key) {
486
+ case 'ArrowLeft':
487
+ case 'ArrowRight': {
488
+ // Clear the activedescendant so NVDA announcements aren't interrupted but retain the focused key in the collection so the
489
+ // user's keyboard navigation restarts from where they left off
490
+ clearVirtualFocus();
491
+ break;
492
+ }
493
+ case 'Enter':
494
+ // Trigger click action on item when Enter key was pressed.
495
+ if (focusedNodeId != null) {
496
+ let item = document.getElementById(focusedNodeId);
497
+ item?.dispatchEvent(new PointerEvent('click', e));
498
+ }
499
+ break;
500
+ }
501
+ } else {
502
+ // TODO: check if we can do this, want to stop textArea from using its default Enter behavior so items are properly triggered
503
+ e.preventDefault();
504
+ }
505
+ };
506
+
507
+ let onKeyUpCapture = useEffectEvent(
508
+ (e: KeyboardEvent) => {
509
+ // Dispatch simulated key up events for things like triggering links in listbox
510
+ // Make sure to stop the propagation of the input keyup event so that the simulated keyup/down pair
511
+ // is detected by usePress instead of the original keyup originating from the input
512
+ if (getEventTarget(e) === keyDownTarget.current) {
513
+ e.stopImmediatePropagation();
514
+ let focusedNodeId = queuedActiveDescendant.current;
515
+ if (focusedNodeId == null) {
516
+ collectionRef.current?.dispatchEvent(new KeyboardEvent(e.type, e));
517
+ } else {
518
+ let item = document.getElementById(focusedNodeId);
519
+ item?.dispatchEvent(new KeyboardEvent(e.type, e));
520
+ }
521
+ }
522
+ },
523
+ subSlot(slot, 'onKeyUpCapture'),
524
+ );
525
+
526
+ useEffect(
527
+ () => {
528
+ document.addEventListener('keyup', onKeyUpCapture, true);
529
+ return () => {
530
+ document.removeEventListener('keyup', onKeyUpCapture, true);
531
+ };
532
+ },
533
+ [],
534
+ subSlot(slot, 'keyupListener'),
535
+ );
536
+
537
+ let stringFormatter = useLocalizedStringFormatter(
538
+ intlMessages,
539
+ '@react-aria/autocomplete',
540
+ subSlot(slot, 'formatter'),
541
+ );
542
+ let collectionProps = useLabels(
543
+ {
544
+ id: collectionId,
545
+ 'aria-label': stringFormatter.format('collectionLabel'),
546
+ },
547
+ undefined,
548
+ subSlot(slot, 'collectionLabels'),
549
+ );
550
+
551
+ let filterFn = useCallback(
552
+ (nodeTextValue: string, node: Node<any>) => {
553
+ if (filter) {
554
+ return filter(nodeTextValue, state.inputValue, node);
555
+ }
556
+
557
+ return true;
558
+ },
559
+ [state.inputValue, filter],
560
+ subSlot(slot, 'filterFn'),
561
+ );
562
+
563
+ // Be sure to clear/restore the virtual + collection focus when blurring/refocusing the field so we only show the
564
+ // focus ring on the virtually focused collection when are actually interacting with the Autocomplete
565
+ // octane adaptation: `e` is the NATIVE FocusEvent (upstream: React's FocusEvent).
566
+ let onBlur = (e: any) => {
567
+ if (!e.isTrusted) {
568
+ return;
569
+ }
570
+
571
+ let lastFocusedNode = queuedActiveDescendant.current
572
+ ? document.getElementById(queuedActiveDescendant.current)
573
+ : null;
574
+ if (lastFocusedNode) {
575
+ dispatchVirtualBlur(lastFocusedNode, e.relatedTarget);
576
+ }
577
+ };
578
+
579
+ let onFocus = (e: any) => {
580
+ if (!e.isTrusted) {
581
+ return;
582
+ }
583
+
584
+ let curFocusedNode = queuedActiveDescendant.current
585
+ ? document.getElementById(queuedActiveDescendant.current)
586
+ : null;
587
+ if (curFocusedNode) {
588
+ let target = getEventTarget(e) as Element;
589
+ queueMicrotask(() => {
590
+ // instead of focusing the last focused node, just focus the collection instead and have the collection handle what item to focus via useSelectableCollection/Item
591
+ dispatchVirtualBlur(target, collectionRef.current);
592
+ dispatchVirtualFocus(collectionRef.current!, target);
593
+ });
594
+ }
595
+ };
596
+
597
+ // Clicking back into the input can happen after focus moved elsewhere in the dialog, while
598
+ // virtual focus is still on an option. Clear virtual focus on pointer down so mouse
599
+ // interactions restore the input state before the click's focus handling runs.
600
+ // Touch is excluded because touch interactions should not move focus back to the input.
601
+ // octane adaptation: `e` is the NATIVE PointerEvent (upstream: React's PointerEvent).
602
+ let onPointerDown = (e: any) => {
603
+ if (
604
+ e.button !== 0 ||
605
+ e.pointerType === 'touch' ||
606
+ queuedActiveDescendant.current == null ||
607
+ inputRef.current == null
608
+ ) {
609
+ return;
610
+ }
611
+
612
+ if (getEventTarget(e) === inputRef.current) {
613
+ clearVirtualFocus();
614
+ }
615
+ };
616
+
617
+ // Only apply the autocomplete specific behaviors if the collection component wrapped by it is actually
618
+ // being filtered/allows filtering by the Autocomplete.
619
+ let inputProps = {
620
+ value: state.inputValue,
621
+ onChange,
622
+ } as AriaTextFieldProps;
623
+
624
+ let virtualFocusProps = {
625
+ onKeyDown,
626
+ 'aria-activedescendant': state.focusedNodeId ?? undefined,
627
+ onBlur,
628
+ onFocus,
629
+ onPointerDown,
630
+ };
631
+
632
+ inputProps = {
633
+ ...inputProps,
634
+ ...(shouldUseVirtualFocus && hasCollection && virtualFocusProps),
635
+ enterKeyHint: 'go',
636
+ 'aria-controls': hasCollection ? collectionId : undefined,
637
+ // TODO: readd proper logic for completionMode = complete (aria-autocomplete: both)
638
+ 'aria-autocomplete': 'list',
639
+ // This disable's iOS's autocorrect suggestions, since the autocomplete provides its own suggestions.
640
+ autoCorrect: 'off',
641
+ // This disable's the macOS Safari spell check auto corrections.
642
+ spellCheck: 'false',
643
+ autoComplete: 'off',
644
+ };
645
+
646
+ return {
647
+ inputProps,
648
+ collectionProps: mergeProps(collectionProps, {
649
+ shouldUseVirtualFocus,
650
+ disallowTypeAhead: shouldUseVirtualFocus,
651
+ autoFocus: autoFocusOnMount ? 'first' : false,
652
+ }),
653
+ collectionRef: mergedCollectionRef,
654
+ filter: filter != null ? filterFn : undefined,
655
+ };
656
+ }