@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
@@ -0,0 +1,218 @@
1
+ /** @jsxImportSource octane */
2
+ // Ported from adobe/react-spectrum@5ecb3333001313e83898cd07644227897e3bae1f (packages/react-aria/src/interactions/createKeyboardShortcutHandler.ts).
3
+ /*
4
+ * Copyright 2025 Adobe. All rights reserved.
5
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License. You may obtain a copy
7
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
8
+ *
9
+ * Unless required by applicable law or agreed to in writing, software distributed under
10
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
11
+ * OF ANY KIND, either express or implied. See the License for the specific language
12
+ * governing permissions and limitations under the License.
13
+ */
14
+
15
+ import { isMac } from '../utils/platform';
16
+ import type { BaseEvent } from './createEventHandler';
17
+ type KeyboardEvent = BaseEvent<globalThis.KeyboardEvent>;
18
+
19
+ export type KeyboardShortcutAction = (
20
+ e: KeyboardEvent,
21
+ ) =>
22
+ void | boolean | Partial<{ shouldContinuePropagation?: boolean; shouldPreventDefault?: boolean }>;
23
+
24
+ /** Maps shortcut strings (e.g. `"Mod+s"`, `"Control+Shift+a"`) to handlers. */
25
+ export type KeyboardShortcutBindings = Record<string, KeyboardShortcutAction>;
26
+
27
+ /** Modifier names in shortcut strings (case-insensitive). Order in the string does not matter. */
28
+ const MODIFIER_NAMES = new Set([
29
+ 'shift',
30
+ 'alt',
31
+ 'control',
32
+ 'meta',
33
+ 'mod', // OS dependent - Cmd on Mac, Control on Windows/Linux
34
+ ]);
35
+
36
+ /** Canonical modifier order for stable keys (sorted, fixed order). */
37
+ const CANONICAL_MODIFIER_ORDER = ['Alt', 'Control', 'Meta', 'Shift'] as const;
38
+
39
+ export interface ParsedKeyboardShortcut {
40
+ shift: boolean;
41
+ alt: boolean;
42
+ ctrl: boolean;
43
+ meta: boolean;
44
+ /**
45
+ * Platform primary: Cmd on Mac, Control on Windows/Linux — expands to Meta or Control in
46
+ * canonical form.
47
+ */
48
+ mod: boolean;
49
+ key: string;
50
+ }
51
+
52
+ /**
53
+ * Builds the set of canonical modifier tokens for a binding.
54
+ * `Mod` contributes Meta (Mac) or Ctrl (non-Mac); explicit Ctrl/Meta add those keys too.
55
+ */
56
+ export function modifierSetFromParsed(parsed: ParsedKeyboardShortcut): Set<string> {
57
+ let set = new Set<string>();
58
+ if (parsed.alt) {
59
+ set.add('Alt');
60
+ }
61
+ if (parsed.shift) {
62
+ set.add('Shift');
63
+ }
64
+ if (parsed.ctrl) {
65
+ set.add('Control');
66
+ }
67
+ if (parsed.meta) {
68
+ set.add('Meta');
69
+ }
70
+ if (parsed.mod) {
71
+ set.add(isMac() ? 'Meta' : 'Control');
72
+ }
73
+ return set;
74
+ }
75
+
76
+ /** Modifier set from a keydown event (native flags only). */
77
+ export function modifierSetFromEvent(e: KeyboardEvent): Set<string> {
78
+ let set = new Set<string>();
79
+ if (e.altKey) {
80
+ set.add('Alt');
81
+ }
82
+ if (e.ctrlKey) {
83
+ set.add('Control');
84
+ }
85
+ if (e.metaKey) {
86
+ set.add('Meta');
87
+ }
88
+ if (e.shiftKey) {
89
+ set.add('Shift');
90
+ }
91
+ return set;
92
+ }
93
+
94
+ function sortedModifierTokens(set: Set<string>): string[] {
95
+ return CANONICAL_MODIFIER_ORDER.filter((name) => set.has(name));
96
+ }
97
+
98
+ /**
99
+ * Parses a shortcut like `"Mod+Shift+z"`, `"Ctrl+Alt+Enter"`, or `"Escape"`.
100
+ * Modifiers are case-insensitive; order does not matter. `control` is an alias for `ctrl`.
101
+ */
102
+ export function parseKeyboardShortcut(spec: string): ParsedKeyboardShortcut {
103
+ let parts = spec.split('+').reduce<ParsedKeyboardShortcut>(
104
+ (prev, part) => {
105
+ let lower = part.toLowerCase();
106
+ if (MODIFIER_NAMES.has(lower)) {
107
+ if (lower === 'shift') {
108
+ prev.shift = true;
109
+ } else if (lower === 'alt') {
110
+ prev.alt = true;
111
+ } else if (lower === 'control') {
112
+ prev.ctrl = true;
113
+ } else if (lower === 'meta') {
114
+ prev.meta = true;
115
+ } else if (lower === 'mod') {
116
+ prev.mod = true;
117
+ }
118
+ } else {
119
+ prev.key = part;
120
+ }
121
+ return prev;
122
+ },
123
+ { shift: false, alt: false, ctrl: false, meta: false, mod: false, key: '' },
124
+ );
125
+ if (parts.key === '') {
126
+ throw new Error(
127
+ `Invalid keyboard shortcut: "${spec}". Must include exactly one non-modifier key (e.g. "a", "Enter", "ArrowDown"). Combine any of Shift, Alt, Ctrl, Meta, and Mod.`,
128
+ );
129
+ }
130
+ return parts;
131
+ }
132
+
133
+ function normalizeEventKey(key: string): string {
134
+ return key.toLowerCase();
135
+ }
136
+
137
+ /** Short aliases for common keys (shortcut side, before match). */
138
+ const KEY_ALIASES: Record<string, string> = {
139
+ space: ' ',
140
+ esc: 'escape',
141
+ del: 'delete',
142
+ ins: 'insert',
143
+ left: 'arrowleft',
144
+ right: 'arrowright',
145
+ up: 'arrowup',
146
+ down: 'arrowdown',
147
+ pageup: 'pageup',
148
+ pagedown: 'pagedown',
149
+ };
150
+
151
+ /** Canonical key segment (lowercase); aliases like `down` → `arrowdown`. */
152
+ function canonicalKeyFromSpecKey(specKey: string): string {
153
+ let k = normalizeEventKey(specKey);
154
+ let aliased = KEY_ALIASES[k];
155
+ return aliased != null ? aliased : k;
156
+ }
157
+
158
+ /** Canonical shortcut string for a binding (modifiers sorted: Alt, Ctrl, Meta, Shift, then key). */
159
+ export function canonicalKeyboardShortcut(parsed: ParsedKeyboardShortcut): string {
160
+ let mods = sortedModifierTokens(modifierSetFromParsed(parsed));
161
+ let key = canonicalKeyFromSpecKey(parsed.key);
162
+ return mods.length > 0 ? `${mods.join('+')}+${key}` : key;
163
+ }
164
+
165
+ /** Canonical shortcut string for a keydown event. */
166
+ export function keyboardEventToCanonicalShortcut(e: KeyboardEvent): string {
167
+ let mods = sortedModifierTokens(modifierSetFromEvent(e));
168
+ let key = normalizeEventKey(e.key);
169
+ let prefix = mods.length > 0 ? `${mods.join('+')}+` : '';
170
+ return prefix + key;
171
+ }
172
+
173
+ /**
174
+ * Returns a keydown handler that runs the action only for an exact modifier+key match.
175
+ * Modifier order in the string does not matter (`Shift+Mod+a` ≡ `Mod+Shift+a`).
176
+ * Any combination of **Shift**, **Alt**, **Ctrl**, **Meta**, and **Mod** is allowed; **Mod** means
177
+ * Cmd on Apple platforms and Ctrl on Windows/Linux (same as before). **control** aliases **ctrl**.
178
+ *
179
+ * Duplicate bindings that normalize to the same shortcut: later object entries win.
180
+ *
181
+ * @example
182
+ * ```tsx
183
+ * let onKeyDown = createKeyboardShortcutHandler({
184
+ * 'Mod+s': e => {
185
+ * e.preventDefault();
186
+ * save();
187
+ * },
188
+ * 'Ctrl+Shift+k': () => palette(),
189
+ * 'Meta+Alt+ArrowLeft': () => back()
190
+ * });
191
+ * ```;
192
+ */
193
+ export function createKeyboardShortcutHandler(
194
+ bindings: KeyboardShortcutBindings,
195
+ ): (e: KeyboardEvent) => void {
196
+ let map = new Map<string, KeyboardShortcutAction>();
197
+ for (let [spec, action] of Object.entries(bindings)) {
198
+ let parsed = parseKeyboardShortcut(spec);
199
+ map.set(canonicalKeyboardShortcut(parsed), action);
200
+ }
201
+
202
+ return (e: KeyboardEvent) => {
203
+ let canonical = keyboardEventToCanonicalShortcut(e);
204
+ let action = map.get(canonical);
205
+ let result = action?.(e);
206
+ if (result === undefined && action !== undefined) {
207
+ result = { shouldContinuePropagation: false, shouldPreventDefault: true };
208
+ } else if (typeof result === 'boolean') {
209
+ result = { shouldContinuePropagation: !result, shouldPreventDefault: result };
210
+ }
211
+ if (result?.shouldPreventDefault) {
212
+ e.preventDefault();
213
+ }
214
+ if (!action || result?.shouldContinuePropagation) {
215
+ e.continuePropagation();
216
+ }
217
+ };
218
+ }
@@ -4,7 +4,7 @@
4
4
  // `string`, which this repo's strict indexing rejects).
