@react-spectrum/list 3.0.0-alpha.10 → 3.0.0-alpha.11

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.
@@ -0,0 +1,42 @@
1
+ import {classNames} from '@react-spectrum/utils';
2
+ import {ItemDropTarget} from '@react-types/shared';
3
+ import listStyles from './styles.css';
4
+ import {ListViewContext} from './ListView';
5
+ import React, {useContext, useRef} from 'react';
6
+ import {useVisuallyHidden} from '@react-aria/visually-hidden';
7
+
8
+ interface InsertionIndicatorProps {
9
+ target: ItemDropTarget,
10
+ isPresentationOnly?: boolean
11
+ }
12
+
13
+ export default function InsertionIndicator(props: InsertionIndicatorProps) {
14
+ let {dropState, dropHooks} = useContext(ListViewContext);
15
+ const {target, isPresentationOnly} = props;
16
+
17
+ let ref = useRef();
18
+ let {dropIndicatorProps} = dropHooks.useDropIndicator(props, dropState, ref);
19
+ let {visuallyHiddenProps} = useVisuallyHidden();
20
+
21
+ let isDropTarget = dropState.isDropTarget(target);
22
+
23
+ return (
24
+ <div role="row" aria-hidden={dropIndicatorProps['aria-hidden']}>
25
+ <div
26
+ role="gridcell"
27
+ aria-selected="false"
28
+ className={
29
+ classNames(
30
+ listStyles,
31
+ 'react-spectrum-ListViewInsertionIndicator',
32
+ {
33
+ 'react-spectrum-ListViewInsertionIndicator--dropTarget': isDropTarget
34
+ }
35
+ )}>
36
+ {!isPresentationOnly &&
37
+ <div {...visuallyHiddenProps} role="button" {...dropIndicatorProps} ref={ref} />
38
+ }
39
+ </div>
40
+ </div>
41
+ );
42
+ }
package/src/ListView.tsx CHANGED
@@ -9,42 +9,42 @@
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 type {DraggableCollectionState} from '@react-stately/dnd';
25
- import {DragHooks} from '@react-spectrum/dnd';
26
- import {DragPreview} from './DragPreview';
27
- import {GridCollection, GridState, useGridState} from '@react-stately/grid';
28
- 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';
29
19
  // @ts-ignore
30
20
  import intlMessages from '../intl/*.json';
31
21
  import {ListLayout} from '@react-stately/layout';
32
22
  import {ListState, useListState} from '@react-stately/list';
33
- import listStyles from './listview.css';
23
+ import listStyles from './styles.css';
34
24
  import {ListViewItem} from './ListViewItem';
25
+ import {mergeProps} from '@react-aria/utils';
35
26
  import {ProgressCircle} from '@react-spectrum/progress';
36
- import React, {ReactElement, useContext, useMemo, useRef} from 'react';
37
- 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';
38
34
  import {useProvider} from '@react-spectrum/provider';
39
35
  import {Virtualizer} from '@react-aria/virtualizer';
40
36
 
41
37
  interface ListViewContextValue<T> {
42
- state: GridState<T, GridCollection<any>>,
43
- keyboardDelegate: GridKeyboardDelegate<T, GridCollection<any>>,
38
+ state: ListState<T>,
44
39
  dragState: DraggableCollectionState,
45
- onAction:(key: string) => void,
40
+ dropState: DroppableCollectionState,
41
+ dragHooks: DragHooks,
42
+ dropHooks: DropHooks,
43
+ onAction:(key: Key) => void,
46
44
  isListDraggable: boolean,
47
- layout: ListLayout<T>
45
+ isListDroppable: boolean,
46
+ layout: ListLayout<T>,
47
+ loadingState: LoadingState
48
48
  }
49
49
 
50
50
  export const ListViewContext = React.createContext<ListViewContextValue<unknown>>(null);
@@ -64,7 +64,7 @@ const ROW_HEIGHTS = {
64
64
  }
65
65
  };
