@react-aria/selection 3.16.2 → 3.17.0

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.
@@ -14,7 +14,7 @@ import {DOMAttributes, FocusableElement, LongPressEvent, PressEvent} from '@reac
14
14
  import {focusSafely} from '@react-aria/focus';
15
15
  import {isCtrlKeyPressed, isNonContiguousSelectionModifier} from './utils';
16
16
  import {Key, RefObject, useEffect, useRef} from 'react';
17
- import {mergeProps} from '@react-aria/utils';
17
+ import {mergeProps, openLink, useRouter} from '@react-aria/utils';
18
18
  import {MultipleSelectionManager} from '@react-stately/selection';
19
19
  import {PressProps, useLongPress, usePress} from '@react-aria/interactions';
20
20
 
@@ -59,7 +59,16 @@ export interface SelectableItemOptions {
59
59
  * Handler that is called when a user performs an action on the item. The exact user event depends on
60
60
  * the collection's `selectionBehavior` prop and the interaction modality.
61
61
  */
62
- onAction?: () => void
62
+ onAction?: () => void,
63
+ /**
64
+ * The behavior of links in the collection.
65
+ * - 'action': link behaves like onAction.
66
+ * - 'selection': link follows selection interactions (e.g. if URL drives selection).
67
+ * - 'override': links override all other interactions (link items are not selectable).
68
+ * - 'none': links are disabled for both selection and actions (e.g. handled elsewhere).
69
+ * @default 'action'
70
+ */
71
+ linkBehavior?: 'action' | 'selection' | 'override' | 'none'
63
72
  }
64
73
 
65
74
  export interface SelectableItemStates {
@@ -107,8 +116,10 @@ export function useSelectableItem(options: SelectableItemOptions): SelectableIte
107
116
  focus,
108
117
  isDisabled,
109
118
  onAction,
110
- allowsDifferentPressOrigin
119
+ allowsDifferentPressOrigin,
120
+ linkBehavior = 'action'
111
121
  } = options;
122
+ let router = useRouter();
112
123
 
113
124
  let onSelect = (e: PressEvent | LongPressEvent | PointerEvent) => {
114
125
  if (e.pointerType === 'keyboard' && isNonContiguousSelectionModifier(e)) {
@@ -118,6 +129,17 @@ export function useSelectableItem(options: SelectableItemOptions): SelectableIte
118
129
  return;
119
130
  }
120
131
 
132
+ if (manager.isLink(key)) {
133
+ if (linkBehavior === 'selection') {
134
+ router.open(ref.current, e);
135
+ // Always set selected keys back to what they were so that select and combobox close.
136
+ manager.setSelectedKeys(manager.selectedKeys);
137
+ return;
138
+ } else if (linkBehavior === 'override' || linkBehavior === 'none') {
139
+ return;
140
+ }
141
+ }
142
+
121
143
  if (manager.selectionMode === 'single') {
122
144
  if (manager.isSelected(key) && !manager.disallowEmptySelection) {
123
145
  manager.toggleSelection(key);
@@ -173,12 +195,14 @@ export function useSelectableItem(options: SelectableItemOptions): SelectableIte
173
195
  // Clicking the checkbox enters selection mode, after which clicking anywhere on any row toggles selection for that row.
174
196
  // With highlight selection, onAction is secondary, and occurs on double click. Single click selects the row.
175
197
  // With touch, onAction occurs on single tap, and long press enters selection mode.
176
- let allowsSelection = !isDisabled && manager.canSelectItem(key);
177
- let allowsActions = onAction && !isDisabled;
198
+ let isLinkOverride = manager.isLink(key) && linkBehavior === 'override';
199
+ let hasLinkAction = manager.isLink(key) && linkBehavior !== 'selection' && linkBehavior !== 'none';
200
+ let allowsSelection = !isDisabled && manager.canSelectItem(key) && !isLinkOverride;
201
+ let allowsActions = (onAction || hasLinkAction) && !isDisabled;
178
202
  let hasPrimaryAction = allowsActions && (
179
203
  manager.selectionBehavior === 'replace'
180
204
  ? !allowsSelection
181
- : manager.isEmpty
205
+ : !allowsSelection || manager.isEmpty
182
206
  );
183
207
  let hasSecondaryAction = allowsActions && allowsSelection && manager.selectionBehavior === 'replace';
184
208
  let hasAction = hasPrimaryAction || hasSecondaryAction;
@@ -188,6 +212,16 @@ export function useSelectableItem(options: SelectableItemOptions): SelectableIte
188
212
  let longPressEnabledOnPressStart = useRef(false);
189
213
  let hadPrimaryActionOnPressStart = useRef(false);
190
214
 
215
+ let performAction = (e) => {
216
+ if (onAction) {
217
+ onAction();
218
+ }
219
+
220
+ if (hasLinkAction) {
221
+ router.open(ref.current, e);
222
+ }
223
+ };
224
+
191
225
  // By default, selection occurs on pointer down. This can be strange if selecting an
192
226
  // item causes the UI to disappear immediately (e.g. menus).
193
227
  // If shouldSelectOnPressUp is true, we use onPressUp instead of onPressStart.
@@ -214,19 +248,19 @@ export function useSelectableItem(options: SelectableItemOptions): SelectableIte
214
248
  return;
215
249
  }
216
250
 
217
- onAction();
218
- } else if (e.pointerType !== 'keyboard') {
251
+ performAction(e);
252
+ } else if (e.pointerType !== 'keyboard' && allowsSelection) {
219
253
  onSelect(e);
220
254
  }
221
255
  };
