@react-spectrum/list 3.0.0-alpha.7 → 3.0.0-beta.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.css +1 -1
- package/dist/main.js +767 -370
- package/dist/main.js.map +1 -1
- package/dist/module.js +765 -368
- package/dist/module.js.map +1 -1
- package/dist/types.d.ts +6 -14
- package/dist/types.d.ts.map +1 -1
- package/package.json +40 -30
- package/src/DragPreview.tsx +60 -0
- package/src/InsertionIndicator.tsx +46 -0
- package/src/ListView.tsx +203 -93
- package/src/ListViewItem.tsx +190 -53
- package/src/RootDropIndicator.tsx +28 -0
- package/src/styles.css +593 -0
- package/src/listview.css +0 -197
package/src/ListView.tsx
CHANGED
|
@@ -9,34 +9,45 @@
|
|
|
9
9
|
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
10
|
* governing permissions and limitations under the License.
|
|
11
11
|
*/
|
|
12
|
-
import {
|
|
13
|
-
AriaLabelingProps,
|
|
14
|
-
AsyncLoadable,
|
|
15
|
-
CollectionBase,
|
|
16
|
-
DOMProps,
|
|
17
|
-
DOMRef,
|
|
18
|
-
LoadingState,
|
|
19
|
-
MultipleSelection,
|
|
20
|
-
SpectrumSelectionProps,
|
|
21
|
-
StyleProps
|
|
22
|
-
} from '@react-types/shared';
|
|
23
12
|
import {classNames, useDOMRef, useStyleProps} from '@react-spectrum/utils';
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
13
|
+
import {DOMRef, LoadingState} from '@react-types/shared';
|
|
14
|
+
import type {DraggableCollectionState, DroppableCollectionState} from '@react-stately/dnd';
|
|
15
|
+
import {DragHooks, DropHooks} from '@react-spectrum/dnd';
|
|
16
|
+
import type {DroppableCollectionResult} from '@react-aria/dnd';
|
|
17
|
+
import {filterDOMProps, useLayoutEffect} from '@react-aria/utils';
|
|
18
|
+
import InsertionIndicator from './InsertionIndicator';
|
|
26
19
|
// @ts-ignore
|
|
27
20
|
import intlMessages from '../intl/*.json';
|
|
28
21
|
import {ListLayout} from '@react-stately/layout';
|
|
29
22
|
import {ListState, useListState} from '@react-stately/list';
|
|
30
|
-
import listStyles from './
|
|
23
|
+
import listStyles from './styles.css';
|
|
31
24
|
import {ListViewItem} from './ListViewItem';
|
|
25
|
+
import {mergeProps} from '@react-aria/utils';
|
|
32
26
|
import {ProgressCircle} from '@react-spectrum/progress';
|
|
33
|
-
import React, {ReactElement, useContext, useMemo} from 'react';
|
|
34
|
-
import {
|
|
27
|
+
import React, {Key, ReactElement, useContext, useMemo, useRef, useState} from 'react';
|
|
28
|
+
import {Rect} from '@react-stately/virtualizer';
|
|
29
|
+
import RootDropIndicator from './RootDropIndicator';
|
|
30
|
+
import {DragPreview as SpectrumDragPreview} from './DragPreview';
|
|
31
|
+
import {SpectrumListProps} from '@react-types/list';
|
|
32
|
+
import {useCollator, useMessageFormatter} from '@react-aria/i18n';
|
|
33
|
+
import {useList} from '@react-aria/list';
|
|
35
34
|
import {useProvider} from '@react-spectrum/provider';
|
|
36
35
|
import {Virtualizer} from '@react-aria/virtualizer';
|
|
37
36
|
|
|
37
|
+
interface ListViewContextValue<T> {
|
|
38
|
+
state: ListState<T>,
|
|
39
|
+
dragState: DraggableCollectionState,
|
|
40
|
+
dropState: DroppableCollectionState,
|
|
41
|
+
dragHooks: DragHooks,
|
|
42
|
+
dropHooks: DropHooks,
|
|
43
|
+
onAction:(key: Key) => void,
|
|
44
|
+
isListDraggable: boolean,
|
|
45
|
+
isListDroppable: boolean,
|
|
46
|
+
layout: ListLayout<T>,
|
|
47
|
+
loadingState: LoadingState
|
|
48
|
+
}
|
|
38
49
|
|
|
39
|
-
export const ListViewContext = React.createContext(null);
|
|
50
|
+
export const ListViewContext = React.createContext<ListViewContextValue<unknown>>(null);
|
|
40
51
|
|
|
41
52
|
const ROW_HEIGHTS = {
|
|
42
53
|
compact: {
|
|
@@ -53,107 +64,171 @@ const ROW_HEIGHTS = {
|
|
|
53
64
|
}
|
|
54
65
|
};
|
|
55
66
|
|
|
56
|
-
|
|
67
|
+
function useListLayout<T>(state: ListState<T>, density: SpectrumListProps<T>['density'], overflowMode: SpectrumListProps<T>['overflowMode']) {
|
|
57
68
|
let {scale} = useProvider();
|
|
58
69
|
let collator = useCollator({usage: 'search', sensitivity: 'base'});
|
|
59
70
|
let isEmpty = state.collection.size === 0;
|
|
60
71
|
let layout = useMemo(() =>
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
72
|
+
new ListLayout<T>({
|
|
73
|
+
estimatedRowHeight: ROW_HEIGHTS[density][scale],
|
|
74
|
+
padding: 0,
|
|
75
|
+
collator,
|
|
76
|
+
loaderHeight: isEmpty ? null : ROW_HEIGHTS[density][scale]
|
|
77
|
+
})
|
|
78
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
79
|
+
, [collator, scale, density, isEmpty, overflowMode]);
|
|
68
80
|
|
|
69
81
|
layout.collection = state.collection;
|
|
70
82
|
layout.disabledKeys = state.disabledKeys;
|
|
71
83
|
return layout;
|
|
72
84
|
}
|
|
73
85
|
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Sets the amount of vertical padding within each cell.
|
|
77
|
-
* @default 'regular'
|
|
78
|
-
*/
|
|
79
|
-
density?: 'compact' | 'regular' | 'spacious',
|
|
80
|
-
isQuiet?: boolean,
|
|
81
|
-
loadingState?: LoadingState,
|
|
82
|
-
renderEmptyState?: () => JSX.Element,
|
|
83
|
-
transitionDuration?: number,
|
|
84
|
-
onAction?: (key: string) => void
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function ListView<T extends object>(props: ListViewProps<T>, ref: DOMRef<HTMLDivElement>) {
|
|
86
|
+
function ListView<T extends object>(props: SpectrumListProps<T>, ref: DOMRef<HTMLDivElement>) {
|
|
88
87
|
let {
|
|
89
88
|
density = 'regular',
|
|
90
89
|
onLoadMore,
|
|
91
90
|
loadingState,
|
|
92
91
|
isQuiet,
|
|
93
|
-
|
|
94
|
-
onAction
|
|
92
|
+
overflowMode = 'truncate',
|
|
93
|
+
onAction,
|
|
94
|
+
dragHooks,
|
|
95
|
+
dropHooks,
|
|
96
|
+
...otherProps
|
|
95
97
|
} = props;
|
|
98
|
+
let isListDraggable = !!dragHooks;
|
|
99
|
+
let isListDroppable = !!dropHooks;
|
|
100
|
+
let dragHooksProvided = useRef(isListDraggable);
|
|
101
|
+
let dropHooksProvided = useRef(isListDroppable);
|
|
102
|
+
if (dragHooksProvided.current !== isListDraggable) {
|
|
103
|
+
console.warn('Drag hooks were provided during one render, but not another. This should be avoided as it may produce unexpected behavior.');
|
|
104
|
+
}
|
|
105
|
+
if (dropHooksProvided.current !== isListDroppable) {
|
|
106
|
+
console.warn('Drop hooks were provided during one render, but not another. This should be avoided as it may produce unexpected behavior.');
|
|
107
|
+
}
|
|
96
108
|
let domRef = useDOMRef(ref);
|
|
97
|
-
let
|
|
109
|
+
let state = useListState({
|
|
110
|
+
...props,
|
|
111
|
+
selectionBehavior: props.selectionStyle === 'highlight' ? 'replace' : 'toggle'
|
|
112
|
+
});
|
|
113
|
+
let {collection, selectionManager} = state;
|
|
98
114
|
let formatMessage = useMessageFormatter(intlMessages);
|
|
99
115
|
let isLoading = loadingState === 'loading' || loadingState === 'loadingMore';
|
|
100
116
|
|
|
101
117
|
let {styleProps} = useStyleProps(props);
|
|
102
|
-
let
|
|
103
|
-
let
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
let
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
118
|
+
let dragState: DraggableCollectionState;
|
|
119
|
+
let preview = useRef(null);
|
|
120
|
+
if (isListDraggable) {
|
|
121
|
+
dragState = dragHooks.useDraggableCollectionState({
|
|
122
|
+
collection,
|
|
123
|
+
selectionManager,
|
|
124
|
+
preview
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
let layout = useListLayout(
|
|
128
|
+
state,
|
|
129
|
+
props.density || 'regular',
|
|
130
|
+
overflowMode
|
|
131
|
+
);
|
|
132
|
+
// !!0 is false, so we can cast size or undefined and they'll be falsy
|
|
133
|
+
layout.allowDisabledKeyFocus = state.selectionManager.disabledBehavior === 'selection' || !!dragState?.draggingKeys.size;
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
let DragPreview = dragHooks?.DragPreview;
|
|
137
|
+
let dropState: DroppableCollectionState;
|
|
138
|
+
let droppableCollection: DroppableCollectionResult;
|
|
139
|
+
let isRootDropTarget: boolean;
|
|
140
|
+
if (isListDroppable) {
|
|
141
|
+
dropState = dropHooks.useDroppableCollectionState({
|
|
142
|
+
collection,
|
|
143
|
+
selectionManager
|
|
144
|
+
});
|
|
145
|
+
droppableCollection = dropHooks.useDroppableCollection({
|
|
146
|
+
keyboardDelegate: layout,
|
|
147
|
+
getDropTargetFromPoint(x, y) {
|
|
148
|
+
let closest = null;
|
|
149
|
+
let closestDistance = Infinity;
|
|
150
|
+
let closestDir = null;
|
|
151
|
+
|
|
152
|
+
x += domRef.current.scrollLeft;
|
|
153
|
+
y += domRef.current.scrollTop;
|
|
154
|
+
|
|
155
|
+
let visible = layout.getVisibleLayoutInfos(new Rect(x - 50, y - 50, x + 50, y + 50));
|
|
156
|
+
|
|
157
|
+
for (let layoutInfo of visible) {
|
|
158
|
+
let r = layoutInfo.rect;
|
|
159
|
+
let points: [number, number, string][] = [
|
|
160
|
+
[r.x, r.y + 4, 'before'],
|
|
161
|
+
[r.maxX, r.y + 4, 'before'],
|
|
162
|
+
[r.x, r.maxY - 8, 'after'],
|
|
163
|
+
[r.maxX, r.maxY - 8, 'after']
|
|
164
|
+
];
|
|
165
|
+
|
|
166
|
+
for (let [px, py, dir] of points) {
|
|
167
|
+
let dx = px - x;
|
|
168
|
+
let dy = py - y;
|
|
169
|
+
let d = dx * dx + dy * dy;
|
|
170
|
+
if (d < closestDistance) {
|
|
171
|
+
closestDistance = d;
|
|
172
|
+
closest = layoutInfo;
|
|
173
|
+
closestDir = dir;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// TODO: Best way to implement only for when closest can be dropped on
|
|
178
|
+
// TODO: Figure out the typescript for this
|
|
179
|
+
// @ts-ignore
|
|
180
|
+
if (y >= r.y + 10 && y <= r.maxY - 10 && collection.getItem(closest.key).value.type === 'folder') {
|
|
181
|
+
closestDir = 'on';
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
let key = closest?.key;
|
|
186
|
+
if (key) {
|
|
187
|
+
return {
|
|
188
|
+
type: 'item',
|
|
189
|
+
key,
|
|
190
|
+
dropPosition: closestDir
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}, dropState, domRef);
|
|
195
|
+
|
|
196
|
+
isRootDropTarget = dropState.isDropTarget({type: 'root'});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
let {gridProps} = useList({
|
|
140
200
|
...props,
|
|
141
201
|
isVirtualized: true,
|
|
142
|
-
keyboardDelegate
|
|
202
|
+
keyboardDelegate: layout,
|
|
203
|
+
onAction
|
|
143
204
|
}, state, domRef);
|
|
144
205
|
|
|
145
206
|
// Sync loading state into the layout.
|
|
146
207
|
layout.isLoading = isLoading;
|
|
147
208
|
|
|
148
|
-
let focusedKey =
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
focusedKey = focusedItem.parentKey;
|
|
209
|
+
let focusedKey = selectionManager.focusedKey;
|
|
210
|
+
if (dropState?.target?.type === 'item') {
|
|
211
|
+
focusedKey = dropState.target.key;
|
|
152
212
|
}
|
|
153
213
|
|
|
214
|
+
// wait for layout to get accurate measurements
|
|
215
|
+
let [isVerticalScrollbarVisible, setVerticalScollbarVisible] = useState(false);
|
|
216
|
+
let [isHorizontalScrollbarVisible, setHorizontalScollbarVisible] = useState(false);
|
|
217
|
+
useLayoutEffect(() => {
|
|
218
|
+
if (domRef.current) {
|
|
219
|
+
// 2 is the width of the border which is not part of the box size
|
|
220
|
+
setVerticalScollbarVisible(domRef.current.clientWidth + 2 < domRef.current.offsetWidth);
|
|
221
|
+
setHorizontalScollbarVisible(domRef.current.clientHeight + 2 < domRef.current.offsetHeight);
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
let hasAnyChildren = useMemo(() => [...collection].some(item => item.hasChildNodes), [collection]);
|
|
226
|
+
|
|
154
227
|
return (
|
|
155
|
-
<ListViewContext.Provider value={{state,
|
|
228
|
+
<ListViewContext.Provider value={{state, dragState, dropState, dragHooks, dropHooks, onAction, isListDraggable, isListDroppable, layout, loadingState}}>
|
|
156
229
|
<Virtualizer
|
|
230
|
+
{...mergeProps(isListDroppable && droppableCollection?.collectionProps, gridProps)}
|
|
231
|
+
{...filterDOMProps(otherProps)}
|
|
157
232
|
{...gridProps}
|
|
158
233
|
{...styleProps}
|
|
159
234
|
isLoading={isLoading}
|
|
@@ -168,25 +243,48 @@ function ListView<T extends object>(props: ListViewProps<T>, ref: DOMRef<HTMLDiv
|
|
|
168
243
|
`react-spectrum-ListView--${density}`,
|
|
169
244
|
'react-spectrum-ListView--emphasized',
|
|
170
245
|
{
|
|
171
|
-
'react-spectrum-ListView--quiet': isQuiet
|
|
246
|
+
'react-spectrum-ListView--quiet': isQuiet,
|
|
247
|
+
'react-spectrum-ListView--loadingMore': loadingState === 'loadingMore',
|
|
248
|
+
'react-spectrum-ListView--draggable': !!isListDraggable,
|
|
249
|
+
'react-spectrum-ListView--dropTarget': !!isRootDropTarget,
|
|
250
|
+
'react-spectrum-ListView--isVerticalScrollbarVisible': isVerticalScrollbarVisible,
|
|
251
|
+
'react-spectrum-ListView--isHorizontalScrollbarVisible': isHorizontalScrollbarVisible,
|
|
252
|
+
'react-spectrum-ListView--hasAnyChildren': hasAnyChildren,
|
|
253
|
+
'react-spectrum-ListView--wrap': overflowMode === 'wrap'
|
|
172
254
|
},
|
|
173
255
|
styleProps.className
|
|
174
256
|
)
|
|
175
257
|
}
|
|
176
258
|
layout={layout}
|
|
177
|
-
collection={
|
|
178
|
-
transitionDuration={
|
|
259
|
+
collection={collection}
|
|
260
|
+
transitionDuration={isLoading ? 160 : 220}>
|
|
179
261
|
{(type, item) => {
|
|
180
262
|
if (type === 'item') {
|
|
181
263
|
return (
|
|
182
|
-
|
|
264
|
+
<>
|
|
265
|
+
{isListDroppable && collection.getKeyBefore(item.key) == null &&
|
|
266
|
+
<RootDropIndicator key="root" />
|
|
267
|
+
}
|
|
268
|
+
{isListDroppable &&
|
|
269
|
+
<InsertionIndicator
|
|
270
|
+
key={`${item.key}-before`}
|
|
271
|
+
target={{key: item.key, type: 'item', dropPosition: 'before'}} />
|
|
272
|
+
}
|
|
273
|
+
<ListViewItem item={item} isEmphasized hasActions={!!onAction} />
|
|
274
|
+
{isListDroppable &&
|
|
275
|
+
<InsertionIndicator
|
|
276
|
+
key={`${item.key}-after`}
|
|
277
|
+
target={{key: item.key, type: 'item', dropPosition: 'after'}}
|
|
278
|
+
isPresentationOnly={collection.getKeyAfter(item.key) != null} />
|
|
279
|
+
}
|
|
280
|
+
</>
|
|
183
281
|
);
|
|
184
282
|
} else if (type === 'loader') {
|
|
185
283
|
return (
|
|
186
284
|
<CenteredWrapper>
|
|
187
285
|
<ProgressCircle
|
|
188
286
|
isIndeterminate
|
|
189
|
-
aria-label={
|
|
287
|
+
aria-label={collection.size > 0 ? formatMessage('loadingMore') : formatMessage('loading')} />
|
|
190
288
|
</CenteredWrapper>
|
|
191
289
|
);
|
|
192
290
|
} else if (type === 'placeholder') {
|
|
@@ -204,11 +302,20 @@ function ListView<T extends object>(props: ListViewProps<T>, ref: DOMRef<HTMLDiv
|
|
|
204
302
|
|
|
205
303
|
}}
|
|
206
304
|
</Virtualizer>
|
|
305
|
+
{DragPreview && isListDraggable &&
|
|
306
|
+
<DragPreview ref={preview}>
|
|
307
|
+
{() => {
|
|
308
|
+
let item = state.collection.getItem(dragState.draggedKey);
|
|
309
|
+
let itemCount = dragState.draggingKeys.size;
|
|
310
|
+
let itemHeight = layout.getLayoutInfo(dragState.draggedKey).rect.height;
|
|
311
|
+
return <SpectrumDragPreview item={item} itemCount={itemCount} itemHeight={itemHeight} />;
|
|
312
|
+
}}
|
|
313
|
+
</DragPreview>
|
|
314
|
+
}
|
|
207
315
|
</ListViewContext.Provider>
|
|
208
316
|
);
|
|
209
317
|
}
|
|
210
318
|
|
|
211
|
-
|
|
212
319
|
function CenteredWrapper({children}) {
|
|
213
320
|
let {state} = useContext(ListViewContext);
|
|
214
321
|
return (
|
|
@@ -230,5 +337,8 @@ function CenteredWrapper({children}) {
|
|
|
230
337
|
);
|
|
231
338
|
}
|
|
232
339
|
|
|
233
|
-
|
|
340
|
+
/**
|
|
341
|
+
* Lists display a linear collection of data. They allow users to quickly scan, sort, compare, and take action on large amounts of data.
|
|
342
|
+
*/
|
|
343
|
+
const _ListView = React.forwardRef(ListView) as <T>(props: SpectrumListProps<T> & {ref?: DOMRef<HTMLDivElement>}) => ReactElement;
|
|
234
344
|
export {_ListView as ListView};
|