66
66
 
67
- export function useListLayout<T>(state: ListState<T>, density: ListViewProps<T>['density']) {
67
+ function useListLayout<T>(state: ListState<T>, density: SpectrumListProps<T>['density'], allowDisabledKeyFocus: boolean) {
68
68
  let {scale} = useProvider();
69
69
  let collator = useCollator({usage: 'search', sensitivity: 'base'});
70
70
  let isEmpty = state.collection.size === 0;
@@ -73,136 +73,155 @@ export function useListLayout<T>(state: ListState<T>, density: ListViewProps<T>[
73
73
  estimatedRowHeight: ROW_HEIGHTS[density][scale],
74
74
  padding: 0,
75
75
  collator,
76
- loaderHeight: isEmpty ? null : ROW_HEIGHTS[density][scale]
76
+ loaderHeight: isEmpty ? null : ROW_HEIGHTS[density][scale],
77
+ allowDisabledKeyFocus
77
78
  })
78
- , [collator, scale, density, isEmpty]);
79
+ , [collator, scale, density, isEmpty, allowDisabledKeyFocus]);
79
80
 
80
81
  layout.collection = state.collection;
81
82
  layout.disabledKeys = state.disabledKeys;
82
83
  return layout;
83
84
  }
84
85
 
85
- interface ListViewProps<T> extends CollectionBase<T>, DOMProps, AriaLabelingProps, StyleProps, MultipleSelection, SpectrumSelectionProps, Omit<AsyncLoadable, 'isLoading'> {
86
- /**
87
- * Sets the amount of vertical padding within each cell.
88
- * @default 'regular'
89
- */
90
- density?: 'compact' | 'regular' | 'spacious',
91
- /** Whether the ListView should be displayed with a quiet style. */
92
- isQuiet?: boolean,
93
- /** The current loading state of the ListView. Determines whether or not the progress circle should be shown. */
94
- loadingState?: LoadingState,
95
- /** Sets what the ListView should render when there is no content to display. */
96
- renderEmptyState?: () => JSX.Element,
97
- /**
98
- * The duration of animated layout changes, in milliseconds. Used by the Virtualizer.
99
- * @default 0
100
- */
101
- transitionDuration?: number,
102
- /**
103
- * Handler that is called when a user performs an action on an item. The exact user event depends on
104
- * the collection's `selectionBehavior` prop and the interaction modality.
105
- */
106
- onAction?: (key: string) => void,
107
- /**
108
- * The drag hooks returned by `useDragHooks` used to enable drag and drop behavior for the ListView. See the
109
- * [docs](https://react-spectrum.adobe.com/react-spectrum/useDragHooks.html) for more info.
110
- */
111
- dragHooks?: DragHooks
112
- }
113
-
114
- function ListView<T extends object>(props: ListViewProps<T>, ref: DOMRef<HTMLDivElement>) {
86
+ function ListView<T extends object>(props: SpectrumListProps<T>, ref: DOMRef<HTMLDivElement>) {
115
87
  let {
116
88
  density = 'regular',
117
89
  onLoadMore,
118
90
  loadingState,
119
91
  isQuiet,
120
- transitionDuration = 0,
92
+ overflowMode = 'truncate',
121
93
  onAction,
122
- dragHooks
94
+ dragHooks,
95
+ dropHooks,
96
+ ...otherProps
123
97
  } = props;
124
98
  let isListDraggable = !!dragHooks;
99
+ let isListDroppable = !!dropHooks;
125
100
  let dragHooksProvided = useRef(isListDraggable);
101
+ let dropHooksProvided = useRef(isListDroppable);
126
102
  if (dragHooksProvided.current !== isListDraggable) {
127
103
  console.warn('Drag hooks were provided during one render, but not another. This should be avoided as it may produce unexpected behavior.');
128
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
+ }
129
108
  let domRef = useDOMRef(ref);
130
- let {collection} = useListState(props);
131
- let formatMessage = useMessageFormatter(intlMessages);
132
- let isLoading = loadingState === 'loading' || loadingState === 'loadingMore';
133
-
134
- let {styleProps} = useStyleProps(props);
135
- let {direction, locale} = useLocale();
136
- let collator = useCollator({usage: 'search', sensitivity: 'base'});
137
- let gridCollection = useMemo(() => new GridCollection({
138
- columnCount: 1,
139
- items: [...collection].map(item => ({
140
- ...item,
141
- hasChildNodes: true,
142
- childNodes: [{
143
- key: `cell-${item.key}`,
144
- type: 'cell',
145
- index: 0,
146
- value: null,
147
- level: 0,
148
- rendered: null,
149
- textValue: item.textValue,
150
- hasChildNodes: false,
151
- childNodes: []
152
- }]
153
- }))
154
- }), [collection]);
155
- let state = useGridState({
109
+ let state = useListState({
156
110
  ...props,
157
- collection: gridCollection,
158
- focusMode: 'cell',
159
111
  selectionBehavior: props.selectionStyle === 'highlight' ? 'replace' : 'toggle'
160
112
  });
161
- let layout = useListLayout(state, props.density || 'regular');
162
- let keyboardDelegate = useMemo(() => new GridKeyboardDelegate({
163
- collection: state.collection,
164
- disabledKeys: state.disabledKeys,
165
- ref: domRef,
166
- direction,
167
- collator,
168
- // Focus the ListView cell instead of the row so that focus doesn't change with left/right arrow keys when there aren't any
169
- // focusable children in the cell.
170
- focusMode: 'cell'
171
- }), [state, domRef, direction, collator]);
113
+ let {collection, selectionManager} = state;
114
+ let formatMessage = useMessageFormatter(intlMessages);
115
+ let isLoading = loadingState === 'loading' || loadingState === 'loadingMore';
172
116
 
173
- let provider = useProvider();
117
+ let {styleProps} = useStyleProps(props);
118
+ let layout = useListLayout(state, props.density || 'regular', state.selectionManager.disabledBehavior === 'selection');
174
119
  let dragState: DraggableCollectionState;
120
+ let preview = useRef(null);
175
121
  if (isListDraggable) {
176
122
  dragState = dragHooks.useDraggableCollectionState({
177
- collection: state.collection,
178
- selectionManager: state.selectionManager,
179
- renderPreview(draggingKeys, draggedKey) {
180
- let item = state.collection.getItem(draggedKey);
181
- let itemCount = draggingKeys.size;
182
- let itemHeight = layout.getLayoutInfo(draggedKey).rect.height;
183
- return <DragPreview item={item} itemCount={itemCount} itemHeight={itemHeight} provider={provider} locale={locale} />;
184
- }
123
+ collection,
124
+ selectionManager,
125
+ preview
185
126
  });
186
127
  }
187
128
 
188
- let {gridProps} = useGrid({
129
+ let DragPreview = dragHooks?.DragPreview;
130
+ let dropState: DroppableCollectionState;
131
+ let droppableCollection: DroppableCollectionResult;
132
+ let isRootDropTarget: boolean;
133
+ if (isListDroppable) {
134
+ dropState = dropHooks.useDroppableCollectionState({
135
+ collection,
136
+ selectionManager
137
+ });
138
+ droppableCollection = dropHooks.useDroppableCollection({
139
+ keyboardDelegate: layout,
140
+ getDropTargetFromPoint(x, y) {
141
+ let closest = null;
142
+ let closestDistance = Infinity;
143
+ let closestDir = null;
144
+
145
+ x += domRef.current.scrollLeft;
146
+ y += domRef.current.scrollTop;
147
+
148
+ let visible = layout.getVisibleLayoutInfos(new Rect(x - 50, y - 50, x + 50, y + 50));
149
+
150
+ for (let layoutInfo of visible) {
151
+ let r = layoutInfo.rect;
152
+ let points: [number, number, string][] = [
153
+ [r.x, r.y + 4, 'before'],
154
+ [r.maxX, r.y + 4, 'before'],
155
+ [r.x, r.maxY - 8, 'after'],
156
+ [r.maxX, r.maxY - 8, 'after']
157
+ ];
158
+
159
+ for (let [px, py, dir] of points) {
160
+ let dx = px - x;
161
+ let dy = py - y;
162
+ let d = dx * dx + dy * dy;
163
+ if (d < closestDistance) {
164
+ closestDistance = d;
165
+ closest = layoutInfo;
166
+ closestDir = dir;
167
+ }
168
+ }
169
+
170
+ // TODO: Best way to implement only for when closest can be dropped on
171
+ // TODO: Figure out the typescript for this
172
+ // @ts-ignore
173
+ if (y >= r.y + 10 && y <= r.maxY - 10 && collection.getItem(closest.key).value.type === 'folder') {
174
+ closestDir = 'on';
175
+ }
176
+ }
177
+
178
+ let key = closest?.key;
179
+ if (key) {
180
+ return {
181
+ type: 'item',
182
+ key,
183
+ dropPosition: closestDir
184
+ };
185
+ }
186
+ }
187
+ }, dropState, domRef);
188
+
189
+ isRootDropTarget = dropState.isDropTarget({type: 'root'});
190
+ }
191
+
192
+ let {gridProps} = useList({
189
193
  ...props,
190
194
  isVirtualized: true,
191
- keyboardDelegate
195
+ keyboardDelegate: layout,
196
+ onAction
192
197
  }, state, domRef);
193
198
 
194
199
  // Sync loading state into the layout.
195
200
  layout.isLoading = isLoading;
196
201
 
197
- let focusedKey = state.selectionManager.focusedKey;
198
- let focusedItem = gridCollection.getItem(state.selectionManager.focusedKey);
199
- if (focusedItem?.parentKey != null) {
200
- focusedKey = focusedItem.parentKey;
202
+ let focusedKey = selectionManager.focusedKey;
203
+ if (dropState?.target?.type === 'item') {
204
+ focusedKey = dropState.target.key;
201
205
  }
202
206
 
207
+ // wait for layout to get accurate measurements
208
+ let [isVerticalScrollbarVisible, setVerticalScollbarVisible] = useState(false);
209
+ let [isHorizontalScrollbarVisible, setHorizontalScollbarVisible] = useState(false);
210
+ useLayoutEffect(() => {
211
+ if (domRef.current) {
212
+ // 2 is the width of the border which is not part of the box size
213
+ setVerticalScollbarVisible(domRef.current.clientWidth + 2 < domRef.current.offsetWidth);
214
+ setHorizontalScollbarVisible(domRef.current.clientHeight + 2 < domRef.current.offsetHeight);
215
+ }
216
+ });
217
+
218
+ let hasAnyChildren = useMemo(() => [...collection].some(item => item.hasChildNodes), [collection]);
219
+
203
220
  return (
204
- <ListViewContext.Provider value={{state, keyboardDelegate, dragState, onAction, isListDraggable, layout}}>
221
+ <ListViewContext.Provider value={{state, dragState, dropState, dragHooks, dropHooks, onAction, isListDraggable, isListDroppable, layout, loadingState}}>
205
222
  <Virtualizer
223
+ {...mergeProps(isListDroppable && droppableCollection?.collectionProps, gridProps)}
224
+ {...filterDOMProps(otherProps)}
206
225
  {...gridProps}
207
226
  {...styleProps}
208
227
  isLoading={isLoading}
@@ -218,26 +237,47 @@ function ListView<T extends object>(props: ListViewProps<T>, ref: DOMRef<HTMLDiv
218
237
  'react-spectrum-ListView--emphasized',
219
238
  {
220
239
  'react-spectrum-ListView--quiet': isQuiet,
221
- 'react-spectrum-ListView--draggable': isListDraggable,
222
- 'react-spectrum-ListView--loadingMore': loadingState === 'loadingMore'
240
+ 'react-spectrum-ListView--loadingMore': loadingState === 'loadingMore',
241
+ 'react-spectrum-ListView--draggable': !!isListDraggable,
242
+ 'react-spectrum-ListView--dropTarget': !!isRootDropTarget,
243
+ 'react-spectrum-ListView--isVerticalScrollbarVisible': isVerticalScrollbarVisible,
244
+ 'react-spectrum-ListView--isHorizontalScrollbarVisible': isHorizontalScrollbarVisible,
245
+ 'react-spectrum-ListView--hasAnyChildren': hasAnyChildren,
246
+ 'react-spectrum-ListView--wrap': overflowMode === 'wrap'
223
247
  },
224
248
  styleProps.className
225
249
  )
226
250
  }
227
251
  layout={layout}
228
- collection={gridCollection}
229
- transitionDuration={transitionDuration}>
252
+ collection={collection}
253
+ transitionDuration={isLoading ? 160 : 220}>
230
254
  {(type, item) => {
231
255
  if (type === 'item') {
232
256
  return (
233
- <ListViewItem item={item} isEmphasized dragHooks={dragHooks} />
257
+ <>
258
+ {isListDroppable && collection.getKeyBefore(item.key) == null &&
259
+ <RootDropIndicator key="root" />
260
+ }
261
+ {isListDroppable &&
262
+ <InsertionIndicator
263
+ key={`${item.key}-before`}
264
+ target={{key: item.key, type: 'item', dropPosition: 'before'}} />
265
+ }
266
+ <ListViewItem item={item} isEmphasized hasActions={!!onAction} />
267
+ {isListDroppable &&
268
+ <InsertionIndicator
269
+ key={`${item.key}-after`}
270
+ target={{key: item.key, type: 'item', dropPosition: 'after'}}
271
+ isPresentationOnly={collection.getKeyAfter(item.key) !== null} />
272
+ }
273
+ </>
234
274
  );
235
275
  } else if (type === 'loader') {
236
276
  return (
237
277
  <CenteredWrapper>
238
278
  <ProgressCircle
239
279
  isIndeterminate
240
- aria-label={state.collection.size > 0 ? formatMessage('loadingMore') : formatMessage('loading')} />
280
+ aria-label={collection.size > 0 ? formatMessage('loadingMore') : formatMessage('loading')} />
241
281
  </CenteredWrapper>
242
282
  );
243
283
  } else if (type === 'placeholder') {
@@ -255,6 +295,16 @@ function ListView<T extends object>(props: ListViewProps<T>, ref: DOMRef<HTMLDiv
255
295
 
256
296
  }}
257
297
  </Virtualizer>
298
+ {DragPreview && isListDraggable &&
299
+ <DragPreview ref={preview}>
300
+ {() => {
301
+ let item = state.collection.getItem(dragState.draggedKey);
302
+ let itemCount = dragState.draggingKeys.size;
303
+ let itemHeight = layout.getLayoutInfo(dragState.draggedKey).rect.height;
304
+ return <SpectrumDragPreview item={item} itemCount={itemCount} itemHeight={itemHeight} />;
305
+ }}
306
+ </DragPreview>
307
+ }
258
308
  </ListViewContext.Provider>
259
309
  );
260
310
  }
@@ -283,5 +333,5 @@ function CenteredWrapper({children}) {
283
333
  /**
284
334
  * Lists display a linear collection of data. They allow users to quickly scan, sort, compare, and take action on large amounts of data.
285
335
  */
286
- const _ListView = React.forwardRef(ListView) as <T>(props: ListViewProps<T> & {ref?: DOMRef<HTMLDivElement>}) => ReactElement;
336
+ const _ListView = React.forwardRef(ListView) as <T>(props: SpectrumListProps<T> & {ref?: DOMRef<HTMLDivElement>}) => ReactElement;
287
337
  export {_ListView as ListView};