@react-spectrum/list 3.0.0-alpha.9 → 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,46 +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
- import {Checkbox} from '@react-spectrum/checkbox';
24
- import {classNames, SlotProvider, useDOMRef, useStyleProps} from '@react-spectrum/utils';
25
- import {Content} from '@react-spectrum/view';
26
- import type {DraggableCollectionState} from '@react-stately/dnd';
27
- import {DragHooks} from '@react-spectrum/dnd';
28
- import {GridCollection, GridState, useGridState} from '@react-stately/grid';
29
- import {GridKeyboardDelegate, useGrid, useGridSelectionCheckbox} from '@react-aria/grid';
12
+ import {classNames, useDOMRef, useStyleProps} from '@react-spectrum/utils';
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';
30
19
  // @ts-ignore
31
20
  import intlMessages from '../intl/*.json';
32
- import ListGripper from '@spectrum-icons/ui/ListGripper';
33
21
  import {ListLayout} from '@react-stately/layout';
34
22
  import {ListState, useListState} from '@react-stately/list';
35
- import listStyles from './listview.css';
23
+ import listStyles from './styles.css';
36
24
  import {ListViewItem} from './ListViewItem';
25
+ import {mergeProps} from '@react-aria/utils';
37
26
  import {ProgressCircle} from '@react-spectrum/progress';
38
- import {Provider, useProvider} from '@react-spectrum/provider';
39
- import React, {ReactElement, useContext, useMemo, useRef} from 'react';
40
- 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';
34
+ import {useProvider} from '@react-spectrum/provider';
41
35
  import {Virtualizer} from '@react-aria/virtualizer';
42
36
 
43
- interface ListViewContextValue {
44
- state: GridState<object, GridCollection<any>>,
45
- keyboardDelegate: GridKeyboardDelegate<unknown, GridCollection<any>>,
37
+ interface ListViewContextValue<T> {
38
+ state: ListState<T>,
46
39
  dragState: DraggableCollectionState,
47
- onAction:(key: string) => void,
48
- isListDraggable: boolean
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
49
48
  }
50
49
 
51
- export const ListViewContext = React.createContext<ListViewContextValue>(null);
50
+ export const ListViewContext = React.createContext<ListViewContextValue<unknown>>(null);
52
51
 
53
52
  const ROW_HEIGHTS = {
54
53
  compact: {
@@ -65,7 +64,7 @@ const ROW_HEIGHTS = {
65
64
  }
66
65
  };
67
66
 
68
- 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']) {
69
68
  let {scale} = useProvider();
70
69
  let collator = useCollator({usage: 'search', sensitivity: 'base'});
71
70
  let isEmpty = state.collection.size === 0;
@@ -76,159 +75,160 @@ export function useListLayout<T>(state: ListState<T>, density: ListViewProps<T>[
76
75
  collator,
77
76
  loaderHeight: isEmpty ? null : ROW_HEIGHTS[density][scale]
78
77
  })
79
- , [collator, scale, density, isEmpty]);
78
+ // eslint-disable-next-line react-hooks/exhaustive-deps
79
+ , [collator, scale, density, isEmpty, overflowMode]);
80
80
 
81
81
  layout.collection = state.collection;
82
82
  layout.disabledKeys = state.disabledKeys;
83
83
  return layout;
84
84
  }
85
85
 
