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

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,9 +9,22 @@
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 {AriaLabelingProps, CollectionBase, DOMProps, DOMRef, StyleProps} from '@react-types/shared';
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';
13
23
  import {classNames, useDOMRef, useStyleProps} from '@react-spectrum/utils';
14
- import {GridCollection, useGridState} from '@react-stately/grid';
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';
15
28
  import {GridKeyboardDelegate, useGrid} from '@react-aria/grid';
16
29
  // @ts-ignore
17
30
  import intlMessages from '../intl/*.json';
@@ -20,71 +33,158 @@ import {ListState, useListState} from '@react-stately/list';
20
33
  import listStyles from './listview.css';
21
34
  import {ListViewItem} from './ListViewItem';
22
35
  import {ProgressCircle} from '@react-spectrum/progress';
23
- import React, {ReactElement, useContext, useMemo} from 'react';
36
+ import React, {ReactElement, useContext, useMemo, useRef} from 'react';
24
37
  import {useCollator, useLocale, useMessageFormatter} from '@react-aria/i18n';
25
38
  import {useProvider} from '@react-spectrum/provider';
26
39
  import {Virtualizer} from '@react-aria/virtualizer';
27
40
 
41
+ interface ListViewContextValue<T> {
42
+ state: GridState<T, GridCollection<any>>,
43
+ keyboardDelegate: GridKeyboardDelegate<T, GridCollection<any>>,
44
+ dragState: DraggableCollectionState,
45
+ onAction:(key: string) => void,
46
+ isListDraggable: boolean,
47
+ layout: ListLayout<T>
48
+ }
49
+
50
+ export const ListViewContext = React.createContext<ListViewContextValue<unknown>>(null);
28
51
 
29
- export const ListViewContext = React.createContext(null);
52
+ const ROW_HEIGHTS = {
53
+ compact: {
54
+ medium: 32,
55
+ large: 40
56
+ },
57
+ regular: {
58
+ medium: 40,
59
+ large: 50
60
+ },
61
+ spacious: {
62
+ medium: 48,
63
+ large: 60
64
+ }
65
+ };
30
66
 
31
- export function useListLayout<T>(state: ListState<T>) {
67
+ export function useListLayout<T>(state: ListState<T>, density: ListViewProps<T>['density']) {
32
68
  let {scale} = useProvider();
33
69
  let collator = useCollator({usage: 'search', sensitivity: 'base'});
70
+ let isEmpty = state.collection.size === 0;
34
71
  let layout = useMemo(() =>
35
- new ListLayout<T>({
36
- estimatedRowHeight: scale === 'large' ? 48 : 32,
37
- padding: 0,
38
- collator
39
- })
40
- , [collator, scale]);
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
+ , [collator, scale, density, isEmpty]);
41
79
 
42
80
  layout.collection = state.collection;
43
81
  layout.disabledKeys = state.disabledKeys;
44
82
  return layout;
45
83
  }
46
84
 
47
- interface ListViewProps<T> extends CollectionBase<T>, DOMProps, AriaLabelingProps, StyleProps {
48
- isLoading?: boolean,
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. */
49
96
  renderEmptyState?: () => JSX.Element,
50
- transitionDuration?: number
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
51
112
  }
52
113
 