5
5
  import { getOwnerDocument } from '../utils/domHelpers';
6
6
 
7
- import { isIOS } from '../utils/platform';
7
+ import { isIOS, isWebKit } from '../utils/platform';
8
8
  import { runAfterTransition } from '../utils/runAfterTransition';
9
9
 
10
10
  // Safari on iOS starts selecting text on long press. The only way to avoid this, it seems,
@@ -29,7 +29,7 @@ let savedUserSelect = '';
29
29
  let modifiedElementMap = new WeakMap<Element, string>();
30
30
 
31
31
  export function disableTextSelection(target?: Element): void {
32
- if (isIOS()) {
32
+ if (isIOS() && isWebKit()) {
33
33
  if (state === 'default') {
34
34
  const documentObject = getOwnerDocument(target);
35
35
  savedUserSelect = documentObject.documentElement.style.webkitUserSelect;
@@ -48,7 +48,7 @@ export function disableTextSelection(target?: Element): void {
48
48
  }
49
49
 
50
50
  export function restoreTextSelection(target?: Element): void {
51
- if (isIOS()) {
51
+ if (isIOS() && isWebKit()) {
52
52
  // If the state is already default, there's nothing to do.
53
53
  // If it is restoring, then there's no need to queue a second restore.
54
54
  if (state !== 'disabled') {
@@ -0,0 +1,125 @@
1
+ /** @jsxImportSource octane */
2
+ // Ported from adobe/react-spectrum@5ecb3333001313e83898cd07644227897e3bae1f (packages/react-aria/src/interactions/useContextMenu.ts).
3
+ /*
4
+ * Copyright 2020 Adobe. All rights reserved.
5
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License. You may obtain a copy
7
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
8
+ *
9
+ * Unless required by applicable law or agreed to in writing, software distributed under
10
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
11
+ * OF ANY KIND, either express or implied. See the License for the specific language
12
+ * governing permissions and limitations under the License.
13
+ */
14
+
15
+ import { HTMLAttributes, useRef } from '../compat/react';
16
+ import { isIOS, isMac } from '../utils/platform';
17
+ import { mergeProps } from '../utils/mergeProps';
18
+ import { useLongPress } from './useLongPress';
19
+
20
+ export interface ContextMenuEvent {
21
+ /** The target element on which the event was triggered. */
22
+ target: Element;
23
+ /** X position relative to the target. */
24
+ x: number;
25
+ /** Y position relative to the target. */
26
+ y: number;
27
+ }
28
+
29
+ export interface ContextMenuProps {
30
+ /** Event that is called when a context menu is triggered. */
31
+ onContextMenu?: (e: ContextMenuEvent) => void;
32
+ }
33
+
34
+ export interface ContextMenuAria {
35
+ /** Props to spread on the target element. */
36
+ contextMenuProps: HTMLAttributes<HTMLElement>;
37
+ }
38
+
39
+ /**
40
+ * Handles context menu events across mouse, touch, keyboard, and screen reader interactions.
41
+ */
42
+ export function useContextMenu(props: ContextMenuProps): ContextMenuAria {
43
+ // How to trigger context menu events on various platforms:
44
+ // - macOS
45
+ // - Mouse right click
46
+ // - Control + click
47
+ // - Control + Enter (does not fire the contextmenu event in certain WebKit / Chrome versions - https://bugs.webkit.org/show_bug.cgi?id=302049, https://issues.chromium.org/issues/369897039)
48
+ // - Control + Option + Shift + M with VoiceOver
49
+ // - Windows / Linux
50
+ // - Mouse right click
51
+ // - Shift + F10
52
+ // - Long press on a touch screen
53
+ // - iOS
54
+ // - Long press (does not fire contextmenu event - https://bugs.webkit.org/show_bug.cgi?id=213953)
55
+ // - Android
56
+ // - Long press
57
+
58
+ let { onContextMenu } = props;
59
+ let firedContextMenuEvent = useRef(false);
60
+
61
+ // iOS does not fire the contextmenu event, so use long press.
62
+ let { longPressProps } = useLongPress({
63
+ onLongPressStart() {
64
+ firedContextMenuEvent.current = false;
65
+ },
66
+ onLongPress(e) {
67
+ if (!firedContextMenuEvent.current) {
68
+ onContextMenu?.({ target: e.target, x: e.x, y: e.y });
69
+ } else {
70
+ firedContextMenuEvent.current = false;
71
+ }
72
+ },
73
+ });
74
+
75
+ if (!onContextMenu) {
76
+ return {
77
+ contextMenuProps: {},
78
+ };
79
+ }
80
+
81
+ return {
82
+ // oxlint-disable-next-line react/react-compiler - it says we are reading a ref during render but that's not true...
83
+ contextMenuProps: mergeProps(isIOS() ? longPressProps : {}, {
84
+ onContextMenu(e: import('octane').MouseEvent<HTMLElement>) {
85
+ e.stopPropagation();
86
+ e.preventDefault();
87
+ firedContextMenuEvent.current = true;
88
+
89
+ let rect = e.currentTarget.getBoundingClientRect();
90
+ onContextMenu({
91
+ target: e.currentTarget,
92
+ x: e.clientX - rect.x,
93
+ y: e.clientY - rect.y,
94
+ });
95
+ },
96
+ onKeyDown(e: import('octane').KeyboardEvent<HTMLElement>) {
97
+ // macOS has a default keyboard shortcut to show the contextmenu: Ctrl + Enter.
98
+ // However, some versions of Safari and Chrome do not trigger the contextmenu event.
99
+ // Fixed in https://github.com/WebKit/WebKit/pull/62278 (currently in WekKit nightly) and
100
+ // https://github.com/chromium/chromium/commit/268c876c191cd4712c2d1043aab9760fb71d9be5 (Chrome 147).
101
+ // Remove this workaround once those are broadly available.
102
+ // An additional bug still occurs when the target has a border-radius: https://bugs.webkit.org/show_bug.cgi?id=317496
103
+ if (isMac()) {
104
+ if (e.ctrlKey && e.key === 'Enter') {
105
+ firedContextMenuEvent.current = false;
106
+ let target = e.currentTarget;
107
+ e.stopPropagation();
108
+ setTimeout(() => {
109
+ if (!firedContextMenuEvent.current) {
110
+ let rect = target.getBoundingClientRect();
111
+ onContextMenu({
112
+ target,
113
+ x: rect.width / 2,
114
+ y: rect.height / 2,
115
+ });
116
+ } else {
117
+ firedContextMenuEvent.current = false;
118
+ }
119
+ }, 10);
120
+ }
121
+ }
122
+ },
123
+ } satisfies HTMLAttributes<HTMLElement>),
124
+ };
125
+ }
@@ -110,8 +110,8 @@ function handleFocusEvent(e: FocusEvent) {
110
110
  // Firefox fires two extra focus events when the user first clicks into an iframe:
111
111
  // first on the window, then on the document. We ignore these events so they don't
112
112
  // cause keyboard focus rings to appear.
113
- let ownerWindow = getOwnerWindow(getEventTarget(e) as Element);
114
- let ownerDocument = getOwnerDocument(getEventTarget(e) as Element);
113
+ let ownerWindow = getOwnerWindow(getEventTarget(e));
114
+ let ownerDocument = getOwnerDocument(getEventTarget(e));
115
115
  if (
116
116
  getEventTarget(e) === ownerWindow ||
117
117
  getEventTarget(e) === ownerDocument ||
@@ -352,7 +352,7 @@ const nonTextInputTypes = new Set([
352
352
  */
353
353
  function isKeyboardFocusEvent(isTextInput: boolean, modality: Modality, e: HandlerEvent) {
354
354
  let eventTarget = e ? (getEventTarget(e) as Element) : undefined;
355
- let document = getOwnerDocument(eventTarget);
355
+ let ownerDocument = getOwnerDocument(eventTarget);
356
356
  let ownerWindow = getOwnerWindow(eventTarget);
357
357
  const IHTMLInputElement =
358
358
  typeof ownerWindow !== 'undefined' ? ownerWindow.HTMLInputElement : HTMLInputElement;
@@ -364,7 +364,7 @@ function isKeyboardFocusEvent(isTextInput: boolean, modality: Modality, e: Handl
364
364
 
365
365
  // For keyboard events that occur on a non-input element that will move focus into input element (aka ArrowLeft going from Datepicker button to the main input group)
366
366
  // we need to rely on the user passing isTextInput into here. This way we can skip toggling focus visiblity for said input element
367
- let activeElement = getActiveElement(document);
367
+ let activeElement = getActiveElement(ownerDocument);
368
368
  isTextInput =
369
369
  isTextInput ||
370
370
  (activeElement instanceof IHTMLInputElement && !nonTextInputTypes.has(activeElement.type)) ||
@@ -1,7 +1,13 @@
1
+ import {
2
+ createKeyboardShortcutHandler,
3
+ type KeyboardShortcutBindings,
4
+ } from './createKeyboardShortcutHandler';
5
+ import { getEventTarget, nodeContains } from '../utils/shadowdom/DOMFunctions';
1
6
  // Ported from react-aria (source: .react-spectrum/packages/react-aria/src/interactions/useKeyboard.ts).
2
7
  // octane adaptations:
3
8
  // - `KeyboardEvents` / `DOMAttributes` from '@react-types/shared' are typed over React's
4
9
  // synthetic events; local structural aliases over native KeyboardEvent replace them.
10
+ // - Shortcut and user callbacks share the BaseEvent wrapper so both have propagation controls.
5
11
  // - Public-hook slot threading (splitSlot) per the binding convention; no octane base hooks
6
12
  // are composed here, so the slot is absorbed and unused.
7
13
  import type { BaseEvent } from './createEventHandler';
@@ -20,6 +26,9 @@ export interface KeyboardEvents {
20
26
  export type DOMAttributes = Record<string, any>;
21
27
 
22
28
  export interface KeyboardProps extends KeyboardEvents {
29
+ shortcuts?: KeyboardShortcutBindings;
30
+ allowRepeats?: boolean;
31
+ allowComposing?: boolean;
23
32
  /** Whether the keyboard events should be disabled. */
24
33
  isDisabled?: boolean;
25
34
  }
@@ -39,12 +48,51 @@ export function useKeyboard(...args: any[]): KeyboardResult {
39
48
  const [user] = splitSlot(args);
40
49
  const props = user[0] as KeyboardProps;
41
50
 
51
+ let { shortcuts, allowRepeats = false, allowComposing = false } = props;
52
+ let onKeyDown;
53
+ let onKeyUp;
54
+ if (shortcuts) {
55
+ let shortcutHandler = createKeyboardShortcutHandler(shortcuts);
56
+ onKeyDown = createEventHandler<KeyboardEvent>((e) => {
57
+ props.onKeyDown?.(e);
58
+ // If keyboard event didn't originate from a child of the current target,
59
+ // then it's a React event coming through a portal. We should ignore it.
60
+ if (!nodeContains(e.currentTarget as Node, getEventTarget(e) as Element)) {
61
+ e.continuePropagation();
62
+ return;
63
+ }
64
+ if ((e.repeat && !allowRepeats) || (e.isComposing && !allowComposing)) {
65
+ e.continuePropagation();
66
+ return;
67
+ }
68
+
69
+ shortcutHandler(e);
70
+ });
71
+ onKeyUp = createEventHandler<KeyboardEvent>((e) => {
72
+ props.onKeyUp?.(e);
73
+ // If keyboard event didn't originate from a child of the current target,
74
+ // then it's a React event coming through a portal. We should ignore it.
75
+ if (!nodeContains(e.currentTarget as Node, getEventTarget(e) as Element)) {
76
+ e.continuePropagation();
77
+ return;
78
+ }
79
+ if ((e.repeat && !allowRepeats) || (e.isComposing && !allowComposing)) {
80
+ e.continuePropagation();
81
+ return;
82
+ }
83
+ // implement shortcut handler on keyup, what should the map be called? or should it be another syntax on shortcuts?
84
+ e.continuePropagation();
85
+ });
86
+ } else {
87
+ onKeyDown = createEventHandler(props.onKeyDown);
88
+ onKeyUp = createEventHandler(props.onKeyUp);
89
+ }
42
90
  return {
43
91
  keyboardProps: props.isDisabled
44
92
  ? {}
45
93
  : {
46
- onKeyDown: createEventHandler(props.onKeyDown),
47
- onKeyUp: createEventHandler(props.onKeyUp),
94
+ onKeyDown,
95
+ onKeyUp,
48
96
  },
49
97
  };
50
98
  }
@@ -4,7 +4,7 @@
4
4
  // synthetic handlers); the `contextmenu` listener param carries an explicit native
5
5
  // annotation (upstream got it contextually).
6
6
  // - Public-hook slot threading (splitSlot/subSlot) per the binding convention.
7
- import type { FocusableElement, LongPressEvent } from '@react-types/shared';
7
+ import type { FocusableElement, LongPressEvent, PressEvent } from '@react-types/shared';
8
8
  import { focusWithoutScrolling } from '../utils/focusWithoutScrolling';
9
9
  import { getOwnerDocument, getOwnerWindow } from '../utils/domHelpers';
10
10
  import { mergeProps } from '../utils/mergeProps';
@@ -21,6 +21,8 @@ type DOMAttributes = Record<string, any>;
21
21
  export interface LongPressProps {
22
22
  /** Whether long press events should be disabled. */
23
23
  isDisabled?: boolean;
24
+ /** Which pointer type to listen for. By default, both mouse and touch are listened for. */
25
+ pointerType?: 'mouse' | 'touch';
24
26
  /** Handler that is called when a long press interaction starts. */
25
27
  onLongPressStart?: (e: LongPressEvent) => void;
26
28
  /**
@@ -65,6 +67,7 @@ export function useLongPress(...args: any[]): LongPressResult {
65
67
 
66
68
  let {
67
69
  isDisabled,
70
+ pointerType,
68
71
  onLongPressStart,
69
72
  onLongPressEnd,
70
73
  onLongPress,
@@ -76,14 +79,20 @@ export function useLongPress(...args: any[]): LongPressResult {
76
79
  undefined,
77
80
  subSlot(slot, 'timer'),
78
81
  );
79
- let { addGlobalListener, removeGlobalListener } = useGlobalListeners(subSlot(slot, 'listeners'));
82
+ let { addGlobalListener, removeAllGlobalListeners } = useGlobalListeners(
83
+ subSlot(slot, 'listeners'),
84
+ );
85
+ let isAcceptedPointerType = (e: PressEvent) =>
86
+ pointerType
87
+ ? e.pointerType === pointerType
88
+ : e.pointerType === 'mouse' || e.pointerType === 'touch';
80
89
 
81
90
  let { pressProps } = usePress(
82
91
  {
83
92
  isDisabled,
84
93
  onPressStart(e) {
85
94
  e.continuePropagation();
86
- if (e.pointerType === 'mouse' || e.pointerType === 'touch') {
95
+ if (isAcceptedPointerType(e)) {
87
96
  if (onLongPressStart) {
88
97
  onLongPressStart({
89
98
  ...e,
@@ -95,6 +104,9 @@ export function useLongPress(...args: any[]): LongPressResult {
95
104
  // Prevent other usePress handlers from also handling this event.
96
105
  e.target.dispatchEvent(new PointerEvent('pointercancel', { bubbles: true }));
97
106
 
107
+ // Prevent default click action (e.g. opening a link) after a long press.
108
+ addGlobalListener(e.target, 'click', (e) => e.preventDefault(), { once: true });
109
+
98
110
  // Ensure target is focused. On touch devices, browsers typically focus on pointer up.
99
111
  if (getOwnerDocument(e.target).activeElement !== e.target) {
100
112
  focusWithoutScrolling(e.target as FocusableElement);
@@ -111,25 +123,24 @@ export function useLongPress(...args: any[]): LongPressResult {
111
123
 
112
124
  // Prevent context menu, which may be opened on long press on touch devices
113
125
  if (e.pointerType === 'touch') {
114
- let onContextMenu = (e: Event) => {
115
- e.preventDefault();
116
- };
117
-
118
- let ownerWindow = getOwnerWindow(e.target);
119
- addGlobalListener(e.target, 'contextmenu', onContextMenu, { once: true });
120
- addGlobalListener(
121
- ownerWindow,
122
- 'pointerup',
123
- () => {
124
- // If no contextmenu event is fired quickly after pointerup, remove the handler
125
- // so future context menu events outside a long press are not prevented.
126
- setTimeout(() => {
127
- removeGlobalListener(e.target, 'contextmenu', onContextMenu);
128
- }, 30);
129
- },
130
- { once: true },
131
- );
126
+ addGlobalListener(e.target, 'contextmenu', (e: Event) => e.preventDefault(), {
127
+ once: true,
128
+ });
132
129
  }
130
+
131
+ let ownerWindow = getOwnerWindow(e.target);
132
+ addGlobalListener(
133
+ ownerWindow,
134
+ 'pointerup',
135
+ () => {
136
+ // If no contextmenu event is fired quickly after pointerup, remove the handler
137
+ // so future context menu events outside a long press are not prevented.
138
+ setTimeout(() => {
139
+ removeAllGlobalListeners();
140
+ }, 100);
141
+ },
142
+ { once: true },
143
+ );
133
144
  }
134
145
  },
135
146
  onPressEnd(e) {
@@ -137,7 +148,7 @@ export function useLongPress(...args: any[]): LongPressResult {
137
148
  clearTimeout(timeRef.current);
138
149
  }
139
150
 
140
- if (onLongPressEnd && (e.pointerType === 'mouse' || e.pointerType === 'touch')) {
151
+ if (onLongPressEnd && isAcceptedPointerType(e)) {
141
152
  onLongPressEnd({
142
153
  ...e,
143
154
  type: 'longpressend',
@@ -130,7 +130,7 @@ export function useMove(...args: any[]): MoveResult {
130
130
  let onMouseUp = (e: MouseEvent) => {
131
131
  if (e.button === 0) {
132
132
  endEvent(e, 'mouse');
133
- let ownerWindow = getOwnerWindow(getEventTarget(e) as Element);
133
+ let ownerWindow = getOwnerWindow(getEventTarget(e));
134
134
  removeGlobalListener(ownerWindow, 'mousemove', onMouseMove, false);
135
135
  removeGlobalListener(ownerWindow, 'mouseup', onMouseUp, false);
136
136
  }
@@ -141,7 +141,7 @@ export function useMove(...args: any[]): MoveResult {
141
141
  e.stopPropagation();
142
142
  e.preventDefault();
143
143
  state.current.lastPosition = { pageX: e.pageX, pageY: e.pageY };
144
- let ownerWindow = getOwnerWindow(getEventTarget(e) as Element);
144
+ let ownerWindow = getOwnerWindow(getEventTarget(e));
145
145
  addGlobalListener(ownerWindow, 'mousemove', onMouseMove, false);
146
146
  addGlobalListener(ownerWindow, 'mouseup', onMouseUp, false);
147
147
  }
@@ -169,7 +169,7 @@ export function useMove(...args: any[]): MoveResult {
169
169
  if (touch >= 0) {
170
170
  endEvent(e, 'touch');
171
171
  state.current.id = null;
172
- let ownerWindow = getOwnerWindow(getEventTarget(e) as Element);
172
+ let ownerWindow = getOwnerWindow(getEventTarget(e));
173
173
  removeGlobalListener(ownerWindow, 'touchmove', onTouchMove);
174
174
  removeGlobalListener(ownerWindow, 'touchend', onTouchEnd);
175
175
  removeGlobalListener(ownerWindow, 'touchcancel', onTouchEnd);
@@ -186,7 +186,7 @@ export function useMove(...args: any[]): MoveResult {
186
186
  e.preventDefault();
187
187
  state.current.lastPosition = { pageX, pageY };
188
188
  state.current.id = identifier;
189
- let ownerWindow = getOwnerWindow(getEventTarget(e) as Element);
189
+ let ownerWindow = getOwnerWindow(getEventTarget(e));
190
190
  addGlobalListener(ownerWindow, 'touchmove', onTouchMove, false);
191
191
  addGlobalListener(ownerWindow, 'touchend', onTouchEnd, false);
192
192
  addGlobalListener(ownerWindow, 'touchcancel', onTouchEnd, false);
@@ -214,7 +214,7 @@ export function useMove(...args: any[]): MoveResult {
214
214
  let pointerType = (e.pointerType || 'mouse') as PointerType;
215
215
  endEvent(e, pointerType);
216
216
  state.current.id = null;
217
- let ownerWindow = getOwnerWindow(getEventTarget(e) as Element);
217
+ let ownerWindow = getOwnerWindow(getEventTarget(e));
218
218
  removeGlobalListener(ownerWindow, 'pointermove', onPointerMove, false);
219
219
  removeGlobalListener(ownerWindow, 'pointerup', onPointerUp, false);
220
220
  removeGlobalListener(ownerWindow, 'pointercancel', onPointerUp, false);
@@ -228,7 +228,7 @@ export function useMove(...args: any[]): MoveResult {
228
228
  e.preventDefault();
229
229
  state.current.lastPosition = { pageX: e.pageX, pageY: e.pageY };
230
230
  state.current.id = e.pointerId;
231
- let ownerWindow = getOwnerWindow(getEventTarget(e) as Element);
231
+ let ownerWindow = getOwnerWindow(getEventTarget(e));
232
232
  addGlobalListener(ownerWindow, 'pointermove', onPointerMove, false);
233
233
  addGlobalListener(ownerWindow, 'pointerup', onPointerUp, false);
234
234
  addGlobalListener(ownerWindow, 'pointercancel', onPointerUp, false);
@@ -1222,7 +1222,15 @@ function shouldPreventDefaultUp(target: Element) {
1222
1222
  }
1223
1223
 
1224
1224
  function shouldPreventDefaultKeyboard(target: Element, key: string) {
1225
+ if (isMac() && key === 'Enter') {
1226
+ return false;
1227
+ }
1228
+
1225
1229
  if (target instanceof HTMLInputElement) {
1230
+ if (key === 'Enter' && (target.type === 'checkbox' || target.type === 'radio')) {
1231
+ // Enter on a checkbox or radio should do an implicit form submission, but not toggle the input.
1232
+ return false;
1233
+ }
1226
1234
  return !isValidInputKey(target, key);
1227
1235
  }
1228
1236