@octanejs/aria 0.0.46 → 0.0.48

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 (201) hide show
  1. package/README.md +7 -6
  2. package/UPSTREAM.md +58 -69
  3. package/package.json +11 -11
  4. package/src/calendar/useCalendarGrid.tsx +64 -62
  5. package/src/collections/Document.ts +3 -0
  6. package/src/color/useColorArea.tsx +95 -69
  7. package/src/color/useColorWheel.tsx +38 -37
  8. package/src/combobox/{useComboBox.ts → useComboBox.tsx} +243 -258
  9. package/src/components/ComboBox.ts +35 -2
  10. package/src/components/Dialog.ts +12 -1
  11. package/src/components/DropZone.tsx +3 -2
  12. package/src/components/FileTrigger.tsx +1 -0
  13. package/src/components/GridList.ts +11 -2
  14. package/src/components/ListBox.ts +1 -1
  15. package/src/components/Menu.ts +1 -0
  16. package/src/components/Meter.ts +3 -1
  17. package/src/components/Popover.ts +20 -8
  18. package/src/components/PreviewTrigger.tsx +101 -0
  19. package/src/components/ProgressBar.ts +10 -2
  20. package/src/components/Select.ts +1 -1
  21. package/src/components/Table.ts +26 -15
  22. package/src/components/TokenField.tsx +395 -0
  23. package/src/components/Tooltip.ts +4 -6
  24. package/src/components/Tree.ts +14 -7
  25. package/src/components/Virtualizer.tsx +30 -14
  26. package/src/components/index.ts +22 -0
  27. package/src/components/utils.ts +1 -1
  28. package/src/datepicker/useDateField.tsx +2 -11
  29. package/src/datepicker/useDatePickerGroup.tsx +44 -33
  30. package/src/datepicker/useDateSegment.tsx +31 -30
  31. package/src/dialog/useDialog.ts +8 -1
  32. package/src/dnd/DragManager.tsx +5 -1
  33. package/src/dnd/useDrag.tsx +2 -1
  34. package/src/focus/FocusScope.ts +2 -2
  35. package/src/grid/useGrid.ts +3 -0
  36. package/src/grid/{useGridCell.ts → useGridCell.tsx} +128 -66
  37. package/src/grid/utils.ts +1 -0
  38. package/src/gridlist/{useGridListItem.ts → useGridListItem.tsx} +154 -112
  39. package/src/index.ts +22 -0
  40. package/src/interactions/createEventHandler.ts +11 -8
  41. package/src/interactions/createKeyboardShortcutHandler.tsx +218 -0
  42. package/src/interactions/textSelection.ts +3 -3
  43. package/src/interactions/useContextMenu.tsx +125 -0
  44. package/src/interactions/useFocusVisible.ts +4 -4
  45. package/src/interactions/useKeyboard.ts +50 -2
  46. package/src/interactions/useLongPress.ts +33 -22
  47. package/src/interactions/useMove.ts +6 -6
  48. package/src/interactions/usePress.ts +8 -0
  49. package/src/interactions/utils.ts +10 -10
  50. package/src/intl/combobox/fi-FI.json +2 -2
  51. package/src/intl/previewtrigger/ar-AE.json +3 -0
  52. package/src/intl/previewtrigger/bg-BG.json +3 -0
  53. package/src/intl/previewtrigger/cs-CZ.json +3 -0
  54. package/src/intl/previewtrigger/da-DK.json +3 -0
  55. package/src/intl/previewtrigger/de-DE.json +3 -0
  56. package/src/intl/previewtrigger/el-GR.json +3 -0
  57. package/src/intl/previewtrigger/en-US.json +3 -0
  58. package/src/intl/previewtrigger/es-ES.json +3 -0
  59. package/src/intl/previewtrigger/et-EE.json +3 -0
  60. package/src/intl/previewtrigger/fi-FI.json +3 -0
  61. package/src/intl/previewtrigger/fr-FR.json +3 -0
  62. package/src/intl/previewtrigger/he-IL.json +3 -0
  63. package/src/intl/previewtrigger/hr-HR.json +3 -0
  64. package/src/intl/previewtrigger/hu-HU.json +3 -0
  65. package/src/intl/previewtrigger/index.ts +73 -0
  66. package/src/intl/previewtrigger/it-IT.json +3 -0
  67. package/src/intl/previewtrigger/ja-JP.json +3 -0
  68. package/src/intl/previewtrigger/ko-KR.json +3 -0
  69. package/src/intl/previewtrigger/lt-LT.json +3 -0
  70. package/src/intl/previewtrigger/lv-LV.json +3 -0
  71. package/src/intl/previewtrigger/nb-NO.json +3 -0
  72. package/src/intl/previewtrigger/nl-NL.json +3 -0
  73. package/src/intl/previewtrigger/pl-PL.json +3 -0
  74. package/src/intl/previewtrigger/pt-BR.json +3 -0
  75. package/src/intl/previewtrigger/pt-PT.json +3 -0
  76. package/src/intl/previewtrigger/ro-RO.json +3 -0
  77. package/src/intl/previewtrigger/ru-RU.json +3 -0
  78. package/src/intl/previewtrigger/sk-SK.json +3 -0
  79. package/src/intl/previewtrigger/sl-SI.json +3 -0
  80. package/src/intl/previewtrigger/sr-SP.json +3 -0
  81. package/src/intl/previewtrigger/sv-SE.json +3 -0
  82. package/src/intl/previewtrigger/tr-TR.json +3 -0
  83. package/src/intl/previewtrigger/uk-UA.json +3 -0
  84. package/src/intl/previewtrigger/zh-CN.json +3 -0
  85. package/src/intl/previewtrigger/zh-TW.json +3 -0
  86. package/src/intl/table/ar-AE.json +3 -3
  87. package/src/intl/table/bg-BG.json +3 -3
  88. package/src/intl/table/cs-CZ.json +3 -3
  89. package/src/intl/table/da-DK.json +3 -3
  90. package/src/intl/table/de-DE.json +3 -3
  91. package/src/intl/table/el-GR.json +3 -3
  92. package/src/intl/table/es-ES.json +3 -3
  93. package/src/intl/table/et-EE.json +3 -3
  94. package/src/intl/table/fi-FI.json +3 -3
  95. package/src/intl/table/fr-FR.json +3 -3
  96. package/src/intl/table/he-IL.json +3 -3
  97. package/src/intl/table/hr-HR.json +3 -3
  98. package/src/intl/table/hu-HU.json +3 -3
  99. package/src/intl/table/it-IT.json +3 -3
  100. package/src/intl/table/ja-JP.json +3 -3
  101. package/src/intl/table/ko-KR.json +3 -3
  102. package/src/intl/table/lt-LT.json +3 -3
  103. package/src/intl/table/lv-LV.json +3 -3
  104. package/src/intl/table/nb-NO.json +3 -3
  105. package/src/intl/table/nl-NL.json +3 -3
  106. package/src/intl/table/pl-PL.json +3 -3
  107. package/src/intl/table/pt-BR.json +3 -3
  108. package/src/intl/table/pt-PT.json +3 -3
  109. package/src/intl/table/ro-RO.json +3 -3
  110. package/src/intl/table/ru-RU.json +3 -3
  111. package/src/intl/table/sk-SK.json +3 -3
  112. package/src/intl/table/sl-SI.json +3 -3
  113. package/src/intl/table/sr-SP.json +3 -3
  114. package/src/intl/table/sv-SE.json +3 -3
  115. package/src/intl/table/tr-TR.json +3 -3
  116. package/src/intl/table/uk-UA.json +3 -3
  117. package/src/intl/table/zh-CN.json +4 -4
  118. package/src/intl/table/zh-TW.json +3 -3
  119. package/src/intl/tree/es-ES.json +1 -1
  120. package/src/link/useLink.ts +2 -0
  121. package/src/menu/useMenuItem.ts +33 -36
  122. package/src/menu/useMenuTrigger.tsx +222 -0
  123. package/src/menu/{useSubmenuTrigger.ts → useSubmenuTrigger.tsx} +116 -151
  124. package/src/numberfield/{useNumberField.ts → useNumberField.tsx} +188 -238
  125. package/src/overlays/Overlay.ts +5 -1
  126. package/src/overlays/ariaHideOutside.ts +6 -8
  127. package/src/overlays/useCloseOnScroll.ts +7 -5
  128. package/src/overlays/{useOverlay.ts → useOverlay.tsx} +76 -83
  129. package/src/overlays/usePopover.ts +10 -5
  130. package/src/overlays/usePreventScroll.ts +4 -4
  131. package/src/progress/useProgressBar.ts +2 -1
  132. package/src/radio/{useRadioGroup.ts → useRadioGroup.tsx} +87 -79
  133. package/src/searchfield/useSearchField.tsx +152 -0
  134. package/src/select/useSelect.ts +20 -20
  135. package/src/selection/useSelectableCollection.tsx +755 -0
  136. package/src/selection/useSelectableItem.ts +48 -11
  137. package/src/slider/{useSliderThumb.ts → useSliderThumb.tsx} +120 -158
  138. package/src/spinbutton/useSpinButton.tsx +335 -0
  139. package/src/stately/combobox/useComboBoxState.ts +1 -1
  140. package/src/stately/grid/useGridState.ts +9 -10
  141. package/src/stately/index.ts +11 -0
  142. package/src/stately/menu/useMenuTriggerState.ts +1 -1
  143. package/src/stately/menu/useSubmenuTriggerState.ts +2 -0
  144. package/src/stately/overlays/useOverlayTriggerState.ts +12 -1
  145. package/src/stately/radio/useRadioGroupState.ts +5 -1
  146. package/src/stately/table/TableUtils.ts +15 -1
  147. package/src/stately/table/useTableState.ts +7 -0
  148. package/src/stately/tokenfield/TokenFieldValue.tsx +440 -0
  149. package/src/stately/tokenfield/useTokenFieldState.tsx +57 -0
  150. package/src/stately/tooltip/useTooltipTriggerState.ts +16 -7
  151. package/src/stately/virtualizer/Layout.tsx +2 -0
  152. package/src/stately/virtualizer/ScrollAnchor.tsx +338 -0
  153. package/src/stately/virtualizer/Virtualizer.tsx +52 -1
  154. package/src/stately/virtualizer/types.tsx +2 -0
  155. package/src/table/useTableCell.ts +12 -0
  156. package/src/table/useTableColumnHeader.ts +8 -1
  157. package/src/table/useTableColumnResize.tsx +359 -0
  158. package/src/tag/useTag.ts +22 -15
  159. package/src/textfield/useTextField.ts +2 -0
  160. package/src/tokenfield/useToken.tsx +64 -0
  161. package/src/tokenfield/useTokenField.tsx +849 -0
  162. package/src/tooltip/usePreviewTrigger.tsx +266 -0
  163. package/src/tooltip/useSafeArea.tsx +171 -0
  164. package/src/upstream-exports/react-aria/mergeRefs.tsx +3 -0
  165. package/src/upstream-exports/react-aria/usePreviewTrigger.tsx +18 -0
  166. package/src/upstream-exports/react-aria/useTokenField.tsx +22 -0
  167. package/src/upstream-exports/react-stately/private/collections/getChildNodes.tsx +8 -0
  168. package/src/upstream-exports/react-stately/private/collections/getItemCount.tsx +2 -0
  169. package/src/upstream-exports/react-stately/private/grid/GridCollection.tsx +7 -0
  170. package/src/upstream-exports/react-stately/private/grid/useGridState.tsx +6 -0
  171. package/src/upstream-exports/react-stately/private/utils/number.tsx +2 -0
  172. package/src/upstream-exports/react-stately/useComboBoxState.tsx +23 -0
  173. package/src/upstream-exports/react-stately/useListState.tsx +15 -0
  174. package/src/upstream-exports/react-stately/useMenuTriggerState.tsx +26 -0
  175. package/src/upstream-exports/react-stately/useMultipleSelectionState.tsx +21 -0
  176. package/src/upstream-exports/react-stately/useNumberFieldState.tsx +20 -0
  177. package/src/upstream-exports/react-stately/useOverlayTriggerState.tsx +20 -0
  178. package/src/upstream-exports/react-stately/useRadioGroupState.tsx +16 -0
  179. package/src/upstream-exports/react-stately/useSearchFieldState.tsx +19 -0
  180. package/src/upstream-exports/react-stately/useSliderState.tsx +20 -0
  181. package/src/upstream-exports/react-stately/useTableState.tsx +45 -0
  182. package/src/upstream-exports/react-stately/useTokenFieldState.tsx +25 -0
  183. package/src/upstream-exports/react-stately/useTooltipTriggerState.tsx +20 -0
  184. package/src/upstream-exports/react-stately/useTreeState.tsx +14 -0
  185. package/src/utils/domHelpers.ts +74 -17
  186. package/src/utils/isVirtualEvent.ts +2 -1
  187. package/src/utils/openLink.ts +8 -3
  188. package/src/utils/platform.ts +7 -3
  189. package/src/utils/scrollIntoView.ts +4 -4
  190. package/src/utils/shadowdom/DOMFunctions.ts +42 -2
  191. package/src/utils/useId.ts +11 -2
  192. package/src/utils/useLoadMoreSentinel.ts +8 -3
  193. package/src/utils/useViewportSize.ts +3 -3
  194. package/src/virtualizer/ScrollView.tsx +13 -4
  195. package/src/virtualizer/VirtualizerItem.tsx +4 -1
  196. package/src/virtualizer/useVirtualizerItem.tsx +43 -3
  197. package/src/menu/useMenuTrigger.ts +0 -189
  198. package/src/searchfield/useSearchField.ts +0 -156
  199. package/src/selection/useSelectableCollection.ts +0 -750
  200. package/src/spinbutton/useSpinButton.ts +0 -380
  201. package/src/table/useTableColumnResize.ts +0 -374
