@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,20 +1,26 @@
1
- // Ported from react-aria (source: .react-spectrum/packages/react-aria/src/overlays/useOverlay.ts).
2
- // octane adaptations: `DOMAttributes` is a local structural prop-bag alias (upstream's is typed
3
- // over React's synthetic handlers); handlers receive NATIVE DOM events (no synthetic layer), so
4
- // `e.nativeEvent.isComposing` → `e.isComposing`; public-hook slot threading (splitSlot/subSlot)
5
- // per the binding convention; the explicit `[isOpen, ref]` dependency array is kept verbatim.
6
- import type { RefObject } from '@react-types/shared';
1
+ /** @jsxImportSource octane */
2
+ type DOMAttributes = Record<string, any>;
3
+ // Ported from adobe/react-spectrum@5ecb3333001313e83898cd07644227897e3bae1f (packages/react-aria/src/overlays/useOverlay.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 { RefObject } from '@react-types/shared';
17
+ import { withSlot } from 'octane';
7
18
  import { getEventTarget } from '../utils/shadowdom/DOMFunctions';
8
19
  import { isElementInChildOfActiveScope } from '../focus/FocusScope';
9
- import { useEffect, useRef } from 'octane';
20
+ import { useEffect, useRef } from '../compat/react';
10
21
  import { useFocusWithin } from '../interactions/useFocusWithin';
11
22
  import { useInteractOutside } from '../interactions/useInteractOutside';
12
-
13
- import { S, splitSlot, subSlot } from '../internal';
14
-
15
- // octane adaptation: minimal structural DOMAttributes (upstream's drags React's synthetic
16
- // handler types along).
17
- type DOMAttributes = Record<string, any>;
23
+ import { useKeyboard } from '../interactions/useKeyboard';
18
24
 
