@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/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 {GridCollection, useGridState} from '@react-stately/grid';
25
- import {GridKeyboardDelegate, useGrid} from '@react-aria/grid';
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 './listview.css';
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 {useCollator, useLocale, useMessageFormatter} from '@react-aria/i18n';
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
- export function useListLayout<T>(state: ListState<T>, density: ListViewProps<T>['density']) {
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
- new ListLayout<T>({
62
- estimatedRowHeight: ROW_HEIGHTS[density][scale],
63
- padding: 0,
64
- collator,
65
- loaderHeight: isEmpty ? null : ROW_HEIGHTS[density][scale]
66
- })
67
- , [collator, scale, density, isEmpty]);
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
- interface ListViewProps<T> extends CollectionBase<T>, DOMProps, AriaLabelingProps, StyleProps, MultipleSelection, SpectrumSelectionProps, Omit<AsyncLoadable, 'isLoading'> {
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
- transitionDuration = 0,
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 {collection} = useListState(props);
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 {direction} = useLocale();
103
- let collator = useCollator({usage: 'search', sensitivity: 'base'});
104
- let gridCollection = useMemo(() => new GridCollection({
105
- columnCount: 1,
106
- items: [...collection].map(item => ({
107
- ...item,
108
- hasChildNodes: true,
109
- childNodes: [{
110
- key: `cell-${item.key}`,
111
- type: 'cell',
112
- index: 0,
113
- value: null,
114
- level: 0,
115
- rendered: null,
116
- textValue: item.textValue,
117
- hasChildNodes: false,
118
- childNodes: []
119
- }]
120
- }))
121
- }), [collection]);
122
- let state = useGridState({
123
- ...props,
124
- collection: gridCollection,
125
- focusMode: 'cell',
126
- selectionBehavior: props.selectionStyle === 'highlight' ? 'replace' : 'toggle'
127
- });
128
- let layout = useListLayout(state, props.density || 'regular');
129
- let keyboardDelegate = useMemo(() => new GridKeyboardDelegate({
130
- collection: state.collection,
131
- disabledKeys: state.disabledKeys,
132
- ref: domRef,
133
- direction,
134
- collator,
135
- // Focus the ListView cell instead of the row so that focus doesn't change with left/right arrow keys when there aren't any
136
- // focusable children in the cell.
137
- focusMode: 'cell'
138
- }), [state, domRef, direction, collator]);
139
- let {gridProps} = useGrid({
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 = state.selectionManager.focusedKey;
149
- let focusedItem = gridCollection.getItem(state.selectionManager.focusedKey);
150
- if (focusedItem?.parentKey != null) {
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, keyboardDelegate}}>
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={gridCollection}
178
- transitionDuration={transitionDuration}>
259
+ collection={collection}
260
+ transitionDuration={isLoading ? 160 : 220}>
179
261
  {(type, item) => {
180
262
  if (type === 'item') {
181
263
  return (
182
- <ListViewItem item={item} onAction={onAction} isEmphasized />
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={state.collection.size > 0 ? formatMessage('loadingMore') : formatMessage('loading')} />
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
- const _ListView = React.forwardRef(ListView) as <T>(props: ListViewProps<T> & {ref?: DOMRef<HTMLDivElement>}) => ReactElement;
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};