@octanejs/aria 0.0.46 → 0.0.47

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} +239 -259
  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} +121 -64
  37. package/src/grid/utils.ts +1 -0
  38. package/src/gridlist/{useGridListItem.ts → useGridListItem.tsx} +152 -111
  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 +217 -0
  123. package/src/menu/{useSubmenuTrigger.ts → useSubmenuTrigger.tsx} +112 -152
  124. package/src/numberfield/{useNumberField.ts → useNumberField.tsx} +185 -239
  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} +72 -84
  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} +83 -80
  133. package/src/searchfield/useSearchField.tsx +147 -0
  134. package/src/select/useSelect.ts +20 -20
  135. package/src/selection/useSelectableCollection.tsx +751 -0
  136. package/src/selection/useSelectableItem.ts +48 -11
  137. package/src/slider/{useSliderThumb.ts → useSliderThumb.tsx} +116 -159
  138. package/src/spinbutton/useSpinButton.tsx +330 -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 +354 -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 +3 -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,38 @@ 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';
42
50
  import { getItemId, listData } from '../listbox/utils';
43
51
  import { getOwnerDocument } from '../utils/domHelpers';
44
- import intlMessages from '../intl/combobox';
52
+ import intlMessages from '../intl/combobox/index';
45
53
  import { isAppleDevice } from '../utils/platform';
46
54
  import { ListKeyboardDelegate } from '../selection/ListKeyboardDelegate';
47
55
  import { mergeProps } from '../utils/mergeProps';
48
- import { privateValidationStateProp } from '../stately/form/useFormValidationState';
56
+ import { privateValidationStateProp } from '../upstream-exports/react-stately/private/form/useFormValidationState';
57
+ import { setInteractionModality } from '../interactions/useFocusVisible';
49
58
  import { useEvent } from '../utils/useEvent';
50
59
  import { useFormReset } from '../utils/useFormReset';
51
60
  import { useId } from '../utils/useId';
61
+ import { useKeyboard } from '../interactions/useKeyboard';
52
62
  import { useLabels } from '../utils/useLabels';
53
63
  import { useLocalizedStringFormatter } from '../i18n/useLocalizedStringFormatter';
54
64
  import { useMenuTrigger } from '../menu/useMenuTrigger';
@@ -57,12 +67,6 @@ import { useSelectableCollection } from '../selection/useSelectableCollection';
57
67
  import { useTextField } from '../textfield/useTextField';
58
68
  import { useUpdateEffect } from '../utils/useUpdateEffect';
59
69
 
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
70
  export interface AriaComboBoxProps<T, M extends SelectionMode = 'single'>