19
25
  export interface AriaOverlayProps {
20
26
  /** Whether the overlay is currently open. */
@@ -63,18 +69,14 @@ const visibleOverlays: RefObject<Element | null>[] = [];
63
69
  * Hides the overlay when the user interacts outside it, when the Escape key is pressed,
64
70
  * or optionally, on blur. Only the top-most overlay will close at once.
65
71
  */
66
- export function useOverlay(props: AriaOverlayProps, ref: RefObject<Element | null>): OverlayAria;
67
- // Slot-threading form: sibling ported hooks pass their derived sub-slot as the trailing arg.
68
72
  export function useOverlay(
69
73
  props: AriaOverlayProps,
70
74
  ref: RefObject<Element | null>,
71
- slot: symbol | undefined,
72
- ): OverlayAria;
73
- export function useOverlay(...args: any[]): OverlayAria {
74
- const [user, slotArg] = splitSlot(args);
75
- const slot = slotArg ?? S('useOverlay');
76
- const props = user[0] as AriaOverlayProps;
77
- const ref = user[1] as RefObject<Element | null>;
75
+ _slot?: symbol,
76
+ ): OverlayAria {
77
+ if (_slot !== undefined) {
78
+ return withSlot(_slot, useOverlay, props, ref);
79
+ }
78
80
 
79
81
  let {
80
82
  onClose,
@@ -85,27 +87,20 @@ export function useOverlay(...args: any[]): OverlayAria {
85
87
  shouldCloseOnInteractOutside,
86
88
  } = props;
87
89
 
88
- let lastVisibleOverlay = useRef<RefObject<Element | null> | undefined>(
89
- undefined,
90
- subSlot(slot, 'lastVisible'),
91
- );
90
+ let lastVisibleOverlay = useRef<RefObject<Element | null> | undefined>(undefined);
92
91
 
93
92
  // Add the overlay ref to the stack of visible overlays on mount, and remove on unmount.
94
- useEffect(
95
- () => {
96
- if (isOpen && !visibleOverlays.includes(ref)) {
97
- visibleOverlays.push(ref);
98
- return () => {
99
- let index = visibleOverlays.indexOf(ref);
100
- if (index >= 0) {
101
- visibleOverlays.splice(index, 1);
102
- }
103
- };
104
- }
105
- },
106
- [isOpen, ref],
107
- subSlot(slot, 'stack'),
108
- );
93
+ useEffect(() => {
94
+ if (isOpen && !visibleOverlays.includes(ref)) {
95
+ visibleOverlays.push(ref);
96
+ return () => {
97
+ let index = visibleOverlays.indexOf(ref);
98
+ if (index >= 0) {
99
+ visibleOverlays.splice(index, 1);
100
+ }
101
+ };
102
+ }
103
+ }, [isOpen, ref]);
109
104
 
110
105
  // Only hide the overlay when it is the topmost visible overlay in the stack
111
106
  let onHide = () => {
@@ -143,55 +138,53 @@ export function useOverlay(...args: any[]): OverlayAria {
143
138
  };
144
139
 
145
140
  // Handle the escape key
146
- let onKeyDown = (e: KeyboardEvent) => {
147
- if (e.key === 'Escape' && !isKeyboardDismissDisabled && !e.isComposing) {
148
- e.stopPropagation();
149
- e.preventDefault();
150
- onHide();
151
- }
152
- };
153
-
154
- // Handle clicking outside the overlay to close it
155
- useInteractOutside(
156
- {
157
- ref,
158
- onInteractOutside: isDismissable && isOpen ? onInteractOutside : undefined,
159
- onInteractOutsideStart,
160
- },
161
- subSlot(slot, 'interactOutside'),
162
- );
163
-
164
- let { focusWithinProps } = useFocusWithin(
165
- {
166
- isDisabled: !shouldCloseOnBlur,
167
- onBlurWithin: (e) => {
168
- // Do not close if relatedTarget is null, which means focus is lost to the body.
169
- // That can happen when switching tabs, or due to a VoiceOver/Chrome bug with Control+Option+Arrow navigation.
170
- // Clicking on the body to close the overlay should already be handled by useInteractOutside.
171
- // https://github.com/adobe/react-spectrum/issues/4130
172
- // https://github.com/adobe/react-spectrum/issues/4922
173
- //
174
- // If focus is moving into a child focus scope (e.g. menu inside a dialog),
175
- // do not close the outer overlay. At this point, the active scope should
176
- // still be the outer overlay, since blur events run before focus.
177
- if (!e.relatedTarget || isElementInChildOfActiveScope(e.relatedTarget as Element)) {
141
+ let { keyboardProps } = useKeyboard({
142
+ shortcuts: {
143
+ Escape: () => {
144
+ if (!isKeyboardDismissDisabled) {
145
+ onHide();
178
146
  return;
179
147
  }
180
-
181
- if (
182
- !shouldCloseOnInteractOutside ||
183
- shouldCloseOnInteractOutside(e.relatedTarget as Element)
184
- ) {
185
- onClose?.();
186
- }
148
+ return false;
187
149
  },
188
150
  },
189
- subSlot(slot, 'focusWithin'),
190
- );
151
+ });
152
+
153
+ // Handle clicking outside the overlay to close it
154
+ useInteractOutside({
155
+ ref,
156
+ onInteractOutside: isDismissable && isOpen ? onInteractOutside : undefined,
157
+ onInteractOutsideStart,
158
+ });
159
+
160
+ let { focusWithinProps } = useFocusWithin({
161
+ isDisabled: !shouldCloseOnBlur,
162
+ onBlurWithin: (e) => {
163
+ // Do not close if relatedTarget is null, which means focus is lost to the body.
164
+ // That can happen when switching tabs, or due to a VoiceOver/Chrome bug with Control+Option+Arrow navigation.
165
+ // Clicking on the body to close the overlay should already be handled by useInteractOutside.
166
+ // https://github.com/adobe/react-spectrum/issues/4130
167
+ // https://github.com/adobe/react-spectrum/issues/4922
168
+ //
169
+ // If focus is moving into a child focus scope (e.g. menu inside a dialog),
170
+ // do not close the outer overlay. At this point, the active scope should
171
+ // still be the outer overlay, since blur events run before focus.
172
+ if (!e.relatedTarget || isElementInChildOfActiveScope(e.relatedTarget as Element)) {
173
+ return;
174
+ }
175
+
176
+ if (
177
+ !shouldCloseOnInteractOutside ||
178
+ shouldCloseOnInteractOutside(e.relatedTarget as Element)
179
+ ) {
180
+ onClose?.();
181
+ }
182
+ },
183
+ });
191
184
 
192
185
  return {
193
186
  overlayProps: {
194
- onKeyDown,
187
+ ...keyboardProps,
195
188
  ...focusWithinProps,
196
189
  },
197
190
  underlayProps: {},
@@ -7,6 +7,7 @@
7
7
  import { ariaHideOutside, keepVisible } from './ariaHideOutside';
8
8
  import { AriaPositionProps, PlacementAxis, useOverlayPosition } from './useOverlayPosition';
9
9
  import type { RefObject } from '@react-types/shared';
10
+ import { FocusWithinProps, useFocusWithin } from '../interactions/useFocusWithin';
10
11
  import { mergeProps } from '../utils/mergeProps';
11
12
  import type { OverlayTriggerState } from '../stately/overlays/useOverlayTriggerState';
12
13
  import { useEffect } from 'octane';
@@ -19,10 +20,10 @@ import { S, splitSlot, subSlot } from '../internal';
19
20
  // handler types along).
20
21
  type DOMAttributes = Record<string, any>;
21
22
 
22
- export interface AriaPopoverProps extends Omit<
23
- AriaPositionProps,
24
- 'isOpen' | 'onClose' | 'targetRef' | 'overlayRef'
25
- > {
23
+ export interface AriaPopoverProps
24
+ extends
25
+ Omit<AriaPositionProps, 'isOpen' | 'onClose' | 'targetRef' | 'overlayRef'>,
26
+ Omit<FocusWithinProps, 'isDisabled'> {
26
27
  /**
27
28
  * The ref for the element which the popover positions itself with respect to.
28
29
  */
@@ -133,6 +134,9 @@ export function usePopover(...args: any[]): PopoverAria {
133
134
  overlayRef: popoverRef,
134
135
  isOpen: state.isOpen,
135
136
  onClose: isNonModal && !isSubmenu ? state.close : null,
137
+ getTargetRect:
138
+ otherProps.getTargetRect ??
139
+ (state.point ? () => new DOMRect(state.point!.x, state.point!.y, 0, 0) : undefined),
136
140
  },
137
141
  subSlot(slot, 'position'),
138
142
  );
@@ -160,8 +164,9 @@ export function usePopover(...args: any[]): PopoverAria {
160
164
  subSlot(slot, 'hideOutside'),
161
165
  );
162
166
 
167
+ let { focusWithinProps } = useFocusWithin(props, subSlot(slot, 'focusWithin'));
163
168
  return {
164
- popoverProps: mergeProps(overlayProps, positionProps),
169
+ popoverProps: mergeProps(overlayProps, positionProps, focusWithinProps),
165
170
  arrowProps,
166
171
  underlayProps,
167
172
  placement,
@@ -8,7 +8,7 @@ import { chain } from '../utils/chain';
8
8
  import { getActiveElement, getEventTarget } from '../utils/shadowdom/DOMFunctions';
9
9
  import { getNonce } from '../utils/getNonce';
10
10
  import { getScrollParent } from '../utils/getScrollParent';
11
- import { isIOS } from '../utils/platform';
11
+ import { isIOS, isWebKit } from '../utils/platform';
12
12
  import { isScrollable } from '../utils/isScrollable';
13
13
  import { useLayoutEffect } from '../utils/useLayoutEffect';
14
14
  import { willOpenKeyboard } from '../utils/keyboard';
@@ -52,8 +52,8 @@ export function usePreventScroll(...args: any[]): void {
52
52
 
53
53
  preventScrollCount++;
54
54
  if (preventScrollCount === 1) {
55
- if (isIOS()) {
56
- restore = preventScrollMobileSafari();
55
+ if (isIOS() && isWebKit()) {
56
+ restore = preventScrollMobileWebKit();
57
57
  } else {
58
58
  restore = preventScrollStandard();
59
59
  }
@@ -108,7 +108,7 @@ function preventScrollStandard() {
108
108
  // 4. When focus moves to an input, create an off screen input and focus that temporarily. This prevents
109
109
  // Safari from scrolling the page. After a small delay, focus the real input and scroll it into view
110
110
  // ourselves, without scrolling the whole page.
111
- function preventScrollMobileSafari() {
111
+ function preventScrollMobileWebKit() {
112
112
  // Set overflow hidden so scrollIntoViewport() (useSelectableCollection) sees isScrollPrevented and
113
113
  // scrolls only scroll parents instead of calling native scrollIntoView() which moves the window.
114
114
  let restoreOverflow = setStyle(document.documentElement, 'overflow', 'hidden');
@@ -93,7 +93,8 @@ export function useProgressBar(...args: any[]): ProgressBarAria {
93
93
  );
94
94
 
95
95
  value = clamp(value, minValue, maxValue);
96
- let percentage = (value - minValue) / (maxValue - minValue);
96
+ let range = maxValue - minValue;
97
+ let percentage = range === 0 ? 0 : (value - minValue) / range;
97
98
  let formatter = useNumberFormatter(formatOptions, subSlot(slot, 'formatter'));
98
99
 
99
100
  if (!isIndeterminate && !valueLabel) {
@@ -1,29 +1,40 @@
1
- // Ported from react-aria (source: .react-spectrum/packages/react-aria/src/radio/useRadioGroup.ts).
2
- // octane adaptations: the keyboard/focus handlers receive native events; public-hook
3
- // slot threading.
4
- import type {
1
+ /** @jsxImportSource octane */
2
+ type DOMAttributes = Record<string, any>;
3
+ // Ported from adobe/react-spectrum@5ecb3333001313e83898cd07644227897e3bae1f (packages/react-aria/src/radio/useRadioGroup.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 {
5
17
  AriaLabelingProps,
6
18
  AriaValidationProps,
7
19
  DOMProps,
8
20
  InputDOMProps,
9
21
  ValidationResult,
10
22
  } from '@react-types/shared';
11
-
12
- // octane adaptation: structural prop bag (upstream's DOMAttributes drags React handler
13
- // types; the keyboard handler here is a native KeyboardEvent handler).
14
- type DOMAttributes = Record<string, any>;
15
-
16
- import { S, splitSlot, subSlot } from '../internal';
23
+ import { withSlot } from 'octane';
17
24
  import { filterDOMProps } from '../utils/filterDOMProps';
18
25
  import { getEventTarget } from '../utils/shadowdom/DOMFunctions';
19
26
  import { getFocusableTreeWalker } from '../focus/FocusScope';
20
27
  import { getOwnerWindow } from '../utils/domHelpers';
21
28
  import { mergeProps } from '../utils/mergeProps';
22
29
  import { radioGroupData } from './utils';
23
- import type { RadioGroupProps, RadioGroupState } from '../stately/radio/useRadioGroupState';
30
+ import {
31
+ RadioGroupProps,
32
+ RadioGroupState,
33
+ } from '../upstream-exports/react-stately/useRadioGroupState';
24
34
  import { useField } from '../label/useField';
25
35
  import { useFocusWithin } from '../interactions/useFocusWithin';
26
36
  import { useId } from '../utils/useId';
37
+ import { useKeyboard } from '../interactions/useKeyboard';
27
38
  import { useLocale } from '../i18n/I18nProvider';
28
39
 
29
40
  export interface AriaRadioGroupProps
@@ -40,18 +51,21 @@ export interface RadioGroupAria extends ValidationResult {
40
51
  errorMessageProps: DOMAttributes;
41
52
  }
42
53
 
43
- export function useRadioGroup(props: AriaRadioGroupProps, state: RadioGroupState): RadioGroupAria;
44
- // Slot-threading form: sibling ported hooks pass their derived sub-slot as the trailing arg.
54
+ /**
55
+ * Provides the behavior and accessibility implementation for a radio group component.
56
+ * Radio groups allow users to select a single item from a list of mutually exclusive options.
57
+ *
58
+ * @param props - Props for the radio group.
59
+ * @param state - State for the radio group, as returned by `useRadioGroupState`.
60
+ */
45
61
  export function useRadioGroup(
46
62
  props: AriaRadioGroupProps,
47
63
  state: RadioGroupState,
48
- slot: symbol | undefined,
49
- ): RadioGroupAria;
50
- export function useRadioGroup(...args: any[]): RadioGroupAria {
51
- const [user, slotArg] = splitSlot(args);
52
- const slot = slotArg ?? S('useRadioGroup');
53
- const props = user[0] as AriaRadioGroupProps;
54
- const state = user[1] as RadioGroupState;
64
+ _slot?: symbol,
65
+ ): RadioGroupAria {
66
+ if (_slot !== undefined) {
67
+ return withSlot(_slot, useRadioGroup, props, state);
68
+ }
55
69
 
56
70
  let {
57
71
  name,
@@ -62,72 +76,42 @@ export function useRadioGroup(...args: any[]): RadioGroupAria {
62
76
  orientation = 'vertical',
63
77
  validationBehavior = 'aria',
64
78
  } = props;
65
- let { direction } = useLocale(subSlot(slot, 'locale'));
79
+ let { direction } = useLocale();
66
80
 
67
81
  let { isInvalid, validationErrors, validationDetails } = state.displayValidation;
68
- let { labelProps, fieldProps, descriptionProps, errorMessageProps } = useField(
69
- {
70
- ...props,
71
- // Radio group is not an HTML input element so it
72
- // shouldn't be labeled by a <label> element.
73
- labelElementType: 'span',
74
- isInvalid: state.isInvalid,
75
- errorMessage: props.errorMessage || validationErrors,
76
- },
77
- subSlot(slot, 'field'),
78
- );
82
+ let { labelProps, fieldProps, descriptionProps, errorMessageProps } = useField({
83
+ ...props,
84
+ // Radio group is not an HTML input element so it
85
+ // shouldn't be labeled by a <label> element.
86
+ labelElementType: 'span',
87
+ isInvalid: state.isInvalid,
88
+ errorMessage: props.errorMessage || validationErrors,
89
+ });
79
90
 
80
91
  let domProps = filterDOMProps(props, { labelable: true });
81
92
 
82
93
  // When the radio group loses focus, reset the focusable radio to null if
83
94
  // there is no selection. This allows tabbing into the group from either
84
95
  // direction to go to the first or last radio.
85
- let { focusWithinProps } = useFocusWithin(
86
- {
87
- onBlurWithin(e: FocusEvent) {
88
- (props as any).onBlur?.(e);
89
- if (!state.selectedValue) {
90
- state.setLastFocusedValue(null);
91
- }
92
- },
93
- onFocusWithin: (props as any).onFocus,
94
- onFocusWithinChange: (props as any).onFocusChange,
96
+ let { focusWithinProps } = useFocusWithin({
97
+ onBlurWithin(e) {
98
+ props.onBlur?.(e);
99
+ if (!state.selectedValue) {
100
+ state.setLastFocusedValue(null);
101
+ }
95
102
  },
96
- subSlot(slot, 'focusWithin'),
97
- );
103
+ onFocusWithin: props.onFocus,
104
+ onFocusWithinChange: props.onFocusChange,
105
+ });
98
106
 
99
- let onKeyDown = (e: KeyboardEvent) => {
100
- let nextDir;
101
- switch (e.key) {
102
- case 'ArrowRight':
103
- if (direction === 'rtl' && orientation !== 'vertical') {
104
- nextDir = 'prev';
105
- } else {
106
- nextDir = 'next';
107
- }
108
- break;
109
- case 'ArrowLeft':
110
- if (direction === 'rtl' && orientation !== 'vertical') {
111
- nextDir = 'next';
112
- } else {
113
- nextDir = 'prev';
114
- }
115
- break;
116
- case 'ArrowDown':
117
- nextDir = 'next';
118
- break;
119
- case 'ArrowUp':
120
- nextDir = 'prev';
121
- break;
122
- default:
123
- return;
124
- }
125
- e.preventDefault();
107
+ function getNextElement(
108
+ nextDir: 'next' | 'prev',
109
+ e: import('../interactions/createEventHandler').BaseEvent<KeyboardEvent>,
110
+ ) {
126
111
  let walker = getFocusableTreeWalker(e.currentTarget as Element, {
127
112
  from: getEventTarget(e) as Element,
128
- accept: (node: Element) =>
129
- node instanceof getOwnerWindow(node).HTMLInputElement &&
130
- (node as HTMLInputElement).type === 'radio',
113
+ accept: (node) =>
114
+ node instanceof getOwnerWindow(node).HTMLInputElement && node.type === 'radio',
131
115
  });
132
116
  let nextElem;
133
117
  if (nextDir === 'next') {
@@ -148,15 +132,39 @@ export function useRadioGroup(...args: any[]): RadioGroupAria {
148
132
  // Call focus on nextElem so that keyboard navigation scrolls the radio into view
149
133
  (nextElem as HTMLInputElement).focus();
150
134
  state.setSelectedValue((nextElem as HTMLInputElement).value);
135
+ return true;
151
136
  }
152
- };
137
+ return false;
138
+ }
139
+
140
+ let { keyboardProps } = useKeyboard({
141
+ shortcuts: {
142
+ ArrowRight: (e) => {
143
+ let nextDir: 'next' | 'prev' =
144
+ direction === 'rtl' && orientation !== 'vertical' ? 'prev' : 'next';
145
+ return getNextElement(nextDir, e);
146
+ },
147
+ ArrowLeft: (e) => {
148
+ let nextDir: 'next' | 'prev' =
149
+ direction === 'rtl' && orientation !== 'vertical' ? 'next' : 'prev';
150
+ return getNextElement(nextDir, e);
151
+ },
152
+ ArrowDown: (e) => {
153
+ return getNextElement('next', e);
154
+ },
155
+ ArrowUp: (e) => {
156
+ return getNextElement('prev', e);
157
+ },
158
+ },
159
+ allowRepeats: true,
160
+ });
153
161
 
154
- let groupName = useId(name, subSlot(slot, 'name'));
162
+ let groupName = useId(name);
155
163
  radioGroupData.set(state, {
156
164
  name: groupName,
157
165
  form,
158
- descriptionId: (descriptionProps as any).id,
159
- errorMessageId: (errorMessageProps as any).id,
166
+ descriptionId: descriptionProps.id,
167
+ errorMessageId: errorMessageProps.id,
160
168
  validationBehavior,
161
169
  });
162
170
 
@@ -164,7 +172,7 @@ export function useRadioGroup(...args: any[]): RadioGroupAria {
164
172
  radioGroupProps: mergeProps(domProps, {
165
173
  // https://www.w3.org/TR/wai-aria-1.2/#radiogroup
166
174
  role: 'radiogroup',
167
- onKeyDown,
175
+ ...keyboardProps,
168
176
  'aria-invalid': state.isInvalid || undefined,
169
177
  'aria-errormessage': props['aria-errormessage'],
170
178
  'aria-readonly': isReadOnly || undefined,
@@ -0,0 +1,152 @@
1
+ /** @jsxImportSource octane */
2
+ type DOMAttributes = Record<string, any>;
3
+ // Ported from adobe/react-spectrum@5ecb3333001313e83898cd07644227897e3bae1f (packages/react-aria/src/searchfield/useSearchField.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 { AriaButtonProps } from '../button/useButton';
17
+ import { withSlot } from 'octane';
18
+ import { AriaTextFieldProps, useTextField } from '../textfield/useTextField';
19
+ import { RefObject, ValidationResult } from '@react-types/shared';
20
+ import { InputHTMLAttributes, LabelHTMLAttributes } from '../compat/react';
21
+ import intlMessages from '../intl/searchfield/index';
22
+ // @ts-ignore
23
+ import { mergeProps } from '../utils/mergeProps';
24
+ import {
25
+ SearchFieldProps,
26
+ SearchFieldState,
27
+ } from '../upstream-exports/react-stately/useSearchFieldState';
28
+ import { useKeyboard } from '../interactions/useKeyboard';
29
+ import { useLocalizedStringFormatter } from '../i18n/useLocalizedStringFormatter';
30
+
31
+ export interface AriaSearchFieldProps extends SearchFieldProps, Omit<AriaTextFieldProps, 'type'> {
32
+ /**
33
+ * An enumerated attribute that defines what action label or icon to preset for the enter key on
34
+ * virtual keyboards. See
35
+ * [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/enterkeyhint).
36
+ */
37
+ enterKeyHint?: 'enter' | 'done' | 'go' | 'next' | 'previous' | 'search' | 'send';
38
+ /**
39
+ * The type of input to render. See
40
+ * [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#htmlattrdeftype).
41
+ *
42
+ * @default 'search'
43
+ */
44
+ type?: 'text' | 'search' | 'url' | 'tel' | 'email' | 'password' | (string & {});
45
+ }
46
+
47
+ export interface SearchFieldAria extends ValidationResult {
48
+ /** Props for the text field's visible label element (if any). */
49
+ labelProps: LabelHTMLAttributes<HTMLLabelElement>;
50
+ /** Props for the input element. */
51
+ inputProps: InputHTMLAttributes<HTMLInputElement>;
52
+ /** Props for the clear button. */
53
+ clearButtonProps: AriaButtonProps;
54
+ /** Props for the searchfield's description element, if any. */
55
+ descriptionProps: DOMAttributes;
56
+ /** Props for the searchfield's error message element, if any. */
57
+ errorMessageProps: DOMAttributes;
58
+ }
59
+
60
+ /**
61
+ * Provides the behavior and accessibility implementation for a search field.
62
+ *
63
+ * @param props - Props for the search field.
64
+ * @param state - State for the search field, as returned by `useSearchFieldState`.
65
+ * @param inputRef - A ref to the input element.
66
+ */
67
+ export function useSearchField(
68
+ props: AriaSearchFieldProps,
69
+ state: SearchFieldState,
70
+ inputRef: RefObject<HTMLInputElement | null>,
71
+ _slot?: symbol,
72
+ ): SearchFieldAria {
73
+ if (_slot !== undefined) {
74
+ return withSlot(_slot, useSearchField, props, state, inputRef);
75
+ }
76
+
77
+ let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-aria/searchfield');
78
+ let { isDisabled, isReadOnly, onSubmit, onClear, type = 'search' } = props;
79
+
80
+ let { keyboardProps } = useKeyboard({
81
+ isDisabled: isDisabled || isReadOnly,
82
+ shortcuts: {
83
+ Enter: () => {
84
+ if (onSubmit) {
85
+ // for backward compatibility;
86
+ // otherwise, "Enter" on an input would trigger a form submit, the default browser behavior
87
+ onSubmit(state.value);
88
+ return;
89
+ }
90
+ return false;
91
+ },
92
+ Escape: () => {
93
+ // Also check the inputRef value for the case where the value was set directly on the input element instead of going through
94
+ // the hook
95
+ if (state.value === '' && (!inputRef.current || inputRef.current.value === '')) {
96
+ return false;
97
+ }
98
+ state.setValue('');
99
+ onClear?.();
100
+ },
101
+ },
102
+ });
103
+
104
+ let onClearButtonClick = () => {
105
+ state.setValue('');
106
+
107
+ if (onClear) {
108
+ onClear();
109
+ }
110
+ };
111
+
112
+ let onPressStart = () => {
113
+ // this is in PressStart for mobile so that touching the clear button doesn't remove focus from
114
+ // the input and close the keyboard
115
+ inputRef.current?.focus();
116
+ };
117
+
118
+ let { labelProps, inputProps, descriptionProps, errorMessageProps, ...validation } = useTextField(
119
+ {
120
+ ...props,
121
+ value: state.value,
122
+ onChange: state.setValue,
123
+ onKeyDown: props.onKeyDown,
124
+ onKeyUp: props.onKeyUp,
125
+ type,
126
+ },
127
+ inputRef,
128
+ );
129
+
130
+ return {
131
+ labelProps,
132
+ // An edge case, in Autocomplete, if the keyboard hanlders are not in this order, then
133
+ // Escape runs autocomplete/listbox first, then the search-field shortcut returns false and
134
+ // continues propagation, leaking Escape to a parent Dialog.
135
+ inputProps: mergeProps(keyboardProps, {
136
+ ...inputProps,
137
+ // already handled by useSearchFieldState
138
+ defaultValue: undefined,
139
+ }),
140
+ clearButtonProps: {
141
+ 'aria-label': stringFormatter.format('Clear search'),
142
+ excludeFromTabOrder: true,
143
+ preventFocusOnPress: true,
144
+ isDisabled: isDisabled || isReadOnly,
145
+ onPress: onClearButtonClick,
146
+ onPressStart,
147
+ },
148
+ descriptionProps,
149
+ errorMessageProps,
150
+ ...validation,
151
+ };
152
+ }