222
256
  } else {
223
- itemPressProps.onPressUp = (e) => {
224
- if (e.pointerType !== 'keyboard') {
257
+ itemPressProps.onPressUp = hasPrimaryAction ? null : (e) => {
258
+ if (e.pointerType !== 'keyboard' && allowsSelection) {
225
259
  onSelect(e);
226
260
  }
227
261
  };
228
262
 
229
- itemPressProps.onPress = hasPrimaryAction ? () => onAction() : null;
263
+ itemPressProps.onPress = hasPrimaryAction ? performAction : null;
230
264
  }
231
265
  } else {
232
266
  itemPressProps.onPressStart = (e) => {
@@ -238,8 +272,10 @@ export function useSelectableItem(options: SelectableItemOptions): SelectableIte
238
272
  // For keyboard, select on key down. If there is an action, the Space key selects on key down,
239
273
  // and the Enter key performs onAction on key up.
240
274
  if (
241
- (e.pointerType === 'mouse' && !hasPrimaryAction) ||
242
- (e.pointerType === 'keyboard' && (!onAction || isSelectionKey()))
275
+ allowsSelection && (
276
+ (e.pointerType === 'mouse' && !hasPrimaryAction) ||
277
+ (e.pointerType === 'keyboard' && (!allowsActions || isSelectionKey()))
278
+ )
243
279
  ) {
244
280
  onSelect(e);
245
281
  }
@@ -257,8 +293,8 @@ export function useSelectableItem(options: SelectableItemOptions): SelectableIte
257
293
  (e.pointerType === 'mouse' && hadPrimaryActionOnPressStart.current)
258
294
  ) {
259
295
  if (hasAction) {
260
- onAction();
261
- } else {
296
+ performAction(e);
297
+ } else if (allowsSelection) {
262
298
  onSelect(e);
263
299
  }
264
300
  }
@@ -274,7 +310,7 @@ export function useSelectableItem(options: SelectableItemOptions): SelectableIte
274
310
  if (modality.current === 'mouse') {
275
311
  e.stopPropagation();
276
312
  e.preventDefault();
277
- onAction();
313
+ performAction(e);
278
314
  }
279
315
  } : undefined;
280
316
 
@@ -301,12 +337,20 @@ export function useSelectableItem(options: SelectableItemOptions): SelectableIte
301
337
  }
302
338
  };
303
339
 