53
114
  function ListView<T extends object>(props: ListViewProps<T>, ref: DOMRef<HTMLDivElement>) {
54
115
  let {
55
- transitionDuration = 0
116
+ density = 'regular',
117
+ onLoadMore,
118
+ loadingState,
119
+ isQuiet,
120
+ transitionDuration = 0,
121
+ onAction,
122
+ dragHooks
56
123
  } = props;
124
+ let isListDraggable = !!dragHooks;
125
+ let dragHooksProvided = useRef(isListDraggable);
126
+ if (dragHooksProvided.current !== isListDraggable) {
127
+ console.warn('Drag hooks were provided during one render, but not another. This should be avoided as it may produce unexpected behavior.');
128
+ }
57
129
  let domRef = useDOMRef(ref);
58
130
  let {collection} = useListState(props);
59
131
  let formatMessage = useMessageFormatter(intlMessages);
132
+ let isLoading = loadingState === 'loading' || loadingState === 'loadingMore';
60
133
 
61
134
  let {styleProps} = useStyleProps(props);
62
- let {direction} = useLocale();
135
+ let {direction, locale} = useLocale();
63
136
  let collator = useCollator({usage: 'search', sensitivity: 'base'});
64
137
  let gridCollection = useMemo(() => new GridCollection({
65
138
  columnCount: 1,
66
139
  items: [...collection].map(item => ({
67
- type: 'item',
140
+ ...item,
141
+ hasChildNodes: true,
68
142
  childNodes: [{
69
- ...item,
143
+ key: `cell-${item.key}`,
144
+ type: 'cell',
70
145
  index: 0,
71
- type: 'cell'
146
+ value: null,
147
+ level: 0,
148
+ rendered: null,
149
+ textValue: item.textValue,
150
+ hasChildNodes: false,
151
+ childNodes: []
72
152
  }]
73
153
  }))
74
154
  }), [collection]);
75
155
  let state = useGridState({
76
156
  ...props,
77
- collection: gridCollection
157
+ collection: gridCollection,
158
+ focusMode: 'cell',
159
+ selectionBehavior: props.selectionStyle === 'highlight' ? 'replace' : 'toggle'
78
160
  });
79
- let layout = useListLayout(state);
161
+ let layout = useListLayout(state, props.density || 'regular');
80
162
  let keyboardDelegate = useMemo(() => new GridKeyboardDelegate({
81
163
  collection: state.collection,
82
164
  disabledKeys: state.disabledKeys,
83
165
  ref: domRef,
84
166
  direction,
85
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.
86
170
  focusMode: 'cell'
87
171
  }), [state, domRef, direction, collator]);
172
+
173
+ let provider = useProvider();
174
+ let dragState: DraggableCollectionState;
175
+ if (isListDraggable) {
176
+ 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
+ }
185
+ });
186
+ }
187
+
88
188
  let {gridProps} = useGrid({
89
189
  ...props,
90
190
  isVirtualized: true,
@@ -92,31 +192,45 @@ function ListView<T extends object>(props: ListViewProps<T>, ref: DOMRef<HTMLDiv
92
192
  }, state, domRef);
93
193
 
94
194
  // Sync loading state into the layout.
95
- layout.isLoading = props.isLoading;
195
+ layout.isLoading = isLoading;
196
+
197
+ let focusedKey = state.selectionManager.focusedKey;
198
+ let focusedItem = gridCollection.getItem(state.selectionManager.focusedKey);
199
+ if (focusedItem?.parentKey != null) {
200
+ focusedKey = focusedItem.parentKey;
201
+ }
96
202
 
97
203
  return (
98
- <ListViewContext.Provider value={{state, keyboardDelegate}}>
204
+ <ListViewContext.Provider value={{state, keyboardDelegate, dragState, onAction, isListDraggable, layout}}>
99
205
  <Virtualizer
100
206
  {...gridProps}
101
207
  {...styleProps}
208
+ isLoading={isLoading}
209
+ onLoadMore={onLoadMore}
102
210
  ref={domRef}
103
- focusedKey={state.selectionManager.focusedKey}
104
- sizeToFit="height"
211
+ focusedKey={focusedKey}
105
212
  scrollDirection="vertical"
106
213
  className={
107
214
  classNames(
108
215
  listStyles,
109
216
  'react-spectrum-ListView',
217
+ `react-spectrum-ListView--${density}`,
218
+ 'react-spectrum-ListView--emphasized',
219
+ {
220
+ 'react-spectrum-ListView--quiet': isQuiet,
221
+ 'react-spectrum-ListView--draggable': isListDraggable,
222
+ 'react-spectrum-ListView--loadingMore': loadingState === 'loadingMore'
223
+ },
110
224
  styleProps.className
111
225
  )
112
226
  }
113
227
  layout={layout}
114
- collection={collection}
228
+ collection={gridCollection}
115
229
  transitionDuration={transitionDuration}>
116
230
  {(type, item) => {
117
231
  if (type === 'item') {
118
232
  return (
119
- <ListViewItem item={item} />
233
+ <ListViewItem item={item} isEmphasized dragHooks={dragHooks} />
120
234
  );
121
235
  } else if (type === 'loader') {
122
236
  return (
@@ -145,14 +259,20 @@ function ListView<T extends object>(props: ListViewProps<T>, ref: DOMRef<HTMLDiv
145
259
  );
146
260
  }
147
261
 
148
-
149
262
  function CenteredWrapper({children}) {
150
263
  let {state} = useContext(ListViewContext);
151
264
  return (
152
265
  <div
153
266
  role="row"
154
267
  aria-rowindex={state.collection.size + 1}
155
- className={classNames(listStyles, 'react-spectrum-ListView-centeredWrapper')}>
268
+ className={
269
+ classNames(
270
+ listStyles,
271
+ 'react-spectrum-ListView-centeredWrapper',
272
+ {
273
+ 'react-spectrum-ListView-centeredWrapper--loadingMore': state.collection.size > 0
274
+ }
275
+ )}>
156
276
  <div role="gridcell">
157
277
  {children}
158
278
  </div>
@@ -160,5 +280,8 @@ function CenteredWrapper({children}) {
160
280
  );
161
281
  }
162
282
 
283
+ /**
284
+ * Lists display a linear collection of data. They allow users to quickly scan, sort, compare, and take action on large amounts of data.
285
+ */
163
286
  const _ListView = React.forwardRef(ListView) as <T>(props: ListViewProps<T> & {ref?: DOMRef<HTMLDivElement>}) => ReactElement;
164
287
  export {_ListView as ListView};
@@ -9,60 +9,192 @@
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 {classNames} from '@react-spectrum/utils';
12
+ import {Checkbox} from '@react-spectrum/checkbox';
13
+ import ChevronLeftMedium from '@spectrum-icons/ui/ChevronLeftMedium';
14
+ import ChevronRightMedium from '@spectrum-icons/ui/ChevronRightMedium';
15
+ import {classNames, ClearSlots, SlotProvider} from '@react-spectrum/utils';
16
+ import {Content} from '@react-spectrum/view';
17
+ import type {DraggableItemResult} from '@react-aria/dnd';
18
+ import {FocusRing, useFocusRing} from '@react-aria/focus';
19
+ import {Grid} from '@react-spectrum/layout';
20
+ import ListGripper from '@spectrum-icons/ui/ListGripper';
13
21
  import listStyles from './listview.css';
14
22
  import {ListViewContext} from './ListView';
15
23
  import {mergeProps} from '@react-aria/utils';
16
24
  import React, {useContext, useRef} from 'react';
17
- import {useFocusRing} from '@react-aria/focus';
18
- import {useGridCell, useGridRow} from '@react-aria/grid';
19
- import {useHover} from '@react-aria/interactions';
20
-
25
+ import {useButton} from '@react-aria/button';
26
+ import {useGridCell, useGridRow, useGridSelectionCheckbox} from '@react-aria/grid';
27
+ import {useHover, usePress} from '@react-aria/interactions';
28
+ import {useLocale} from '@react-aria/i18n';
29
+ import {useVisuallyHidden} from '@react-aria/visually-hidden';
21
30
 
22
31
  export function ListViewItem(props) {
23
32
  let {
24
- item
33
+ item,
34
+ isEmphasized,
35
+ dragHooks
25
36
  } = props;
26
- let {state} = useContext(ListViewContext);
27
- let ref = useRef<HTMLDivElement>();
37
+ let cellNode = [...item.childNodes][0];
38
+ let {state, dragState, onAction, isListDraggable, layout} = useContext(ListViewContext);
39
+
40
+ let {direction} = useLocale();
41
+ let rowRef = useRef<HTMLDivElement>();
42
+ let cellRef = useRef<HTMLDivElement>();
28
43
  let {
29
44
  isFocusVisible: isFocusVisibleWithin,
30
45
  focusProps: focusWithinProps
31
46
  } = useFocusRing({within: true});
32
47
  let {isFocusVisible, focusProps} = useFocusRing();
33
- let {hoverProps, isHovered} = useHover({});
48
+ let allowsInteraction = state.selectionManager.selectionMode !== 'none' || onAction;
49
+ let isDisabled = !allowsInteraction || state.disabledKeys.has(item.key);
50
+ let isDraggable = dragState?.isDraggable(item.key) && !isDisabled;
51
+ let {hoverProps, isHovered} = useHover({isDisabled});
52
+ let {pressProps, isPressed} = usePress({isDisabled});
34
53
  let {rowProps} = useGridRow({
35
54
  node: item,
36
- isVirtualized: true
37
- }, state, ref);
55
+ isVirtualized: true,
56
+ onAction: onAction ? () => onAction(item.key) : undefined,
57
+ shouldSelectOnPressUp: isListDraggable
58
+ }, state, rowRef);
38
59
  let {gridCellProps} = useGridCell({
39
- node: item,
60
+ node: cellNode,
40
61
  focusMode: 'cell'
41
- }, state, ref);
62
+ }, state, cellRef);
63
+ let draggableItem: DraggableItemResult;
64
+ if (isListDraggable) {
65
+ // eslint-disable-next-line react-hooks/rules-of-hooks
66
+ draggableItem = dragHooks.useDraggableItem({key: item.key}, dragState);
67
+ }
42
68
  const mergedProps = mergeProps(
43
69
  gridCellProps,
44
70
  hoverProps,
45
71
  focusWithinProps,
46
72
  focusProps
47
73
  );
74
+ let {checkboxProps} = useGridSelectionCheckbox({...props, key: item.key}, state);
75
+
76
+ let dragButtonRef = React.useRef();
77
+ let {buttonProps} = useButton({
78
+ ...draggableItem?.dragButtonProps,
79
+ elementType: 'div'
80
+ }, dragButtonRef);
81
+
82
+ let chevron = null;
83
+ if (item.props.hasChildItems) {
84
+ chevron = direction === 'ltr'
85
+ ? (
86
+ <ChevronRightMedium
87
+ aria-hidden="true"
88
+ UNSAFE_className={listStyles['react-spectrum-ListViewItem-parentIndicator']} />
89
+ )
90
+ : (
91
+ <ChevronLeftMedium
92
+ aria-hidden="true"
93
+ UNSAFE_className={listStyles['react-spectrum-ListViewItem-parentIndicator']} />
94
+ );
95
+ }
96
+
97
+ let showCheckbox = state.selectionManager.selectionMode !== 'none' && state.selectionManager.selectionBehavior === 'toggle';
98
+ let isSelected = state.selectionManager.isSelected(item.key);
99
+ let showDragHandle = isDraggable && isFocusVisibleWithin;
100
+ let {visuallyHiddenProps} = useVisuallyHidden();
101
+ let isFirstRow = item.prevKey == null;
102
+ let isLastRow = item.nextKey == null;
103
+ // Figure out if the ListView content is equal or greater in height to the container. If so, we'll need to round the bottom
104
+ // border corners of the last row when selected and we can get rid of the bottom border if it isn't selected to avoid border overlap
105
+ // with bottom border
106
+ let isFlushWithContainerBottom = false;
107
+ if (isLastRow) {
108
+ if (layout.getContentSize()?.height >= layout.virtualizer?.getVisibleRect().height) {
109
+ isFlushWithContainerBottom = true;
110
+ }
111
+ }
48
112
 
49
113
  return (
50
- <div {...rowProps}>
114
+ <div
115
+ {...mergeProps(rowProps, pressProps, isDraggable && draggableItem?.dragProps)}
116
+ className={
117
+ classNames(
118
+ listStyles,
119
+ 'react-spectrum-ListView-row',
120
+ {
121
+ 'focus-ring': isFocusVisible
122
+ }
123
+ )
124
+ }
125
+ ref={rowRef}>
51
126
  <div
52
127
  className={
53
128
  classNames(
54
129
  listStyles,
55
130
  'react-spectrum-ListViewItem',
56
131
  {
132
+ 'is-active': isPressed,
57
133
  'is-focused': isFocusVisibleWithin,
58
134
  'focus-ring': isFocusVisible,
59
- 'is-hovered': isHovered
135
+ 'is-hovered': isHovered,
136
+ 'is-selected': isSelected,
137
+ 'is-next-selected': state.selectionManager.isSelected(item.nextKey),
138
+ 'react-spectrum-ListViewItem--highlightSelection': state.selectionManager.selectionBehavior === 'replace' && (isSelected || state.selectionManager.isSelected(item.nextKey)),
139
+ 'react-spectrum-ListViewItem--draggable': isDraggable,
140
+ 'react-spectrum-ListViewItem--firstRow': isFirstRow,
141
+ 'react-spectrum-ListViewItem--lastRow': isLastRow,
142
+ 'react-spectrum-ListViewItem--isFlushBottom': isFlushWithContainerBottom
60
143
  }
61
144
  )
62
145
  }
63
- ref={ref}
146
+ ref={cellRef}
64
147
  {...mergedProps}>
65
- {item.rendered}
148
+ <Grid UNSAFE_className={listStyles['react-spectrum-ListViewItem-grid']}>
149
+ {isListDraggable &&
150
+ <div className={listStyles['react-spectrum-ListViewItem-draghandle-container']}>
151
+ {isDraggable &&
152
+ <FocusRing focusRingClass={classNames(listStyles, 'focus-ring')}>
153
+ <div
154
+ {...buttonProps as React.HTMLAttributes<HTMLElement>}
155
+ className={
156
+ classNames(
157
+ listStyles,
158
+ 'react-spectrum-ListViewItem-draghandle-button'
159
+ )
160
+ }
161
+ style={!showDragHandle ? {...visuallyHiddenProps.style} : {}}
162
+ ref={dragButtonRef}
163
+ draggable="true">
164
+ <ListGripper />
165
+ </div>
166
+ </FocusRing>
167
+ }
168
+ </div>
169
+ }
170
+ {showCheckbox &&
171
+ <Checkbox
172
+ UNSAFE_className={listStyles['react-spectrum-ListViewItem-checkbox']}
173
+ {...checkboxProps}
174
+ isEmphasized={isEmphasized} />
175
+ }
176
+ <SlotProvider
177
+ slots={{
178
+ content: {UNSAFE_className: listStyles['react-spectrum-ListViewItem-content']},
179
+ text: {UNSAFE_className: listStyles['react-spectrum-ListViewItem-content']},
180
+ description: {UNSAFE_className: listStyles['react-spectrum-ListViewItem-description']},
181
+ icon: {UNSAFE_className: listStyles['react-spectrum-ListViewItem-icon'], size: 'M'},
182
+ image: {UNSAFE_className: listStyles['react-spectrum-ListViewItem-image']},
183
+ link: {UNSAFE_className: listStyles['react-spectrum-ListViewItem-content'], isQuiet: true},
184
+ actionButton: {UNSAFE_className: listStyles['react-spectrum-ListViewItem-actions'], isQuiet: true},
185
+ actionGroup: {
186
+ UNSAFE_className: listStyles['react-spectrum-ListViewItem-actions'],
187
+ isQuiet: true,
188
+ density: 'compact'
189
+ },
190
+ actionMenu: {UNSAFE_className: listStyles['react-spectrum-ListViewItem-actionmenu'], isQuiet: true}
191
+ }}>
192
+ {typeof item.rendered === 'string' ? <Content>{item.rendered}</Content> : item.rendered}
193
+ <ClearSlots>
194
+ {chevron}
195
+ </ClearSlots>
196
+ </SlotProvider>
197
+ </Grid>
66
198
  </div>
67
199
  </div>
68
200
  );