@react-aria/selection 3.6.0 → 3.7.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.
- package/dist/main.js +138 -34
- package/dist/main.js.map +1 -1
- package/dist/module.js +132 -33
- package/dist/module.js.map +1 -1
- package/dist/types.d.ts +10 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +7 -7
- package/src/useSelectableCollection.ts +16 -15
- package/src/useSelectableItem.ts +106 -15
- package/src/utils.ts +34 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@react-aria/selection",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.7.0",
|
|
4
4
|
"description": "Spectrum UI components in React",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"main": "dist/main.js",
|
|
@@ -19,12 +19,12 @@
|
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"@babel/runtime": "^7.6.2",
|
|
21
21
|
"@react-aria/focus": "^3.5.0",
|
|
22
|
-
"@react-aria/i18n": "^3.3.
|
|
23
|
-
"@react-aria/interactions": "^3.
|
|
24
|
-
"@react-aria/utils": "^3.
|
|
22
|
+
"@react-aria/i18n": "^3.3.3",
|
|
23
|
+
"@react-aria/interactions": "^3.7.0",
|
|
24
|
+
"@react-aria/utils": "^3.10.0",
|
|
25
25
|
"@react-stately/collections": "^3.3.3",
|
|
26
|
-
"@react-stately/selection": "^3.
|
|
27
|
-
"@react-types/shared": "^3.
|
|
26
|
+
"@react-stately/selection": "^3.8.0",
|
|
27
|
+
"@react-types/shared": "^3.10.0"
|
|
28
28
|
},
|
|
29
29
|
"peerDependencies": {
|
|
30
30
|
"react": "^16.8.0 || ^17.0.0-rc.1"
|
|
@@ -32,5 +32,5 @@
|
|
|
32
32
|
"publishConfig": {
|
|
33
33
|
"access": "public"
|
|
34
34
|
},
|
|
35
|
-
"gitHead": "
|
|
35
|
+
"gitHead": "896eabe5521a0349a675c4773ed7629addd4b8c4"
|
|
36
36
|
}
|
|
@@ -13,19 +13,12 @@
|
|
|
13
13
|
import {FocusEvent, HTMLAttributes, Key, KeyboardEvent, RefObject, useEffect, useRef} from 'react';
|
|
14
14
|
import {focusSafely, getFocusableTreeWalker} from '@react-aria/focus';
|
|
15
15
|
import {FocusStrategy, KeyboardDelegate} from '@react-types/shared';
|
|
16
|
-
import {focusWithoutScrolling,
|
|
16
|
+
import {focusWithoutScrolling, mergeProps, useEvent} from '@react-aria/utils';
|
|
17
|
+
import {isCtrlKeyPressed, isNonContiguousSelectionModifier} from './utils';
|
|
17
18
|
import {MultipleSelectionManager} from '@react-stately/selection';
|
|
18
19
|
import {useLocale} from '@react-aria/i18n';
|
|
19
20
|
import {useTypeSelect} from './useTypeSelect';
|
|
20
21
|
|
|
21
|
-
function isCtrlKeyPressed(e: KeyboardEvent) {
|
|
22
|
-
if (isMac()) {
|
|
23
|
-
return e.metaKey;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
return e.ctrlKey;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
22
|
interface SelectableCollectionOptions {
|
|
30
23
|
/**
|
|
31
24
|
* An interface for reading and updating multiple selection state.
|
|
@@ -105,7 +98,7 @@ export function useSelectableCollection(options: SelectableCollectionOptions): S
|
|
|
105
98
|
shouldFocusWrap = false,
|
|
106
99
|
disallowEmptySelection = false,
|
|
107
100
|
disallowSelectAll = false,
|
|
108
|
-
selectOnFocus =
|
|
101
|
+
selectOnFocus = manager.selectionBehavior === 'replace',
|
|
109
102
|
disallowTypeAhead = false,
|
|
110
103
|
shouldUseVirtualFocus,
|
|
111
104
|
allowsTabNavigation = false,
|
|
@@ -115,16 +108,16 @@ export function useSelectableCollection(options: SelectableCollectionOptions): S
|
|
|
115
108
|
} = options;
|
|
116
109
|
let {direction} = useLocale();
|
|
117
110
|
|
|
111
|
+
|
|
118
112
|
let onKeyDown = (e: KeyboardEvent) => {
|
|
119
113
|
// Prevent option + tab from doing anything since it doesn't move focus to the cells, only buttons/checkboxes
|
|
120
114
|
if (e.altKey && e.key === 'Tab') {
|
|
121
115
|
e.preventDefault();
|
|
122
116
|
}
|
|
123
117
|
|
|
124
|
-
// Let child element (e.g. menu button) handle the event if the Alt key is pressed.
|
|
125
118
|
// Keyboard events bubble through portals. Don't handle keyboard events
|
|
126
119
|
// for elements outside the collection (e.g. menus).
|
|
127
|
-
if (
|
|
120
|
+
if (!ref.current.contains(e.target as HTMLElement)) {
|
|
128
121
|
return;
|
|
129
122
|
}
|
|
130
123
|
|
|
@@ -134,7 +127,7 @@ export function useSelectableCollection(options: SelectableCollectionOptions): S
|
|
|
134
127
|
|
|
135
128
|
if (e.shiftKey && manager.selectionMode === 'multiple') {
|
|
136
129
|
manager.extendSelection(key);
|
|
137
|
-
} else if (selectOnFocus) {
|
|
130
|
+
} else if (selectOnFocus && !isNonContiguousSelectionModifier(e)) {
|
|
138
131
|
manager.replaceSelection(key);
|
|
139
132
|
}
|
|
140
133
|
}
|
|
@@ -291,14 +284,22 @@ export function useSelectableCollection(options: SelectableCollectionOptions): S
|
|
|
291
284
|
manager.setFocused(true);
|
|
292
285
|
|
|
293
286
|
if (manager.focusedKey == null) {
|
|
287
|
+
let navigateToFirstKey = (key: Key | undefined) => {
|
|
288
|
+
if (key != null) {
|
|
289
|
+
manager.setFocusedKey(key);
|
|
290
|
+
if (selectOnFocus) {
|
|
291
|
+
manager.replaceSelection(key);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
};
|
|
294
295
|
// If the user hasn't yet interacted with the collection, there will be no focusedKey set.
|
|
295
296
|
// Attempt to detect whether the user is tabbing forward or backward into the collection
|
|
296
297
|
// and either focus the first or last item accordingly.
|
|
297
298
|
let relatedTarget = e.relatedTarget as Element;
|
|
298
299
|
if (relatedTarget && (e.currentTarget.compareDocumentPosition(relatedTarget) & Node.DOCUMENT_POSITION_FOLLOWING)) {
|
|
299
|
-
|
|
300
|
+
navigateToFirstKey(manager.lastSelectedKey ?? delegate.getLastKey());
|
|
300
301
|
} else {
|
|
301
|
-
|
|
302
|
+
navigateToFirstKey(manager.firstSelectedKey ?? delegate.getFirstKey());
|
|
302
303
|
}
|
|
303
304
|
} else if (!isVirtualized) {
|
|
304
305
|
// Restore the scroll position to what it was before.
|
package/src/useSelectableItem.ts
CHANGED
|
@@ -11,10 +11,12 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import {focusSafely} from '@react-aria/focus';
|
|
14
|
-
import {HTMLAttributes, Key, RefObject, useEffect} from 'react';
|
|
14
|
+
import {HTMLAttributes, Key, RefObject, useEffect, useRef} from 'react';
|
|
15
|
+
import {isCtrlKeyPressed, isNonContiguousSelectionModifier} from './utils';
|
|
16
|
+
import {LongPressEvent, PressEvent} from '@react-types/shared';
|
|
17
|
+
import {mergeProps} from '@react-aria/utils';
|
|
15
18
|
import {MultipleSelectionManager} from '@react-stately/selection';
|
|
16
|
-
import {
|
|
17
|
-
import {PressProps} from '@react-aria/interactions';
|
|
19
|
+
import {PressProps, useLongPress, usePress} from '@react-aria/interactions';
|
|
18
20
|
|
|
19
21
|
interface SelectableItemOptions {
|
|
20
22
|
/**
|
|
@@ -45,14 +47,23 @@ interface SelectableItemOptions {
|
|
|
45
47
|
/**
|
|
46
48
|
* Whether the option should use virtual focus instead of being focused directly.
|
|
47
49
|
*/
|
|
48
|
-
shouldUseVirtualFocus?: boolean
|
|
50
|
+
shouldUseVirtualFocus?: boolean,
|
|
51
|
+
/** Whether the item is disabled. */
|
|
52
|
+
isDisabled?: boolean,
|
|
53
|
+
/**
|
|
54
|
+
* Handler that is called when a user performs an action on the cell. The exact user event depends on
|
|
55
|
+
* the collection's `selectionBehavior` prop and the interaction modality.
|
|
56
|
+
*/
|
|
57
|
+
onAction?: () => void
|
|
49
58
|
}
|
|
50
59
|
|
|
51
60
|
interface SelectableItemAria {
|
|
52
61
|
/**
|
|
53
62
|
* Props to be spread on the item root node.
|
|
54
63
|
*/
|
|
55
|
-
itemProps: HTMLAttributes<HTMLElement
|
|
64
|
+
itemProps: HTMLAttributes<HTMLElement>,
|
|
65
|
+
/** Whether the item is currently in a pressed state. */
|
|
66
|
+
isPressed: boolean
|
|
56
67
|
}
|
|
57
68
|
|
|
58
69
|
/**
|
|
@@ -66,10 +77,35 @@ export function useSelectableItem(options: SelectableItemOptions): SelectableIte
|
|
|
66
77
|
shouldSelectOnPressUp,
|
|
67
78
|
isVirtualized,
|
|
68
79
|
shouldUseVirtualFocus,
|
|
69
|
-
focus
|
|
80
|
+
focus,
|
|
81
|
+
isDisabled,
|
|
82
|
+
onAction
|
|
70
83
|
} = options;
|
|
71
84
|
|
|
72
|
-
let onSelect = (e: PressEvent | PointerEvent) =>
|
|
85
|
+
let onSelect = (e: PressEvent | LongPressEvent | PointerEvent) => {
|
|
86
|
+
if (e.pointerType === 'keyboard' && isNonContiguousSelectionModifier(e)) {
|
|
87
|
+
manager.toggleSelection(key);
|
|
88
|
+
} else {
|
|
89
|
+
if (manager.selectionMode === 'none') {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (manager.selectionMode === 'single') {
|
|
94
|
+
if (manager.isSelected(key) && !manager.disallowEmptySelection) {
|
|
95
|
+
manager.toggleSelection(key);
|
|
96
|
+
} else {
|
|
97
|
+
manager.replaceSelection(key);
|
|
98
|
+
}
|
|
99
|
+
} else if (e && e.shiftKey) {
|
|
100
|
+
manager.extendSelection(key);
|
|
101
|
+
} else if (manager.selectionBehavior === 'toggle' || (e && (isCtrlKeyPressed(e) || e.pointerType === 'touch' || e.pointerType === 'virtual'))) {
|
|
102
|
+
// if touch or virtual (VO) then we just want to toggle, otherwise it's impossible to multi select because they don't have modifier keys
|
|
103
|
+
manager.toggleSelection(key);
|
|
104
|
+
} else {
|
|
105
|
+
manager.replaceSelection(key);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
};
|
|
73
109
|
|
|
74
110
|
// Focus the associated DOM node when this item becomes the focusedKey
|
|
75
111
|
let isFocused = key === manager.focusedKey;
|
|
@@ -98,6 +134,11 @@ export function useSelectableItem(options: SelectableItemOptions): SelectableIte
|
|
|
98
134
|
};
|
|
99
135
|
}
|
|
100
136
|
|
|
137
|
+
let modality = useRef(null);
|
|
138
|
+
let hasPrimaryAction = onAction && manager.selectionMode === 'none';
|
|
139
|
+
let hasSecondaryAction = onAction && manager.selectionMode !== 'none' && manager.selectionBehavior === 'replace';
|
|
140
|
+
let allowsSelection = !isDisabled && manager.canSelectItem(key);
|
|
141
|
+
|
|
101
142
|
// By default, selection occurs on pointer down. This can be strange if selecting an
|
|
102
143
|
// item causes the UI to disappear immediately (e.g. menus).
|
|
103
144
|
// If shouldSelectOnPressUp is true, we use onPressUp instead of onPressStart.
|
|
@@ -105,29 +146,40 @@ export function useSelectableItem(options: SelectableItemOptions): SelectableIte
|
|
|
105
146
|
// we want to be able to have the pointer down on the trigger that opens the menu and
|
|
106
147
|
// the pointer up on the menu item rather than requiring a separate press.
|
|
107
148
|
// For keyboard events, selection still occurs on key down.
|
|
149
|
+
let itemPressProps: PressProps = {};
|
|
108
150
|
if (shouldSelectOnPressUp) {
|
|
109
|
-
|
|
151
|
+
itemPressProps.onPressStart = (e) => {
|
|
152
|
+
modality.current = e.pointerType;
|
|
110
153
|
if (e.pointerType === 'keyboard') {
|
|
111
154
|
onSelect(e);
|
|
112
155
|
}
|
|
113
156
|
};
|
|
114
157
|
|
|
115
|
-
|
|
158
|
+
itemPressProps.onPressUp = (e) => {
|
|
116
159
|
if (e.pointerType !== 'keyboard') {
|
|
117
160
|
onSelect(e);
|
|
118
161
|
}
|
|
119
162
|
};
|
|
163
|
+
|
|
164
|
+
itemPressProps.onPress = hasPrimaryAction ? () => onAction() : null;
|
|
120
165
|
} else {
|
|
121
166
|
// On touch, it feels strange to select on touch down, so we special case this.
|
|
122
|
-
|
|
123
|
-
|
|
167
|
+
itemPressProps.onPressStart = (e) => {
|
|
168
|
+
modality.current = e.pointerType;
|
|
169
|
+
if (e.pointerType !== 'touch' && e.pointerType !== 'virtual') {
|
|
124
170
|
onSelect(e);
|
|
125
171
|
}
|
|
126
172
|
};
|
|
127
173
|
|
|
128
|
-
|
|
129
|
-
if (e.pointerType === 'touch') {
|
|
130
|
-
|
|
174
|
+
itemPressProps.onPress = (e) => {
|
|
175
|
+
if (e.pointerType === 'touch' || e.pointerType === 'virtual' || hasPrimaryAction) {
|
|
176
|
+
// Single tap on touch with selectionBehavior = 'replace' performs an action, i.e. navigation.
|
|
177
|
+
// Also perform action on press up when selectionMode = 'none'.
|
|
178
|
+
if (hasPrimaryAction || hasSecondaryAction) {
|
|
179
|
+
onAction();
|
|
180
|
+
} else {
|
|
181
|
+
onSelect(e);
|
|
182
|
+
}
|
|
131
183
|
}
|
|
132
184
|
};
|
|
133
185
|
}
|
|
@@ -136,7 +188,46 @@ export function useSelectableItem(options: SelectableItemOptions): SelectableIte
|
|
|
136
188
|
itemProps['data-key'] = key;
|
|
137
189
|
}
|
|
138
190
|
|
|
191
|
+
itemPressProps.preventFocusOnPress = shouldUseVirtualFocus;
|
|
192
|
+
let {pressProps, isPressed} = usePress(itemPressProps);
|
|
193
|
+
|
|
194
|
+
// Double clicking with a mouse with selectionBehavior = 'replace' performs an action.
|
|
195
|
+
let onDoubleClick = hasSecondaryAction ? (e) => {
|
|
196
|
+
if (modality.current === 'mouse') {
|
|
197
|
+
e.stopPropagation();
|
|
198
|
+
e.preventDefault();
|
|
199
|
+
onAction();
|
|
200
|
+
}
|
|
201
|
+
} : undefined;
|
|
202
|
+
|
|
203
|
+
// Long pressing an item with touch when selectionBehavior = 'replace' switches the selection behavior
|
|
204
|
+
// to 'toggle'. This changes the single tap behavior from performing an action (i.e. navigating) to
|
|
205
|
+
// selecting, and may toggle the appearance of a UI affordance like checkboxes on each item.
|
|
206
|
+
// TODO: what about when drag and drop is also enabled??
|
|
207
|
+
let {longPressProps} = useLongPress({
|
|
208
|
+
isDisabled: !hasSecondaryAction,
|
|
209
|
+
onLongPress(e) {
|
|
210
|
+
if (e.pointerType === 'touch') {
|
|
211
|
+
onSelect(e);
|
|
212
|
+
manager.setSelectionBehavior('toggle');
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// Pressing the Enter key with selectionBehavior = 'replace' performs an action (i.e. navigation).
|
|
218
|
+
let onKeyUp = hasSecondaryAction ? (e: KeyboardEvent) => {
|
|
219
|
+
if (e.key === 'Enter') {
|
|
220
|
+
onAction();
|
|
221
|
+
}
|
|
222
|
+
} : undefined;
|
|
223
|
+
|
|
139
224
|
return {
|
|
140
|
-
itemProps
|
|
225
|
+
itemProps: mergeProps(
|
|
226
|
+
itemProps,
|
|
227
|
+
allowsSelection || hasPrimaryAction ? pressProps : {},
|
|
228
|
+
hasSecondaryAction ? longPressProps : {},
|
|
229
|
+
{onKeyUp, onDoubleClick}
|
|
230
|
+
),
|
|
231
|
+
isPressed
|
|
141
232
|
};
|
|
142
233
|
}
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2020 Adobe. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import {isAppleDevice} from '@react-aria/utils';
|
|
14
|
+
import {isMac} from '@react-aria/utils';
|
|
15
|
+
|
|
16
|
+
interface Event {
|
|
17
|
+
altKey: boolean,
|
|
18
|
+
ctrlKey: boolean,
|
|
19
|
+
metaKey: boolean
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function isNonContiguousSelectionModifier(e: Event) {
|
|
23
|
+
// Ctrl + Arrow Up/Arrow Down has a system wide meaning on macOS, so use Alt instead.
|
|
24
|
+
// On Windows and Ubuntu, Alt + Space has a system wide meaning.
|
|
25
|
+
return isAppleDevice() ? e.altKey : e.ctrlKey;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function isCtrlKeyPressed(e: Event) {
|
|
29
|
+
if (isMac()) {
|
|
30
|
+
return e.metaKey;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return e.ctrlKey;
|
|
34
|
+
}
|