86
- interface ListViewProps<T> extends CollectionBase<T>, DOMProps, AriaLabelingProps, StyleProps, MultipleSelection, SpectrumSelectionProps, Omit<AsyncLoadable, 'isLoading'> {
87
- /**
88
- * Sets the amount of vertical padding within each cell.
89
- * @default 'regular'
90
- */
91
- density?: 'compact' | 'regular' | 'spacious',
92
- isQuiet?: boolean,
93
- loadingState?: LoadingState,
94
- renderEmptyState?: () => JSX.Element,
95
- transitionDuration?: number,
96
- onAction?: (key: string) => void,
97
- dragHooks?: DragHooks
98
- }
99
-
100
- function ListView<T extends object>(props: ListViewProps<T>, ref: DOMRef<HTMLDivElement>) {
86
+ function ListView<T extends object>(props: SpectrumListProps<T>, ref: DOMRef<HTMLDivElement>) {
101
87
  let {
102
88
  density = 'regular',
103
89
  onLoadMore,
104
90
  loadingState,
105
91
  isQuiet,
106
- transitionDuration = 0,
92
+ overflowMode = 'truncate',
107
93
  onAction,
108
- dragHooks
94
+ dragHooks,
95
+ dropHooks,
96
+ ...otherProps
109
97
  } = props;
110
98
  let isListDraggable = !!dragHooks;
99
+ let isListDroppable = !!dropHooks;
111
100
  let dragHooksProvided = useRef(isListDraggable);
101
+ let dropHooksProvided = useRef(isListDroppable);
112
102
  if (dragHooksProvided.current !== isListDraggable) {
113
103
  console.warn('Drag hooks were provided during one render, but not another. This should be avoided as it may produce unexpected behavior.');
114
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
+ }
115
108
  let domRef = useDOMRef(ref);
116
- let {collection} = useListState(props);
117
- let formatMessage = useMessageFormatter(intlMessages);
118
- let isLoading = loadingState === 'loading' || loadingState === 'loadingMore';
119
-
120
- let {styleProps} = useStyleProps(props);
121
- let {direction} = useLocale();
122
- let collator = useCollator({usage: 'search', sensitivity: 'base'});
123
- let gridCollection = useMemo(() => new GridCollection({
124
- columnCount: 1,
125
- items: [...collection].map(item => ({
126
- ...item,
127
- hasChildNodes: true,
128
- childNodes: [{
129
- key: `cell-${item.key}`,
130
- type: 'cell',
131
- index: 0,
132
- value: null,
133
- level: 0,
134
- rendered: null,
135
- textValue: item.textValue,
136
- hasChildNodes: false,
137
- childNodes: []
138
- }]
139
- }))
140
- }), [collection]);
141
- let state = useGridState({
109
+ let state = useListState({
142
110
  ...props,
143
- collection: gridCollection,
144
- focusMode: 'cell',
145
111
  selectionBehavior: props.selectionStyle === 'highlight' ? 'replace' : 'toggle'
146
112
  });
147
- let layout = useListLayout(state, props.density || 'regular');
148
- let keyboardDelegate = useMemo(() => new GridKeyboardDelegate({
149
- collection: state.collection,
150
- disabledKeys: state.disabledKeys,
151
- ref: domRef,
152
- direction,
153
- collator,
154
- // Focus the ListView cell instead of the row so that focus doesn't change with left/right arrow keys when there aren't any
155
- // focusable children in the cell.
156
- focusMode: 'cell'
157
- }), [state, domRef, direction, collator]);
113
+ let {collection, selectionManager} = state;
114
+ let formatMessage = useMessageFormatter(intlMessages);
115
+ let isLoading = loadingState === 'loading' || loadingState === 'loadingMore';
158
116
 
159
- let provider = useProvider();
160
- let {checkboxProps} = useGridSelectionCheckbox({key: null}, state);
117
+ let {styleProps} = useStyleProps(props);
161
118
  let dragState: DraggableCollectionState;
119
+ let preview = useRef(null);
162
120
  if (isListDraggable) {
163
121
  dragState = dragHooks.useDraggableCollectionState({
164
- collection: state.collection,
165
- selectionManager: state.selectionManager,
166
- renderPreview(selectedKeys, draggedKey) {
167
- let item = state.collection.getItem(draggedKey);
168
- let itemWidth = domRef.current.offsetWidth;
169
- let showCheckbox = state.selectionManager.selectionMode !== 'none' && state.selectionManager.selectionBehavior === 'toggle';
170
- let isSelected = state.selectionManager.isSelected(item.key);
171
- return (
172
- <Provider
173
- {...provider}
174
- UNSAFE_className={classNames(listStyles, 'react-spectrum-ListViewItem', 'is-dragging')}
175
- UNSAFE_style={{width: itemWidth, paddingInlineStart: 0}}>
176
- <div className={listStyles['react-spectrum-ListViewItem-grid']}>
177
- <div className={listStyles['react-spectrum-ListViewItem-draghandle-container']}>
178
- <div className={listStyles['react-spectrum-ListViewItem-draghandle-button']}>
179
- <ListGripper />
180
- </div>
181
- </div>
182
- {showCheckbox &&
183
- <Checkbox
184
- isSelected={isSelected}
185
- UNSAFE_className={listStyles['react-spectrum-ListViewItem-checkbox']}
186
- isEmphasized
187
- aria-label={checkboxProps['aria-label']} />
188
- }
189
- <SlotProvider
190
- slots={{
191
- content: {UNSAFE_className: listStyles['react-spectrum-ListViewItem-content']},
192
- text: {UNSAFE_className: listStyles['react-spectrum-ListViewItem-content']},
193
- description: {UNSAFE_className: listStyles['react-spectrum-ListViewItem-description']},
194
- icon: {UNSAFE_className: listStyles['react-spectrum-ListViewItem-icon'], size: 'M'},
195
- image: {UNSAFE_className: listStyles['react-spectrum-ListViewItem-image']},
196
- link: {UNSAFE_className: listStyles['react-spectrum-ListViewItem-content'], isQuiet: true},
197
- actionButton: {UNSAFE_className: listStyles['react-spectrum-ListViewItem-actions'], isQuiet: true},
198
- actionGroup: {
199
- UNSAFE_className: listStyles['react-spectrum-ListViewItem-actions'],
200
- isQuiet: true,
201
- density: 'compact'
202
- },
203
- actionMenu: {UNSAFE_className: listStyles['react-spectrum-ListViewItem-actionmenu'], isQuiet: true}
204
- }}>
205
- {typeof item.rendered === 'string' ? <Content>{item.rendered}</Content> : item.rendered}
206
- </SlotProvider>
207
- </div>
208
- </Provider>
209
- );
210
- }
122
+ collection,
123
+ selectionManager,
124
+ preview
211
125
  });
212
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
+ }
213
198
 
214
- let {gridProps} = useGrid({
199
+ let {gridProps} = useList({
215
200
  ...props,
216
201
  isVirtualized: true,
217
- keyboardDelegate
202
+ keyboardDelegate: layout,
203
+ onAction
218
204
  }, state, domRef);
219
205
 
220
206
  // Sync loading state into the layout.
221
207
  layout.isLoading = isLoading;
222
208
 
223
- let focusedKey = state.selectionManager.focusedKey;
224
- let focusedItem = gridCollection.getItem(state.selectionManager.focusedKey);
225
- if (focusedItem?.parentKey != null) {
226
- focusedKey = focusedItem.parentKey;
209
+ let focusedKey = selectionManager.focusedKey;
210
+ if (dropState?.target?.type === 'item') {
211
+ focusedKey = dropState.target.key;
227
212
  }
228
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
+
229
227
  return (
230
- <ListViewContext.Provider value={{state, keyboardDelegate, dragState, onAction, isListDraggable}}>
228
+ <ListViewContext.Provider value={{state, dragState, dropState, dragHooks, dropHooks, onAction, isListDraggable, isListDroppable, layout, loadingState}}>
231
229
  <Virtualizer
230
+ {...mergeProps(isListDroppable && droppableCollection?.collectionProps, gridProps)}
231
+ {...filterDOMProps(otherProps)}
232
232
  {...gridProps}
233
233
  {...styleProps}
234
234
  isLoading={isLoading}
@@ -244,25 +244,47 @@ function ListView<T extends object>(props: ListViewProps<T>, ref: DOMRef<HTMLDiv
244
244
  'react-spectrum-ListView--emphasized',
245
245
  {
246
246
  'react-spectrum-ListView--quiet': isQuiet,
247
- 'react-spectrum-ListView--draggable': isListDraggable
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'
248
254
  },
249
255
  styleProps.className
250
256
  )
251
257
  }
252
258
  layout={layout}
253
- collection={gridCollection}
254
- transitionDuration={transitionDuration}>
259
+ collection={collection}
260
+ transitionDuration={isLoading ? 160 : 220}>
255
261
  {(type, item) => {
256
262
  if (type === 'item') {
257
263
  return (
258
- <ListViewItem item={item} isEmphasized dragHooks={dragHooks} />
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
+ </>
259
281
  );
260
282
  } else if (type === 'loader') {
261
283
  return (
262
284
  <CenteredWrapper>
263
285
  <ProgressCircle
264
286
  isIndeterminate
265
- aria-label={state.collection.size > 0 ? formatMessage('loadingMore') : formatMessage('loading')} />
287
+ aria-label={collection.size > 0 ? formatMessage('loadingMore') : formatMessage('loading')} />
266
288
  </CenteredWrapper>
267
289
  );
268
290
  } else if (type === 'placeholder') {
@@ -280,11 +302,20 @@ function ListView<T extends object>(props: ListViewProps<T>, ref: DOMRef<HTMLDiv
280
302
 
281
303
  }}
282
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
+ }
283
315
  </ListViewContext.Provider>
284
316
  );
285
317
  }
286
318
 
287
-
288
319
  function CenteredWrapper({children}) {
289
320
  let {state} = useContext(ListViewContext);
290
321
  return (
@@ -306,5 +337,8 @@ function CenteredWrapper({children}) {
306
337
  );
307
338
  }
308
339
 
309
- 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;
310
344
  export {_ListView as ListView};