67
71
  extends ComboBoxProps<T, M>, DOMProps, InputDOMProps, AriaLabelingProps {
68
72
  /** Whether keyboard navigation is circular. */
@@ -81,6 +85,12 @@ export interface AriaComboBoxOptions<T, M extends SelectionMode = 'single'> exte
81
85
  listBoxRef: RefObject<HTMLElement | null>;
82
86
  /** The ref for the optional list box popup trigger button. */
83
87
  buttonRef?: RefObject<Element | null>;
88
+ /**
89
+ * The HTML element used to render the label, e.g. 'label', or 'span'.
90
+ *
91
+ * @default 'label'
92
+ */
93
+ labelElementType?: ElementType;
84
94
  /** An optional keyboard delegate implementation, to override the default. */
85
95
  keyboardDelegate?: KeyboardDelegate;
86
96
  /**
@@ -95,7 +105,7 @@ export interface ComboBoxAria<T> extends ValidationResult {
95
105
  /** Props for the label element. */
96
106
  labelProps: DOMAttributes;
97
107
  /** Props for the combo box input element. */
98
- inputProps: DOMAttributes;
108
+ inputProps: InputHTMLAttributes<HTMLInputElement>;
99
109
  /** Props for the list box, to be passed to `useListBox`. */
100
110
  listBoxProps: AriaListBoxOptions<T>;
101
111
  /** Props for the optional trigger button, to be passed to `useButton`. */
@@ -119,19 +129,8 @@ export interface ComboBoxAria<T> extends ValidationResult {
119
129
  export function useComboBox<T, M extends SelectionMode = 'single'>(
120
130
  props: AriaComboBoxOptions<T, M>,
121
131
  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>;
134
-
132
+ _slot?: symbol,
133
+ ): ComboBoxAria<T> {
135
134
  let {
136
135
  buttonRef,
137
136
  popoverRef,
@@ -144,22 +143,17 @@ export function useComboBox(...args: any[]): ComboBoxAria<any> {
144
143
  isReadOnly,
145
144
  isDisabled,
146
145
  } = props;
147
- let backupBtnRef = useRef(null, subSlot(slot, 'backupBtn'));
146
+ let backupBtnRef = useRef(null);
148
147
  buttonRef = buttonRef ?? backupBtnRef;
149
148
 
150
- let stringFormatter = useLocalizedStringFormatter(
151
- intlMessages,
152
- '@react-aria/combobox',
153
- subSlot(slot, 'formatter'),
154
- );
155
- let { menuTriggerProps, menuProps } = useMenuTrigger<any>(
149
+ let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-aria/combobox');
150
+ let { menuTriggerProps, menuProps } = useMenuTrigger<T>(
156
151
  {
157
152
  type: 'listbox',
158
153
  isDisabled: isDisabled || isReadOnly,
159
154
  },
160
155
  state,
161
156
  buttonRef,
162
- subSlot(slot, 'menuTrigger'),
163
157
  );
164
158
 
165
159
  // Set listbox id so it can be used when calling getItemId later
@@ -179,41 +173,28 @@ export function useComboBox(...args: any[]): ComboBoxAria<any> {
179
173
  layoutDelegate,
180
174
  }),
181
175
  [keyboardDelegate, layoutDelegate, collection, disabledKeys, listBoxRef],
182
- subSlot(slot, 'delegate'),
183
176
  );
184
177
 
185
178
  // 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
- );
179
+ let { collectionProps } = useSelectableCollection({
180
+ selectionManager: state.selectionManager,
181
+ keyboardDelegate: delegate,
182
+ disallowTypeAhead: true,
183
+ disallowEmptySelection: true,
184
+ shouldFocusWrap,
185
+ ref: inputRef,
186
+ // Prevent item scroll behavior from being applied here, should be handled in the user's Popover + ListBox component
187
+ isVirtualized: true,
188
+ });
199
189
 
200
190
  let router = useRouter();
201
191
 
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
-
192
+ // for textfield specific operations
193
+ let { keyboardProps } = useKeyboard({
194
+ shortcuts: {
195
+ Enter: (e) => {
196
+ // Prevent default form submission if menu is open since we may be selecting a option
197
+ let shouldPreventDefault = state.isOpen;
217
198
  // If the focused item is a link, trigger opening it. Items that are links are not selectable.
218
199
  if (state.isOpen && listBoxRef.current && state.selectionManager.focusedKey != null) {
219
200
  let collectionItem = state.collection.getItem(state.selectionManager.focusedKey);
@@ -221,7 +202,7 @@ export function useComboBox(...args: any[]): ComboBoxAria<any> {
221
202
  let item = listBoxRef.current.querySelector(
222
203
  `[data-key="${CSS.escape(state.selectionManager.focusedKey.toString())}"]`,
223
204
  );
224
- if (e.key === 'Enter' && item instanceof HTMLAnchorElement) {
205
+ if (item instanceof HTMLAnchorElement) {
225
206
  router.open(
226
207
  item,
227
208
  e,
@@ -230,43 +211,70 @@ export function useComboBox(...args: any[]): ComboBoxAria<any> {
230
211
  );
231
212
  }
232
213
  state.close();
233
- break;
214
+ return { shouldPreventDefault };
234
215
  } else if (collectionItem?.props.onAction) {
235
216
  collectionItem.props.onAction();
236
217
  state.close();
237
- break;
218
+ return { shouldPreventDefault };
238
219
  }
239
220
  }
240
- if (e.key === 'Enter' || state.isOpen) {
241
- state.commit();
221
+ state.commit();
222
+ return { shouldPreventDefault };
223
+ },
224
+ Tab: () => {
225
+ // If the focused item is a link, trigger opening it. Items that are links are not selectable.
226
+ if (state.isOpen && listBoxRef.current && state.selectionManager.focusedKey != null) {
227
+ let collectionItem = state.collection.getItem(state.selectionManager.focusedKey);
228
+ if (collectionItem?.props.href) {
229
+ state.close();
230
+ return { shouldPreventDefault: false };
231
+ } else if (collectionItem?.props.onAction) {
232
+ collectionItem.props.onAction();
233
+ state.close();
234
+ return { shouldPreventDefault: false, shouldContinuePropagation: true };
235
+ }
242
236
  }
243
- if (e.key === 'Tab') {
244
- e.continuePropagation();
237
+ if (state.isOpen) {
238
+ state.commit();
245
239
  }
246
-
247
- break;
248
- case 'Escape':
240
+ return { shouldPreventDefault: false, shouldContinuePropagation: true };
241
+ },
242
+ Escape: () => {
243
+ let shouldContinuePropagation = false;
249
244
  if (!state.selectionManager.isEmpty || state.inputValue === '' || props.allowsCustomValue) {
250
- e.continuePropagation();
245
+ shouldContinuePropagation = true;
251
246
  }
252
247
  state.revert();
253
- break;
254
- case 'ArrowDown':
248
+ return { shouldContinuePropagation };
249
+ },
250
+ },
251
+ });
252
+
253
+ let { keyboardProps: repeatKeyboardProps } = useKeyboard({
254
+ shortcuts: {
255
+ ArrowDown: () => {
255
256
  state.open('first', 'manual');
256
- break;
257
- case 'ArrowUp':
257
+ return { shouldPreventDefault: false };
258
+ },
259
+ ArrowUp: () => {
258
260
  state.open('last', 'manual');
259
- break;
260
- case 'ArrowLeft':
261
- case 'ArrowRight':
261
+ return { shouldPreventDefault: false };
262
+ },
263
+ ArrowLeft: () => {
262
264
  state.selectionManager.setFocusedKey(null);
263
- break;
264
- }
265
- };
265
+ return { shouldPreventDefault: false };
266
+ },
267
+ ArrowRight: () => {
268
+ state.selectionManager.setFocusedKey(null);
269
+ return { shouldPreventDefault: false };
270
+ },
271
+ },
272
+ allowRepeats: true,
273
+ });
266
274
 
267
- let onBlur = (e: any) => {
275
+ let onBlur = (e: FocusEvent) => {
268
276
  let blurFromButton = buttonRef?.current && buttonRef.current === e.relatedTarget;
269
- let blurIntoPopover = nodeContains(popoverRef.current, e.relatedTarget);
277
+ let blurIntoPopover = nodeContains(popoverRef.current, e.relatedTarget as Node | null);
270
278
  // Ignore blur if focused moved to the button(if exists) or into the popover.
271
279
  if (blurFromButton || blurIntoPopover) {
272
280
  return;
@@ -279,7 +287,7 @@ export function useComboBox(...args: any[]): ComboBoxAria<any> {
279
287
  state.setFocused(false);
280
288
  };
281
289
 
282
- let onFocus = (e: any) => {
290
+ let onFocus = (e: FocusEvent) => {
283
291
  if (state.isFocused) {
284
292
  return;
285
293
  }
@@ -291,10 +299,10 @@ export function useComboBox(...args: any[]): ComboBoxAria<any> {
291
299
  state.setFocused(true);
292
300
  };
293
301
 
294
- let valueId = useValueId(
295
- [state.selectionManager.selectedKeys, state.selectionManager.selectionMode],
296
- subSlot(slot, 'valueId'),
297
- );
302
+ let valueId = useValueId([
303
+ state.selectionManager.selectedKeys,
304
+ state.selectionManager.selectionMode,
305
+ ]);
298
306
  let { isInvalid, validationErrors, validationDetails } = state.displayValidation;
299
307
  let { labelProps, inputProps, descriptionProps, errorMessageProps } = useTextField(
300
308
  {
@@ -306,7 +314,13 @@ export function useComboBox(...args: any[]): ComboBoxAria<any> {
306
314
  : props.isRequired,
307
315
  onChange: state.setInputValue,
308
316
  onKeyDown: !isReadOnly
309
- ? chain(state.isOpen && collectionProps.onKeyDown, onKeyDown, props.onKeyDown)
317
+ ? // oxlint-disable-next-line react/react-compiler
318
+ chain(
319
+ state.isOpen && collectionProps.onKeyDown,
320
+ keyboardProps.onKeyDown,
321
+ repeatKeyboardProps.onKeyDown,
322
+ props.onKeyDown,
323
+ )
310
324
  : props.onKeyDown,
311
325
  onBlur,
312
326
  value: state.inputValue,
@@ -319,10 +333,9 @@ export function useComboBox(...args: any[]): ComboBoxAria<any> {
319
333
  [valueId, props['aria-describedby']].filter(Boolean).join(' ') || undefined,
320
334
  },
321
335
  inputRef,
322
- subSlot(slot, 'textField'),
323
336
  );
324
337
 
325
- useFormReset(inputRef, state.defaultValue, state.setValue, subSlot(slot, 'formReset'));
338
+ useFormReset(inputRef, state.defaultValue, state.setValue);
326
339
 
327
340
  // Press handlers for the ComboBox button
328
341
  let onPress = (e: PressEvent) => {
@@ -343,29 +356,21 @@ export function useComboBox(...args: any[]): ComboBoxAria<any> {
343
356
  }
344
357
  };
345
358
 
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
- );
359
+ let triggerLabelProps = useLabels({
360
+ id: menuTriggerProps.id,
361
+ 'aria-label': stringFormatter.format('buttonLabel'),
362
+ 'aria-labelledby': props['aria-labelledby'] || labelProps.id,
363
+ });
355
364
 
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
- );
365
+ let listBoxProps = useLabels({
366
+ id: menuProps.id,
367
+ 'aria-label': stringFormatter.format('listboxLabel'),
368
+ 'aria-labelledby': props['aria-labelledby'] || labelProps.id,
369
+ });
365
370
 
366
371
  // 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) => {
372
+ let lastEventTime = useRef(0);
373
+ let onTouchEnd = (e: TouchEvent) => {
369
374
  if (isDisabled || isReadOnly) {
370
375
  return;
371
376
  }
@@ -401,117 +406,89 @@ export function useComboBox(...args: any[]): ComboBoxAria<any> {
401
406
  : undefined;
402
407
  let sectionKey = focusedItem?.parentKey ?? null;
403
408
  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
- );
409
+ let lastSection = useRef<typeof sectionKey>(sectionKey);
410
+ let lastItem = useRef<typeof itemKey>(itemKey);
411
+ useEffect(() => {
412
+ if (isAppleDevice() && focusedItem != null && itemKey != null && itemKey !== lastItem.current) {
413
+ let isSelected = state.selectionManager.isSelected(itemKey);
414
+ let section = sectionKey != null ? state.collection.getItem(sectionKey) : null;
415
+ let sectionTitle =
416
+ section?.['aria-label'] ||
417
+ (typeof section?.rendered === 'string' ? section.rendered : '') ||
418
+ '';
419
+
420
+ let announcement = stringFormatter.format('focusAnnouncement', {
421
+ isGroupChange: (section && sectionKey !== lastSection.current) ?? false,
422
+ groupTitle: sectionTitle,
423
+ groupCount: section ? [...getChildNodes(section, state.collection)].length : 0,
424
+ optionText: focusedItem['aria-label'] || focusedItem.textValue || '',
425
+ isSelected,
426
+ });
427
+
428
+ announce(announcement);
429
+ }
430
+
431
+ lastSection.current = sectionKey;
432
+ lastItem.current = itemKey;
433
+ });
439
434
 
440
435
  // Announce the number of available suggestions when it changes
441
436
  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
- );
437
+ let lastSize = useRef(optionCount);
438
+ let lastOpen = useRef(state.isOpen);
439
+ useEffect(() => {
440
+ // Only announce the number of options available when the menu opens if there is no
441
+ // focused item, otherwise screen readers will typically read e.g. "1 of 6".
442
+ // The exception is VoiceOver since this isn't included in the message above.
443
+ let didOpenWithoutFocusedItem =
444
+ state.isOpen !== lastOpen.current &&
445
+ (state.selectionManager.focusedKey == null || isAppleDevice());
446
+
447
+ if (state.isOpen && (didOpenWithoutFocusedItem || optionCount !== lastSize.current)) {
448
+ let announcement = stringFormatter.format('countAnnouncement', { optionCount });
449
+ announce(announcement);
450
+ }
451
+
452
+ lastSize.current = optionCount;
453
+ lastOpen.current = state.isOpen;
454
+ });
465
455
 
466
456
  // Announce when a selection occurs for VoiceOver. Other screen readers typically do this automatically.
467
457
  // 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
- );
458
+ let lastSelectedKey = useRef<typeof state.selectedKey>(state.selectedKey);
459
+ useEffect(() => {
460
+ if (
461
+ isAppleDevice() &&
462
+ state.isFocused &&
463
+ state.selectedItem &&
464
+ state.selectedKey !== lastSelectedKey.current
465
+ ) {
466
+ let optionText = state.selectedItem['aria-label'] || state.selectedItem.textValue || '';
467
+ let announcement = stringFormatter.format('selectedAnnouncement', { optionText });
468
+ announce(announcement);
469
+ }
488
470
 
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
- );
471
+ lastSelectedKey.current = state.selectedKey;
472
+ });
500
473
 
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
- );
474
+ useEffect(() => {
475
+ if (state.isOpen) {
476
+ return ariaHideOutside(
477
+ [inputRef.current, popoverRef.current].filter((element) => element != null),
478
+ );
479
+ }
480
+ }, [state.isOpen, inputRef, popoverRef]);
481
+
482
+ useUpdateEffect(() => {
483
+ // Re-show focus ring when there is no virtually focused item.
484
+ if (
485
+ !focusedItem &&
486
+ inputRef.current &&
487
+ getActiveElement(getOwnerDocument(inputRef.current)) === inputRef.current
488
+ ) {
489
+ dispatchVirtualFocus(inputRef.current, null);
490
+ }
491
+ }, [focusedItem]);
515
492
 
516
493
  useEvent(
517
494
  listBoxRef,
@@ -521,11 +498,20 @@ export function useComboBox(...args: any[]): ComboBoxAria<any> {
521
498
  state.close();
522
499
  }
523
500
  : undefined,
524
- subSlot(slot, 'itemAction'),
525
501
  );
526
502
 
527
503
  return {
528
- labelProps,
504
+ labelProps: {
505
+ ...labelProps,
506
+ // Focus the input in case the label is not a native <label> element.
507
+ onClick:
508
+ ('htmlFor' in labelProps && labelProps.htmlFor) || props.isDisabled
509
+ ? undefined
510
+ : () => {
511
+ inputRef.current?.focus();
512
+ setInteractionModality('keyboard');
513
+ },
514
+ },
529
515
  buttonProps: {
530
516
  ...menuTriggerProps,
531
517
  ...triggerLabelProps,
@@ -535,6 +521,7 @@ export function useComboBox(...args: any[]): ComboBoxAria<any> {
535
521
  onPressStart,
536
522
  isDisabled: isDisabled || isReadOnly,
537
523
  },
524
+ // oxlint-disable-next-line react/react-compiler
538
525
  inputProps: mergeProps(inputProps, {
539
526
  role: 'combobox',
540
527
  'aria-expanded': menuTriggerProps['aria-expanded'],
@@ -573,14 +560,10 @@ export function useComboBox(...args: any[]): ComboBoxAria<any> {
573
560
  // These re-renders preempt async state updates in the useEffect, which ends up running multiple times
574
561
  // prior to the state being updated. This results in onSelectionChange being called multiple times.
575
562
  // 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'));
563
+ function useValueId(depArray: ReadonlyArray<any> = []): string | undefined {
564
+ let id = useId();
565
+ let [exists, setExists] = useState(true);
566
+ let [lastDeps, setLastDeps] = useState(depArray);
584
567
 
585
568
  // If the deps changed, set exists to true so we can test whether the element exists.
586
569
  if (lastDeps.some((v, i) => !Object.is(v, depArray[i]))) {
@@ -588,15 +571,12 @@ function useValueId(
588
571
  setLastDeps(depArray);
589
572
  }
590
573
 
591
- useEffect(
592
- () => {
593
- if (exists && !document.getElementById(id)) {
594
- setExists(false);
595
- }
596
- },
597
- [id, exists, lastDeps],
598
- subSlot(slot, 'existsEffect'),
599
- );
574
+ useEffect(() => {
575
+ if (exists && !document.getElementById(id)) {
576
+ // oxlint-disable-next-line react/react-compiler
577
+ setExists(false);
578
+ }
579
+ }, [id, exists, lastDeps]);
600
580
 
601
581
  return exists ? id : undefined;
602
582
  }