@react-aria/selection 3.16.2 → 3.17.1
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.
- package/dist/import.mjs +62 -35
- package/dist/main.js +61 -34
- package/dist/main.js.map +1 -1
- package/dist/module.js +62 -35
- package/dist/module.js.map +1 -1
- package/dist/types.d.ts +22 -49
- package/dist/types.d.ts.map +1 -1
- package/package.json +11 -10
- package/src/ListKeyboardDelegate.ts +3 -1
- package/src/useSelectableCollection.ts +30 -4
- package/src/useSelectableItem.ts +61 -17
- package/src/useSelectableList.ts +10 -72
package/src/useSelectableItem.ts
CHANGED
|
@@ -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
|
|
177
|
-
let
|
|
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
|
-
|
|
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 ?
|
|
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
|
-
|
|
242
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 as any).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),
|
package/src/useSelectableList.ts
CHANGED
|
@@ -10,71 +10,25 @@
|
|
|
10
10
|
* governing permissions and limitations under the License.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
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 {
|