340
+ // Prevent default on link clicks so that we control exactly
341
+ // when they open (to match selection behavior).
342
+ let onClick = manager.isLink(key) ? e => {
343
+ if (!openLink.isOpening) {
344
+ e.preventDefault();
345
+ }
346
+ } : undefined;
347
+
304
348
  return {
305
349
  itemProps: mergeProps(
306
350
  itemProps,
307
351
  allowsSelection || hasPrimaryAction ? pressProps : {},
308
352
  longPressEnabled ? longPressProps : {},
309
- {onDoubleClick, onDragStartCapture}
353
+ {onDoubleClick, onDragStartCapture, onClick}
310
354
  ),
311
355
  isPressed,
312
356
  isSelected: manager.isSelected(key),
@@ -10,71 +10,25 @@
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
12
 
13
- import {Collection, DOMAttributes, FocusStrategy, KeyboardDelegate, Node} from '@react-types/shared';
14
- import {Key, RefObject, useMemo} from 'react';
13
+ import {AriaSelectableCollectionOptions, useSelectableCollection} from './useSelectableCollection';
14
+ import {Collection, DOMAttributes, KeyboardDelegate, Node} from '@react-types/shared';
15
+ import {Key, useMemo} from 'react';
15
16
  import {ListKeyboardDelegate} from './ListKeyboardDelegate';
16
- import {MultipleSelectionManager} from '@react-stately/selection';
17
17
  import {useCollator} from '@react-aria/i18n';
18
- import {useSelectableCollection} from './useSelectableCollection';
19
18
 
20
- export interface AriaSelectableListOptions {
21
- /**
22
- * An interface for reading and updating multiple selection state.
23
- */
24
- selectionManager: MultipleSelectionManager,
19
+ export interface AriaSelectableListOptions extends Omit<AriaSelectableCollectionOptions, 'keyboardDelegate'> {
25
20
  /**
26
21
  * State of the collection.
27
22
  */
28
23
  collection: Collection<Node<unknown>>,
29
24
  /**
30
- * The item keys that are disabled. These items cannot be selected, focused, or otherwise interacted with.
31
- */
32
- disabledKeys: Set<Key>,
33
- /**
34
- * A ref to the item.
35
- */
36
- ref?: RefObject<HTMLElement>,
37
- /**
38
- * A delegate that returns collection item keys with respect to visual layout.
25
+ * A delegate object that implements behavior for keyboard focus movement.
39
26
  */
40
27
  keyboardDelegate?: KeyboardDelegate,
41
28
  /**
42
- * Whether the collection or one of its items should be automatically focused upon render.
43
- * @default false
44
- */
45
- autoFocus?: boolean | FocusStrategy,
46
- /**
47
- * Whether focus should wrap around when the end/start is reached.
48
- * @default false
49
- */
50
- shouldFocusWrap?: boolean,
51
- /**
52
- * Whether the option is contained in a virtual scroller.
53
- */
54
- isVirtualized?: boolean,
55
- /**
56
- * Whether the collection allows empty selection.
57
- * @default false
58
- */
59
- disallowEmptySelection?: boolean,
60
- /**
61
- * Whether selection should occur automatically on focus.
62
- * @default false
63
- */
64
- selectOnFocus?: boolean,
65
- /**
66
- * Whether typeahead is disabled.
67
- * @default false
68
- */
69
- disallowTypeAhead?: boolean,
70
- /**
71
- * Whether the collection items should use virtual focus instead of being focused directly.
72
- */
73
- shouldUseVirtualFocus?: boolean,
74
- /**
75
- * Whether navigation through tab key is enabled.
29
+ * The item keys that are disabled. These items cannot be selected, focused, or otherwise interacted with.
76
30
  */
77
- allowsTabNavigation?: boolean
31
+ disabledKeys: Set<Key>
78
32
  }
79
33
 
80
34
  export interface SelectableListAria {
@@ -93,15 +47,7 @@ export function useSelectableList(props: AriaSelectableListOptions): SelectableL
93
47
  collection,
94
48
  disabledKeys,
95
49
  ref,
96
- keyboardDelegate,
97
- autoFocus,
98
- shouldFocusWrap,
99
- isVirtualized,
100
- disallowEmptySelection,
101
- selectOnFocus = selectionManager.selectionBehavior === 'replace',
102
- disallowTypeAhead,
103
- shouldUseVirtualFocus,
104
- allowsTabNavigation
50
+ keyboardDelegate
105
51
  } = props;
106
52
 
107
53
  // By default, a KeyboardDelegate is provided which uses the DOM to query layout information (e.g. for page up/page down).
@@ -113,18 +59,10 @@ export function useSelectableList(props: AriaSelectableListOptions): SelectableL
113
59
  ), [keyboardDelegate, collection, disabledKeys, ref, collator, disabledBehavior]);
114
60
 
115
61
  let {collectionProps} = useSelectableCollection({
62
+ ...props,
116
63
  ref,
117
64
  selectionManager,
118
- keyboardDelegate: delegate,
119
- autoFocus,
120
- shouldFocusWrap,
121
- disallowEmptySelection,
122
- selectOnFocus,
123
- disallowTypeAhead,
124
- shouldUseVirtualFocus,
125
- allowsTabNavigation,
126
- isVirtualized,
127
- scrollRef: ref
65
+ keyboardDelegate: delegate
128
66
  });
129
67
 
130
68
  return {