@@ -1,18 +1,22 @@
1
- // Ported from react-aria (source: .react-spectrum/packages/react-aria/src/combobox/useComboBox.ts).
2
- // octane adaptations:
3
- // - The combobox text input is a TEXT input: upstream feeds the input value via a synthetic
4
- // `onChange` passed to useTextField. octane's useTextField already wires that `onChange`
5
- // onto the NATIVE `input` event (see textfield/useTextField.ts). So this port keeps
6
- // `onChange: state.setInputValue` VERBATIM — the onChange→onInput DOM wiring lives in
7
- // useTextField, and the value-level `onInputChange(value)` public callback is unchanged.
8
- // - onKeyDown receives the ported BaseEvent Proxy (from useKeyboard/createEventHandler via
9
- // useTextField's useFocusable). `.nativeEvent.isComposing` → `e.isComposing` (the Proxy
10
- // forwards the read to the live native event); `continuePropagation`/`preventDefault` work
11
- // through the Proxy. FocusEvent/TouchEvent become native DOM events; element types → `any`.
12
- // - The Parcel glob intl import becomes the generated '../intl/combobox' dictionary index.
13
- // - Public-hook slot threading (splitSlot/subSlot) per the binding convention; the module-local
14
- // `useValueId` helper takes a threaded slot; dep-less upstream useEffects become explicit `null`.
15
- import type {
1
+ /** @jsxImportSource octane */
2
+ type DOMAttributes = Record<string, any>;
3
+ // Ported from adobe/react-spectrum@5ecb3333001313e83898cd07644227897e3bae1f (packages/react-aria/src/combobox/useComboBox.ts).
4
+ /*
5
+ * Copyright 2020 Adobe. All rights reserved.
6
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License. You may obtain a copy
8
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software distributed under
11
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
12
+ * OF ANY KIND, either express or implied. See the License for the specific language
13
+ * governing permissions and limitations under the License.
14
+ */
15
+
16
+ import { announce } from '../live-announcer/LiveAnnouncer';
17
+ import { AriaButtonProps } from '../button/useButton';
18
+ import { ariaHideOutside } from '../overlays/ariaHideOutside';
19
+ import {
16
20
  AriaLabelingProps,
17
21
  DOMProps,
18
22
  InputDOMProps,
@@ -23,32 +27,39 @@ import type {
23
27
  RouterOptions,
24
28
  ValidationResult,
25
29
  } from '@react-types/shared';
26
- import { useEffect, useMemo, useRef, useState } from 'octane';
27
-
28
- import { announce } from '../live-announcer/LiveAnnouncer';
29
- import type { AriaButtonProps } from '../button/useButton';
30
- import { ariaHideOutside } from '../overlays/ariaHideOutside';
31
- import type { AriaListBoxOptions } from '../listbox/useListBox';
30
+ import { AriaListBoxOptions } from '../listbox/useListBox';
32
31
  import { chain } from '../utils/chain';
33
32
  import {
34
- type ComboBoxState,
35
- type SelectionMode,
36
- type ComboBoxProps,
37
- } from '../stately/combobox/useComboBoxState';
33
+ ComboBoxProps,
34
+ ComboBoxState,
35
+ SelectionMode,
36
+ } from '../upstream-exports/react-stately/useComboBoxState';
38
37
  import { dispatchVirtualFocus } from '../focus/virtualFocus';
38
+ import {
39
+ ElementType,
40
+ InputHTMLAttributes,
41
+ TouchEvent,
42
+ useEffect,
43
+ useMemo,
44
+ useRef,
45
+ useState,
46
+ } from '../compat/react';
39
47
  import { getActiveElement, getEventTarget, nodeContains } from '../utils/shadowdom/DOMFunctions';
40
- import { getChildNodes } from '../stately/collections/getChildNodes';
41
- import { getItemCount } from '../stately/collections/getItemCount';
48
+ import { getChildNodes } from '../upstream-exports/react-stately/private/collections/getChildNodes';
49
+ import { getItemCount } from '../upstream-exports/react-stately/private/collections/getItemCount';
50
+ import { withSlot } from 'octane';
42
51
  import { getItemId, listData } from '../listbox/utils';
43
52
  import { getOwnerDocument } from '../utils/domHelpers';
44
- import intlMessages from '../intl/combobox';
53
+ import intlMessages from '../intl/combobox/index';
45
54
  import { isAppleDevice } from '../utils/platform';
46
55
  import { ListKeyboardDelegate } from '../selection/ListKeyboardDelegate';
47
56
  import { mergeProps } from '../utils/mergeProps';
48
- import { privateValidationStateProp } from '../stately/form/useFormValidationState';
57
+ import { privateValidationStateProp } from '../upstream-exports/react-stately/private/form/useFormValidationState';
58
+ import { setInteractionModality } from '../interactions/useFocusVisible';
49
59
  import { useEvent } from '../utils/useEvent';
50
60
  import { useFormReset } from '../utils/useFormReset';
51
61
  import { useId } from '../utils/useId';
62
+ import { useKeyboard } from '../interactions/useKeyboard';
52
63
  import { useLabels } from '../utils/useLabels';
53
64
  import { useLocalizedStringFormatter } from '../i18n/useLocalizedStringFormatter';
54
65
  import { useMenuTrigger } from '../menu/useMenuTrigger';
@@ -57,12 +68,6 @@ import { useSelectableCollection } from '../selection/useSelectableCollection';
57
68
  import { useTextField } from '../textfield/useTextField';
58
69
  import { useUpdateEffect } from '../utils/useUpdateEffect';
59
70
 
60
- import { S, splitSlot, subSlot } from '../internal';
61
-
62
- // octane adaptation: element/DOM attribute types collapse to structural bags (upstream's
63
- // versions are typed over React's SyntheticEvent / JSX intrinsic-element machinery).
64
- type DOMAttributes = Record<string, any>;
65
-
66
71
  export interface AriaComboBoxProps<T, M extends SelectionMode = 'single'>
67
72
  extends ComboBoxProps<T, M>, DOMProps, InputDOMProps, AriaLabelingProps {
68
73
  /** Whether keyboard navigation is circular. */
@@ -81,6 +86,12 @@ export interface AriaComboBoxOptions<T, M extends SelectionMode = 'single'> exte
81
86
  listBoxRef: RefObject<HTMLElement | null>;
82
87
  /** The ref for the optional list box popup trigger button. */
83
88
  buttonRef?: RefObject<Element | null>;
89
+ /**
90
+ * The HTML element used to render the label, e.g. 'label', or 'span'.
91
+ *
92
+ * @default 'label'
93
+ */
94
+ labelElementType?: ElementType;
84
95
  /** An optional keyboard delegate implementation, to override the default. */
85
96
  keyboardDelegate?: KeyboardDelegate;
86
97
  /**
@@ -95,7 +106,7 @@ export interface ComboBoxAria<T> extends ValidationResult {
95
106
  /** Props for the label element. */
96
107
  labelProps: DOMAttributes;
97
108
  /** Props for the combo box input element. */
98
- inputProps: DOMAttributes;
109
+ inputProps: InputHTMLAttributes<HTMLInputElement>;
99
110
  /** Props for the list box, to be passed to `useListBox`. */
100
111
  listBoxProps: AriaListBoxOptions<T>;
101
112
  /** Props for the optional trigger button, to be passed to `useButton`. */
@@ -119,18 +130,11 @@ export interface ComboBoxAria<T> extends ValidationResult {
119
130
  export function useComboBox<T, M extends SelectionMode = 'single'>(
120
131
  props: AriaComboBoxOptions<T, M>,
121
132
  state: ComboBoxState<T, M>,
122
- ): ComboBoxAria<T>;
123
- // Slot-threading form: sibling ported hooks pass their derived sub-slot as the trailing arg.
124
- export function useComboBox<T, M extends SelectionMode = 'single'>(
125
- props: AriaComboBoxOptions<T, M>,
126
- state: ComboBoxState<T, M>,
127
- slot: symbol | undefined,
128
- ): ComboBoxAria<T>;
129
- export function useComboBox(...args: any[]): ComboBoxAria<any> {
130
- const [user, slotArg] = splitSlot(args);
131
- const slot = slotArg ?? S('useComboBox');
132
- const props = user[0] as AriaComboBoxOptions<any, any>;
133
- const state = user[1] as ComboBoxState<any, any>;
133
+ _slot?: symbol,
134
+ ): ComboBoxAria<T> {
135
+ if (_slot !== undefined) {
136
+ return withSlot(_slot, useComboBox<T, M>, props, state);
137
+ }
134
138
 
135
139
  let {
136
140
  buttonRef,
@@ -144,22 +148,17 @@ export function useComboBox(...args: any[]): ComboBoxAria<any> {
144
148
  isReadOnly,
145
149
  isDisabled,
146
150
  } = props;
147
- let backupBtnRef = useRef(null, subSlot(slot, 'backupBtn'));
151
+ let backupBtnRef = useRef(null);
148
152
  buttonRef = buttonRef ?? backupBtnRef;
149
153
 
150
- let stringFormatter = useLocalizedStringFormatter(
151
- intlMessages,
152
- '@react-aria/combobox',
153
- subSlot(slot, 'formatter'),
154
- );
155
- let { menuTriggerProps, menuProps } = useMenuTrigger<any>(
154
+ let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-aria/combobox');
155
+ let { menuTriggerProps, menuProps } = useMenuTrigger<T>(
156
156
  {
157
157
  type: 'listbox',
158
158
  isDisabled: isDisabled || isReadOnly,
159
159
  },
160
160
  state,
161
161
  buttonRef,
162
- subSlot(slot, 'menuTrigger'),
163
162
  );
164
163
 
165
164
  // Set listbox id so it can be used when calling getItemId later
@@ -179,41 +178,28 @@ export function useComboBox(...args: any[]): ComboBoxAria<any> {
179
178
  layoutDelegate,
180
179
  }),
181
180
  [keyboardDelegate, layoutDelegate, collection, disabledKeys, listBoxRef],
182
- subSlot(slot, 'delegate'),
183
181
  );
184
182
 
185
183
  // Use useSelectableCollection to get the keyboard handlers to apply to the textfield
186
- let { collectionProps } = useSelectableCollection(
187
- {
188
- selectionManager: state.selectionManager,
189
- keyboardDelegate: delegate,
190
- disallowTypeAhead: true,
191
- disallowEmptySelection: true,
192
- shouldFocusWrap,
193
- ref: inputRef,
194
- // Prevent item scroll behavior from being applied here, should be handled in the user's Popover + ListBox component
195
- isVirtualized: true,
196
- },
197
- subSlot(slot, 'selectable'),
198
- );
184
+ let { collectionProps } = useSelectableCollection({
185
+ selectionManager: state.selectionManager,
186
+ keyboardDelegate: delegate,
187
+ disallowTypeAhead: true,
188
+ disallowEmptySelection: true,
189
+ shouldFocusWrap,
190
+ ref: inputRef,
191
+ // Prevent item scroll behavior from being applied here, should be handled in the user's Popover + ListBox component
192
+ isVirtualized: true,
193
+ });
199
194
 
200
195
  let router = useRouter();
201
196
 
202
- // For textfield specific keydown operations
203
- let onKeyDown = (e: any) => {
204
- // octane adaptation: `.nativeEvent.isComposing` → `e.isComposing` (the BaseEvent Proxy
205
- // forwards the read to the live native event).
206
- if (e.isComposing) {
207
- return;
208
- }
209
- switch (e.key) {
210
- case 'Enter':
211
- case 'Tab':
212
- // Prevent form submission if menu is open since we may be selecting a option
213
- if (state.isOpen && e.key === 'Enter') {
214
- e.preventDefault();
215
- }
216
-
197
+ // for textfield specific operations
198
+ let { keyboardProps } = useKeyboard({
199
+ shortcuts: {
200
+ Enter: (e) => {
201
+ // Prevent default form submission if menu is open since we may be selecting a option
202
+ let shouldPreventDefault = state.isOpen;
217
203
  // If the focused item is a link, trigger opening it. Items that are links are not selectable.
218
204
  if (state.isOpen && listBoxRef.current && state.selectionManager.focusedKey != null) {
219
205
  let collectionItem = state.collection.getItem(state.selectionManager.focusedKey);
@@ -221,7 +207,7 @@ export function useComboBox(...args: any[]): ComboBoxAria<any> {
221
207
  let item = listBoxRef.current.querySelector(
222
208
  `[data-key="${CSS.escape(state.selectionManager.focusedKey.toString())}"]`,
223
209
  );
224
- if (e.key === 'Enter' && item instanceof HTMLAnchorElement) {
210
+ if (item instanceof HTMLAnchorElement) {
225
211
  router.open(
226
212
  item,
227
213
  e,
@@ -230,43 +216,70 @@ export function useComboBox(...args: any[]): ComboBoxAria<any> {
230
216
  );
231
217
  }
232
218
  state.close();
233
- break;
219
+ return { shouldPreventDefault };
234
220
  } else if (collectionItem?.props.onAction) {
235
221
  collectionItem.props.onAction();
236
222
  state.close();
237
- break;
223
+ return { shouldPreventDefault };
238
224
  }
239
225
  }
240
- if (e.key === 'Enter' || state.isOpen) {
241
- state.commit();
226
+ state.commit();
227
+ return { shouldPreventDefault };
228
+ },
229
+ Tab: () => {
230
+ // If the focused item is a link, trigger opening it. Items that are links are not selectable.
231
+ if (state.isOpen && listBoxRef.current && state.selectionManager.focusedKey != null) {
232
+ let collectionItem = state.collection.getItem(state.selectionManager.focusedKey);
233
+ if (collectionItem?.props.href) {
234
+ state.close();
235
+ return { shouldPreventDefault: false };
236
+ } else if (collectionItem?.props.onAction) {
237
+ collectionItem.props.onAction();
238
+ state.close();
239
+ return { shouldPreventDefault: false, shouldContinuePropagation: true };
240
+ }
242
241
  }
243
- if (e.key === 'Tab') {
244
- e.continuePropagation();
242
+ if (state.isOpen) {
243
+ state.commit();
245
244
  }
246
-
247
- break;
248
- case 'Escape':
245
+ return { shouldPreventDefault: false, shouldContinuePropagation: true };
246
+ },
247
+ Escape: () => {
248
+ let shouldContinuePropagation = false;
249
249
  if (!state.selectionManager.isEmpty || state.inputValue === '' || props.allowsCustomValue) {
250
- e.continuePropagation();
250
+ shouldContinuePropagation = true;
251
251
  }
252
252
  state.revert();
253
- break;
254
- case 'ArrowDown':
253
+ return { shouldContinuePropagation };
254
+ },
255
+ },
256
+ });
257
+
258
+ let { keyboardProps: repeatKeyboardProps } = useKeyboard({
259
+ shortcuts: {
260
+ ArrowDown: () => {
255
261
  state.open('first', 'manual');
256
- break;
257
- case 'ArrowUp':
262
+ return { shouldPreventDefault: false };
263
+ },
264
+ ArrowUp: () => {
258
265
  state.open('last', 'manual');
259
- break;
260
- case 'ArrowLeft':
261
- case 'ArrowRight':
266
+ return { shouldPreventDefault: false };
267
+ },
268
+ ArrowLeft: () => {
262
269
  state.selectionManager.setFocusedKey(null);
263
- break;
264
- }
265
- };
270
+ return { shouldPreventDefault: false };
271
+ },
272
+ ArrowRight: () => {
273
+ state.selectionManager.setFocusedKey(null);
274
+ return { shouldPreventDefault: false };
275
+ },
276
+ },
277
+ allowRepeats: true,
278
+ });
266
279
 
267
- let onBlur = (e: any) => {
280
+ let onBlur = (e: FocusEvent) => {
268
281
  let blurFromButton = buttonRef?.current && buttonRef.current === e.relatedTarget;
269
- let blurIntoPopover = nodeContains(popoverRef.current, e.relatedTarget);
282
+ let blurIntoPopover = nodeContains(popoverRef.current, e.relatedTarget as Node | null);
270
283
  // Ignore blur if focused moved to the button(if exists) or into the popover.
271
284
  if (blurFromButton || blurIntoPopover) {
272
285
  return;
@@ -279,7 +292,7 @@ export function useComboBox(...args: any[]): ComboBoxAria<any> {
279
292
  state.setFocused(false);
280
293
  };
281
294
 
282
- let onFocus = (e: any) => {
295
+ let onFocus = (e: FocusEvent) => {
283
296
  if (state.isFocused) {
284
297
  return;
285
298
  }
@@ -291,10 +304,10 @@ export function useComboBox(...args: any[]): ComboBoxAria<any> {
291
304
  state.setFocused(true);
292
305
  };
293
306
 
294
- let valueId = useValueId(
295
- [state.selectionManager.selectedKeys, state.selectionManager.selectionMode],
296
- subSlot(slot, 'valueId'),
297
- );
307
+ let valueId = useValueId([
308
+ state.selectionManager.selectedKeys,
309
+ state.selectionManager.selectionMode,
310
+ ]);
298
311
  let { isInvalid, validationErrors, validationDetails } = state.displayValidation;
299
312
  let { labelProps, inputProps, descriptionProps, errorMessageProps } = useTextField(
300
313
  {
@@ -306,7 +319,13 @@ export function useComboBox(...args: any[]): ComboBoxAria<any> {
306
319
  : props.isRequired,
307
320
  onChange: state.setInputValue,
308
321
  onKeyDown: !isReadOnly
309
- ? chain(state.isOpen && collectionProps.onKeyDown, onKeyDown, props.onKeyDown)
322
+ ? // oxlint-disable-next-line react/react-compiler
323
+ chain(
324
+ state.isOpen && collectionProps.onKeyDown,
325
+ keyboardProps.onKeyDown,
326
+ repeatKeyboardProps.onKeyDown,
327
+ props.onKeyDown,
328
+ )
310
329
  : props.onKeyDown,
311
330
  onBlur,
312
331
  value: state.inputValue,
@@ -319,10 +338,9 @@ export function useComboBox(...args: any[]): ComboBoxAria<any> {
319
338
  [valueId, props['aria-describedby']].filter(Boolean).join(' ') || undefined,
320
339
  },
321
340
  inputRef,
322
- subSlot(slot, 'textField'),
323
341
  );
324
342
 
325
- useFormReset(inputRef, state.defaultValue, state.setValue, subSlot(slot, 'formReset'));
343
+ useFormReset(inputRef, state.defaultValue, state.setValue);
326
344
 
327
345
  // Press handlers for the ComboBox button
328
346
  let onPress = (e: PressEvent) => {
@@ -343,29 +361,21 @@ export function useComboBox(...args: any[]): ComboBoxAria<any> {
343
361
  }
344
362
  };
345
363
 
346
- let triggerLabelProps = useLabels(
347
- {
348
- id: menuTriggerProps.id,
349
- 'aria-label': stringFormatter.format('buttonLabel'),
350
- 'aria-labelledby': props['aria-labelledby'] || labelProps.id,
351
- },
352
- undefined,
353
- subSlot(slot, 'triggerLabels'),
354
- );
364
+ let triggerLabelProps = useLabels({
365
+ id: menuTriggerProps.id,
366
+ 'aria-label': stringFormatter.format('buttonLabel'),
367
+ 'aria-labelledby': props['aria-labelledby'] || labelProps.id,
368
+ });
355
369
 
356
- let listBoxProps = useLabels(
357
- {
358
- id: menuProps.id,
359
- 'aria-label': stringFormatter.format('listboxLabel'),
360
- 'aria-labelledby': props['aria-labelledby'] || labelProps.id,
361
- },
362
- undefined,
363
- subSlot(slot, 'listBoxLabels'),
364
- );
370
+ let listBoxProps = useLabels({
371
+ id: menuProps.id,
372
+ 'aria-label': stringFormatter.format('listboxLabel'),
373
+ 'aria-labelledby': props['aria-labelledby'] || labelProps.id,
374
+ });
365
375
 
366
376
  // If a touch happens on direct center of ComboBox input, might be virtual click from iPad so open ComboBox menu
367
- let lastEventTime = useRef(0, subSlot(slot, 'lastEventTime'));
368
- let onTouchEnd = (e: any) => {
377
+ let lastEventTime = useRef(0);
378
+ let onTouchEnd = (e: TouchEvent) => {
369
379
  if (isDisabled || isReadOnly) {
370
380
  return;
371
381
  }
@@ -401,117 +411,89 @@ export function useComboBox(...args: any[]): ComboBoxAria<any> {
401
411
  : undefined;
402
412
  let sectionKey = focusedItem?.parentKey ?? null;
403
413
  let itemKey = state.selectionManager.focusedKey ?? null;
404
- let lastSection = useRef(sectionKey, subSlot(slot, 'lastSection'));
405
- let lastItem = useRef(itemKey, subSlot(slot, 'lastItem'));
406
- // intentional omit dependency array, want this to happen on every render
407
- useEffect(
408
- () => {
409
- if (
410
- isAppleDevice() &&
411
- focusedItem != null &&
412
- itemKey != null &&
413
- itemKey !== lastItem.current
414
- ) {
415
- let isSelected = state.selectionManager.isSelected(itemKey);
416
- let section = sectionKey != null ? state.collection.getItem(sectionKey) : null;
417
- let sectionTitle =
418
- section?.['aria-label'] ||
419
- (typeof section?.rendered === 'string' ? section.rendered : '') ||
420
- '';
421
-
422
- let announcement = stringFormatter.format('focusAnnouncement', {
423
- isGroupChange: (section && sectionKey !== lastSection.current) ?? false,
424
- groupTitle: sectionTitle,
425
- groupCount: section ? [...getChildNodes(section, state.collection)].length : 0,
426
- optionText: focusedItem['aria-label'] || focusedItem.textValue || '',
427
- isSelected,
428
- });
429
-
430
- announce(announcement);
431
- }
432
-
433
- lastSection.current = sectionKey;
434
- lastItem.current = itemKey;
435
- },
436
- null,
437
- subSlot(slot, 'announceFocus'),
438
- );
414
+ let lastSection = useRef<typeof sectionKey>(sectionKey);
415
+ let lastItem = useRef<typeof itemKey>(itemKey);
416
+ useEffect(() => {
417
+ if (isAppleDevice() && focusedItem != null && itemKey != null && itemKey !== lastItem.current) {
418
+ let isSelected = state.selectionManager.isSelected(itemKey);
419
+ let section = sectionKey != null ? state.collection.getItem(sectionKey) : null;
420
+ let sectionTitle =
421
+ section?.['aria-label'] ||
422
+ (typeof section?.rendered === 'string' ? section.rendered : '') ||
423
+ '';
424
+
425
+ let announcement = stringFormatter.format('focusAnnouncement', {
426
+ isGroupChange: (section && sectionKey !== lastSection.current) ?? false,
427
+ groupTitle: sectionTitle,
428
+ groupCount: section ? [...getChildNodes(section, state.collection)].length : 0,
429
+ optionText: focusedItem['aria-label'] || focusedItem.textValue || '',
430
+ isSelected,
431
+ });
432
+
433
+ announce(announcement);
434
+ }
435
+
436
+ lastSection.current = sectionKey;
437
+ lastItem.current = itemKey;
438
+ });
439
439
 
440
440
  // Announce the number of available suggestions when it changes
441
441
  let optionCount = getItemCount(state.collection);
442
- let lastSize = useRef(optionCount, subSlot(slot, 'lastSize'));
443
- let lastOpen = useRef(state.isOpen, subSlot(slot, 'lastOpen'));
444
- // intentional omit dependency array, want this to happen on every render
445
- useEffect(
446
- () => {
447
- // Only announce the number of options available when the menu opens if there is no
448
- // focused item, otherwise screen readers will typically read e.g. "1 of 6".
449
- // The exception is VoiceOver since this isn't included in the message above.
450
- let didOpenWithoutFocusedItem =
451
- state.isOpen !== lastOpen.current &&
452
- (state.selectionManager.focusedKey == null || isAppleDevice());
453
-
454
- if (state.isOpen && (didOpenWithoutFocusedItem || optionCount !== lastSize.current)) {
455
- let announcement = stringFormatter.format('countAnnouncement', { optionCount });
456
- announce(announcement);
457
- }
458
-
459
- lastSize.current = optionCount;
460
- lastOpen.current = state.isOpen;
461
- },
462
- null,
463
- subSlot(slot, 'announceCount'),
464
- );
442
+ let lastSize = useRef(optionCount);
443
+ let lastOpen = useRef(state.isOpen);
444
+ useEffect(() => {
445
+ // Only announce the number of options available when the menu opens if there is no
446
+ // focused item, otherwise screen readers will typically read e.g. "1 of 6".
447
+ // The exception is VoiceOver since this isn't included in the message above.
448
+ let didOpenWithoutFocusedItem =
449
+ state.isOpen !== lastOpen.current &&
450
+ (state.selectionManager.focusedKey == null || isAppleDevice());
451
+
452
+ if (state.isOpen && (didOpenWithoutFocusedItem || optionCount !== lastSize.current)) {
453
+ let announcement = stringFormatter.format('countAnnouncement', { optionCount });
454
+ announce(announcement);
455
+ }
456
+
457
+ lastSize.current = optionCount;
458
+ lastOpen.current = state.isOpen;
459
+ });
465
460
 
466
461
  // Announce when a selection occurs for VoiceOver. Other screen readers typically do this automatically.
467
462
  // TODO: do we need to do this for multi-select?
468
- let lastSelectedKey = useRef(state.selectedKey, subSlot(slot, 'lastSelectedKey'));
469
- // intentional omit dependency array, want this to happen on every render
470
- useEffect(
471
- () => {
472
- if (
473
- isAppleDevice() &&
474
- state.isFocused &&
475
- state.selectedItem &&
476
- state.selectedKey !== lastSelectedKey.current
477
- ) {
478
- let optionText = state.selectedItem['aria-label'] || state.selectedItem.textValue || '';
479
- let announcement = stringFormatter.format('selectedAnnouncement', { optionText });
480
- announce(announcement);
481
- }
482
-
483
- lastSelectedKey.current = state.selectedKey;
484
- },
485
- null,
486
- subSlot(slot, 'announceSelected'),
487
- );
463
+ let lastSelectedKey = useRef<typeof state.selectedKey>(state.selectedKey);
464
+ useEffect(() => {
465
+ if (
466
+ isAppleDevice() &&
467
+ state.isFocused &&
468
+ state.selectedItem &&
469
+ state.selectedKey !== lastSelectedKey.current
470
+ ) {
471
+ let optionText = state.selectedItem['aria-label'] || state.selectedItem.textValue || '';
472
+ let announcement = stringFormatter.format('selectedAnnouncement', { optionText });
473
+ announce(announcement);
474
+ }
488
475
 
489
- useEffect(
490
- () => {
491
- if (state.isOpen) {
492
- return ariaHideOutside(
493
- [inputRef.current, popoverRef.current].filter((element) => element != null),
494
- );
495
- }
496
- },
497
- [state.isOpen, inputRef, popoverRef],
498
- subSlot(slot, 'hideOutside'),
499
- );
476
+ lastSelectedKey.current = state.selectedKey;
477
+ });
500
478
 
501
- useUpdateEffect(
502
- () => {
503
- // Re-show focus ring when there is no virtually focused item.
504
- if (
505
- !focusedItem &&
506
- inputRef.current &&
507
- getActiveElement(getOwnerDocument(inputRef.current)) === inputRef.current
508
- ) {
509
- dispatchVirtualFocus(inputRef.current, null);
510
- }
511
- },
512
- [focusedItem],
513
- subSlot(slot, 'virtualFocus'),
514
- );
479
+ useEffect(() => {
480
+ if (state.isOpen) {
481
+ return ariaHideOutside(
482
+ [inputRef.current, popoverRef.current].filter((element) => element != null),
483
+ );
484
+ }
485
+ }, [state.isOpen, inputRef, popoverRef]);
486
+
487
+ useUpdateEffect(() => {
488
+ // Re-show focus ring when there is no virtually focused item.
489
+ if (
490
+ !focusedItem &&
491
+ inputRef.current &&
492
+ getActiveElement(getOwnerDocument(inputRef.current)) === inputRef.current
493
+ ) {
494
+ dispatchVirtualFocus(inputRef.current, null);
495
+ }
496
+ }, [focusedItem]);
515
497
 
516
498
  useEvent(
517
499
  listBoxRef,
@@ -521,11 +503,20 @@ export function useComboBox(...args: any[]): ComboBoxAria<any> {
521
503
  state.close();
522
504
  }
523
505
  : undefined,
524
- subSlot(slot, 'itemAction'),
525
506
  );
526
507
 
527
508
  return {
528
- labelProps,
509
+ labelProps: {
510
+ ...labelProps,
511
+ // Focus the input in case the label is not a native <label> element.
512
+ onClick:
513
+ ('htmlFor' in labelProps && labelProps.htmlFor) || props.isDisabled
514
+ ? undefined
515
+ : () => {
516
+ inputRef.current?.focus();
517
+ setInteractionModality('keyboard');
518
+ },
519
+ },
529
520
  buttonProps: {
530
521
  ...menuTriggerProps,
531
522
  ...triggerLabelProps,
@@ -535,6 +526,7 @@ export function useComboBox(...args: any[]): ComboBoxAria<any> {
535
526
  onPressStart,
536
527
  isDisabled: isDisabled || isReadOnly,
537
528
  },
529
+ // oxlint-disable-next-line react/react-compiler
538
530
  inputProps: mergeProps(inputProps, {
539
531
  role: 'combobox',
540
532
  'aria-expanded': menuTriggerProps['aria-expanded'],
@@ -573,14 +565,10 @@ export function useComboBox(...args: any[]): ComboBoxAria<any> {
573
565
  // These re-renders preempt async state updates in the useEffect, which ends up running multiple times
574
566
  // prior to the state being updated. This results in onSelectionChange being called multiple times.
575
567
  // TODO: refactor useComboBoxState to avoid this.
576
- // octane adaptation: takes a threaded slot (the module is not octane-compiled).
577
- function useValueId(
578
- depArray: ReadonlyArray<any> = [],
579
- slot?: symbol | undefined,
580
- ): string | undefined {
581
- let id = useId(subSlot(slot, 'id'));
582
- let [exists, setExists] = useState(true, subSlot(slot, 'exists'));
583
- let [lastDeps, setLastDeps] = useState(depArray, subSlot(slot, 'lastDeps'));
568
+ function useValueId(depArray: ReadonlyArray<any> = []): string | undefined {
569
+ let id = useId();
570
+ let [exists, setExists] = useState(true);
571
+ let [lastDeps, setLastDeps] = useState(depArray);
584
572
 
585
573
  // If the deps changed, set exists to true so we can test whether the element exists.
586
574
  if (lastDeps.some((v, i) => !Object.is(v, depArray[i]))) {
@@ -588,15 +576,12 @@ function useValueId(
588
576
  setLastDeps(depArray);
589
577
  }
590
578
 
591
- useEffect(
592
- () => {
593
- if (exists && !document.getElementById(id)) {
594
- setExists(false);
595
- }
596
- },
597
- [id, exists, lastDeps],
598
- subSlot(slot, 'existsEffect'),
599
- );
579
+ useEffect(() => {
580
+ if (exists && !document.getElementById(id)) {
581
+ // oxlint-disable-next-line react/react-compiler
582
+ setExists(false);
583
+ }
584
+ }, [id, exists, lastDeps]);
600
585
 
601
586
  return exists ? id : undefined;
602
587
  }