@react-spectrum/table 3.10.1-nightly.4028 → 3.10.1-nightly.4036

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/TableView.tsx CHANGED
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Copyright 2020 Adobe. All rights reserved.
2
+ * Copyright 2023 Adobe. All rights reserved.
3
3
  * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
4
  * you may not use this file except in compliance with the License. You may obtain a copy
5
5
  * of the License at http://www.apache.org/licenses/LICENSE-2.0
@@ -10,221 +10,21 @@
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
12
 
13
- import {AriaLabelingProps, DOMProps, DOMRef, DropTarget, FocusableElement, FocusableRef, SpectrumSelectionProps, StyleProps} from '@react-types/shared';
14
- import ArrowDownSmall from '@spectrum-icons/ui/ArrowDownSmall';
15
- import {chain, mergeProps, scrollIntoView, scrollIntoViewport} from '@react-aria/utils';
16
- import {Checkbox} from '@react-spectrum/checkbox';
17
- import ChevronDownMedium from '@spectrum-icons/ui/ChevronDownMedium';
18
- import {
19
- classNames,
20
- useDOMRef,
21
- useFocusableRef,
22
- useIsMobileDevice,
23
- useStyleProps,
24
- useUnwrapDOMRef
25
- } from '@react-spectrum/utils';
26
- import {ColumnSize, SpectrumColumnProps, TableProps} from '@react-types/table';
27
- import type {DragAndDropHooks} from '@react-spectrum/dnd';
28
- import type {DraggableCollectionState, DroppableCollectionState} from '@react-stately/dnd';
29
- import type {DraggableItemResult, DropIndicatorAria, DroppableCollectionResult, DroppableItemResult} from '@react-aria/dnd';
30
- import {FocusRing, FocusScope, useFocusRing} from '@react-aria/focus';
31
- import {getInteractionModality, useHover, usePress} from '@react-aria/interactions';
32
- import {GridNode} from '@react-types/grid';
33
- import {InsertionIndicator} from './InsertionIndicator';
34
- // @ts-ignore
35
- import intlMessages from '../intl/*.json';
36
- import {Item, Menu, MenuTrigger} from '@react-spectrum/menu';
37
- import {layoutInfoToStyle, ScrollView, setScrollLeft, useVirtualizer, VirtualizerItem} from '@react-aria/virtualizer';
38
- import ListGripper from '@spectrum-icons/ui/ListGripper';
39
- import {Nubbin} from './Nubbin';
40
- import {ProgressCircle} from '@react-spectrum/progress';
41
- import React, {DOMAttributes, HTMLAttributes, Key, ReactElement, ReactNode, useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react';
42
- import {Resizer} from './Resizer';
43
- import {ReusableView, useVirtualizerState} from '@react-stately/virtualizer';
44
- import {RootDropIndicator} from './RootDropIndicator';
45
- import {DragPreview as SpectrumDragPreview} from './DragPreview';
46
- import styles from '@adobe/spectrum-css-temp/components/table/vars.css';
47
- import stylesOverrides from './table.css';
48
- import {TableColumnLayout, TableState, useTableState} from '@react-stately/table';
49
- import {TableLayout} from '@react-stately/layout';
50
- import {Tooltip, TooltipTrigger} from '@react-spectrum/tooltip';
51
- import {useButton} from '@react-aria/button';
52
- import {useLocale, useLocalizedStringFormatter} from '@react-aria/i18n';
53
- import {useProvider, useProviderProps} from '@react-spectrum/provider';
54
- import {
55
- useTable,
56
- useTableCell,
57
- useTableColumnHeader,
58
- useTableHeaderRow,
59
- useTableRow,
60
- useTableRowGroup,
61
- useTableSelectAllCheckbox,
62
- useTableSelectionCheckbox
63
- } from '@react-aria/table';
64
- import {useVisuallyHidden, VisuallyHidden} from '@react-aria/visually-hidden';
13
+ import {DOMRef} from '@react-types/shared';
14
+ import React, {ReactElement, useState} from 'react';
15
+ import {SpectrumTableProps} from './TableViewWrapper';
16
+ import {TableViewBase} from './TableViewBase';
17
+ import {useTableState} from '@react-stately/table';
65
18
 
66
- const DEFAULT_HEADER_HEIGHT = {
67
- medium: 34,
68
- large: 40
69
- };
19
+ interface TableProps<T> extends Omit<SpectrumTableProps<T>, 'UNSTABLE_allowsExpandableRows'> {}
70
20
 
71
- const DEFAULT_HIDE_HEADER_CELL_WIDTH = {
72
- medium: 38,
73
- large: 46
74
- };
75
-
76
- const ROW_HEIGHTS = {
77
- compact: {
78
- medium: 32,
79
- large: 40
80
- },
81
- regular: {
82
- medium: 40,
83
- large: 50
84
- },
85
- spacious: {
86
- medium: 48,
87
- large: 60
88
- }
89
- };
90
-
91
- const SELECTION_CELL_DEFAULT_WIDTH = {
92
- medium: 38,
93
- large: 48
94
- };
95
-
96
- const DRAG_BUTTON_CELL_DEFAULT_WIDTH = {
97
- medium: 16,
98
- large: 20
99
- };
100
-
101
- interface TableContextValue<T> {
102
- state: TableState<T>,
103
- dragState: DraggableCollectionState,
104
- dropState: DroppableCollectionState,
105
- dragAndDropHooks: DragAndDropHooks['dragAndDropHooks'],
106
- isTableDraggable: boolean,
107
- isTableDroppable: boolean,
108
- shouldShowCheckboxes: boolean,
109
- layout: TableLayout<T> & {tableState: TableState<T>},
110
- headerRowHovered: boolean,
111
- isInResizeMode: boolean,
112
- setIsInResizeMode: (val: boolean) => void,
113
- isEmpty: boolean,
114
- onFocusedResizer: () => void,
115
- onResizeStart: (widths: Map<Key, ColumnSize>) => void,
116
- onResize: (widths: Map<Key, ColumnSize>) => void,
117
- onResizeEnd: (widths: Map<Key, ColumnSize>) => void,
118
- headerMenuOpen: boolean,
119
- setHeaderMenuOpen: (val: boolean) => void
120
- }
121
-
122
- const TableContext = React.createContext<TableContextValue<unknown>>(null);
123
- export function useTableContext() {
124
- return useContext(TableContext);
125
- }
126
-
127
- const VirtualizerContext = React.createContext(null);
128
- export function useVirtualizerContext() {
129
- return useContext(VirtualizerContext);
130
- }
131
-
132
- export interface SpectrumTableProps<T> extends TableProps<T>, SpectrumSelectionProps, DOMProps, AriaLabelingProps, StyleProps {
133
- /**
134
- * Sets the amount of vertical padding within each cell.
135
- * @default 'regular'
136
- */
137
- density?: 'compact' | 'regular' | 'spacious',
138
- /**
139
- * Sets the overflow behavior for the cell contents.
140
- * @default 'truncate'
141
- */
142
- overflowMode?: 'wrap' | 'truncate',
143
- /** Whether the TableView should be displayed with a quiet style. */
144
- isQuiet?: boolean,
145
- /** Sets what the TableView should render when there is no content to display. */
146
- renderEmptyState?: () => JSX.Element,
147
- /** Handler that is called when a user performs an action on a row. */
148
- onAction?: (key: Key) => void,
149
- /**
150
- * Handler that is called when a user starts a column resize.
151
- */
152
- onResizeStart?: (widths: Map<Key, ColumnSize>) => void,
153
- /**
154
- * Handler that is called when a user performs a column resize.
155
- * Can be used with the width property on columns to put the column widths into
156
- * a controlled state.
157
- */
158
- onResize?: (widths: Map<Key, ColumnSize>) => void,
159
- /**
160
- * Handler that is called after a user performs a column resize.
161
- * Can be used to store the widths of columns for another future session.
162
- */
163
- onResizeEnd?: (widths: Map<Key, ColumnSize>) => void,
164
- /**
165
- * The drag and drop hooks returned by `useDragAndDrop` used to enable drag and drop behavior for the TableView.
166
- * @version alpha
167
- */
168
- dragAndDropHooks?: DragAndDropHooks['dragAndDropHooks']
169
- }
170
-
171
- function TableView<T extends object>(props: SpectrumTableProps<T>, ref: DOMRef<HTMLDivElement>) {
172
- props = useProviderProps(props);
21
+ function TableView<T extends object>(props: TableProps<T>, ref: DOMRef<HTMLDivElement>) {
173
22
  let {
174
- isQuiet,
175
- onAction,
176
- onResizeStart: propsOnResizeStart,
177
- onResizeEnd: propsOnResizeEnd,
23
+ selectionStyle,
178
24
  dragAndDropHooks
179
25
  } = props;
26
+ let [showSelectionCheckboxes, setShowSelectionCheckboxes] = useState(selectionStyle !== 'highlight');
180
27
  let isTableDraggable = !!dragAndDropHooks?.useDraggableCollectionState;
181
- let isTableDroppable = !!dragAndDropHooks?.useDroppableCollectionState;
182
- let dragHooksProvided = useRef(isTableDraggable);
183
- let dropHooksProvided = useRef(isTableDroppable);
184
- useEffect(() => {
185
- if (dragHooksProvided.current !== isTableDraggable) {
186
- console.warn('Drag hooks were provided during one render, but not another. This should be avoided as it may produce unexpected behavior.');
187
- }
188
- if (dropHooksProvided.current !== isTableDroppable) {
189
- console.warn('Drop hooks were provided during one render, but not another. This should be avoided as it may produce unexpected behavior.');
190
- }
191
- }, [isTableDraggable, isTableDroppable]);
192
- let {styleProps} = useStyleProps(props);
193
-
194
- let [showSelectionCheckboxes, setShowSelectionCheckboxes] = useState(props.selectionStyle !== 'highlight');
195
- let {direction} = useLocale();
196
- let {scale} = useProvider();
197
-
198
- const getDefaultWidth = useCallback(({props: {hideHeader, isSelectionCell, showDivider, isDragButtonCell}}: GridNode<T>): ColumnSize | null | undefined => {
199
- if (hideHeader) {
200
- let width = DEFAULT_HIDE_HEADER_CELL_WIDTH[scale];
201
- return showDivider ? width + 1 : width;
202
- } else if (isSelectionCell) {
203
- return SELECTION_CELL_DEFAULT_WIDTH[scale];
204
- } else if (isDragButtonCell) {
205
- return DRAG_BUTTON_CELL_DEFAULT_WIDTH[scale];
206
- }
207
- }, [scale]);
208
-
209
- const getDefaultMinWidth = useCallback(({props: {hideHeader, isSelectionCell, showDivider, isDragButtonCell}}: GridNode<T>): ColumnSize | null | undefined => {
210
- if (hideHeader) {
211
- let width = DEFAULT_HIDE_HEADER_CELL_WIDTH[scale];
212
- return showDivider ? width + 1 : width;
213
- } else if (isSelectionCell) {
214
- return SELECTION_CELL_DEFAULT_WIDTH[scale];
215
- } else if (isDragButtonCell) {
216
- return DRAG_BUTTON_CELL_DEFAULT_WIDTH[scale];
217
- }
218
- return 75;
219
- }, [scale]);
220
-
221
- // Starts when the user selects resize from the menu, ends when resizing ends
222
- // used to control the visibility of the resizer Nubbin
223
- let [isInResizeMode, setIsInResizeMode] = useState(false);
224
- // Starts when the resizer is actually moved
225
- // entering resizing/exiting resizing doesn't trigger a render
226
- // with table layout, so we need to track it here
227
- let [, setIsResizing] = useState(false);
228
28
  let state = useTableState({
229
29
  ...props,
230
30
  showSelectionCheckboxes,
@@ -238,1216 +38,10 @@ function TableView<T extends object>(props: SpectrumTableProps<T>, ref: DOMRef<H
238
38
  setShowSelectionCheckboxes(shouldShowCheckboxes);
239
39
  }
240
40
 
241
- let domRef = useDOMRef(ref);
242
- let headerRef = useRef<HTMLDivElement>();
243
- let bodyRef = useRef<HTMLDivElement>();
244
- let stringFormatter = useLocalizedStringFormatter(intlMessages);
245
-
246
- let density = props.density || 'regular';
247
- let columnLayout = useMemo(
248
- () => new TableColumnLayout({
249
- getDefaultWidth,
250
- getDefaultMinWidth
251
- }),
252
- [getDefaultWidth, getDefaultMinWidth]
253
- );
254
- let tableLayout = useMemo(() => new TableLayout({
255
- // If props.rowHeight is auto, then use estimated heights based on scale, otherwise use fixed heights.
256
- rowHeight: props.overflowMode === 'wrap'
257
- ? null
258
- : ROW_HEIGHTS[density][scale],
259
- estimatedRowHeight: props.overflowMode === 'wrap'
260
- ? ROW_HEIGHTS[density][scale]
261
- : null,
262
- headingHeight: props.overflowMode === 'wrap'
263
- ? null
264
- : DEFAULT_HEADER_HEIGHT[scale],
265
- estimatedHeadingHeight: props.overflowMode === 'wrap'
266
- ? DEFAULT_HEADER_HEIGHT[scale]
267
- : null,
268
- columnLayout,
269
- initialCollection: state.collection
270
- }),
271
- // don't recompute when state.collection changes, only used for initial value
272
- // eslint-disable-next-line react-hooks/exhaustive-deps
273
- [props.overflowMode, scale, density, columnLayout]
274
- );
275
-
276
- // Use a proxy so that a new object is created for each render so that alternate instances aren't affected by mutation.
277
- // This can be thought of as equivalent to `{…tableLayout, tableState: state}`, but works with classes as well.
278
- let layout = useMemo(() => {
279
- let proxy = new Proxy(tableLayout, {
280
- get(target, prop, receiver) {
281
- return prop === 'tableState' ? state : Reflect.get(target, prop, receiver);
282
- }
283
- });
284
- return proxy as TableLayout<T> & {tableState: TableState<T>};
285
- }, [state, tableLayout]);
286
-
287
- let dragState: DraggableCollectionState;
288
- let preview = useRef(null);
289
- if (isTableDraggable) {
290
- dragState = dragAndDropHooks.useDraggableCollectionState({
291
- collection: state.collection,
292
- selectionManager: state.selectionManager,
293
- preview
294
- });
295
- dragAndDropHooks.useDraggableCollection({}, dragState, domRef);
296
- }
297
-
298
- let DragPreview = dragAndDropHooks?.DragPreview;
299
- let dropState: DroppableCollectionState;
300
- let droppableCollection: DroppableCollectionResult;
301
- let isRootDropTarget: boolean;
302
- if (isTableDroppable) {
303
- dropState = dragAndDropHooks.useDroppableCollectionState({
304
- collection: state.collection,
305
- selectionManager: state.selectionManager
306
- });
307
- droppableCollection = dragAndDropHooks.useDroppableCollection({
308
- keyboardDelegate: layout,
309
- dropTargetDelegate: layout
310
- }, dropState, domRef);
311
-
312
- isRootDropTarget = dropState.isDropTarget({type: 'root'});
313
- }
314
-
315
- let {gridProps} = useTable({
316
- ...props,
317
- isVirtualized: true,
318
- layout,
319
- onRowAction: onAction
320
- }, state, domRef);
321
- let [headerMenuOpen, setHeaderMenuOpen] = useState(false);
322
- let [headerRowHovered, setHeaderRowHovered] = useState(false);
323
-
324
- // This overrides collection view's renderWrapper to support DOM hierarchy.
325
- type View = ReusableView<GridNode<T>, ReactNode>;
326
- let renderWrapper = (parent: View, reusableView: View, children: View[], renderChildren: (views: View[]) => ReactElement[]) => {
327
- let style = layoutInfoToStyle(reusableView.layoutInfo, direction, parent && parent.layoutInfo);
328
- if (style.overflow === 'hidden') {
329
- style.overflow = 'visible'; // needed to support position: sticky
330
- }
331
-
332
- if (reusableView.viewType === 'rowgroup') {
333
- return (
334
- <TableRowGroup key={reusableView.key} style={style}>
335
- {isTableDroppable &&
336
- <RootDropIndicator key="root" />
337
- }
338
- {renderChildren(children)}
339
- </TableRowGroup>
340
- );
341
- }
342
-
343
- if (reusableView.viewType === 'header') {
344
- return (
345
- <TableHeader
346
- key={reusableView.key}
347
- style={style}>
348
- {renderChildren(children)}
349
- </TableHeader>
350
- );
351
- }
352
-
353
- if (reusableView.viewType === 'row') {
354
- return (
355
- <TableRow
356
- key={reusableView.key}
357
- item={reusableView.content}
358
- style={style}
359
- hasActions={onAction}
360
- isTableDroppable={isTableDroppable}
361
- isTableDraggable={isTableDraggable}>
362
- {renderChildren(children)}
363
- </TableRow>
364
- );
365
- }
366
-
367
- if (reusableView.viewType === 'headerrow') {
368
- return (
369
- <TableHeaderRow
370
- onHoverChange={setHeaderRowHovered}
371
- key={reusableView.key}
372
- style={style}
373
- item={reusableView.content}>
374
- {renderChildren(children)}
375
- </TableHeaderRow>
376
- );
377
- }
378
- let isDropTarget: boolean;
379
- let isRootDroptarget: boolean;
380
- if (isTableDroppable) {
381
- if (parent.content) {
382
- isDropTarget = dropState.isDropTarget({type: 'item', dropPosition: 'on', key: parent.content.key});
383
- }
384
- isRootDroptarget = dropState.isDropTarget({type: 'root'});
385
- }
386
-
387
- return (
388
- <VirtualizerItem
389
- key={reusableView.key}
390
- layoutInfo={reusableView.layoutInfo}
391
- virtualizer={reusableView.virtualizer}
392
- parent={parent?.layoutInfo}
393
- className={
394
- classNames(
395
- styles,
396
- 'spectrum-Table-cellWrapper',
397
- classNames(
398
- stylesOverrides,
399
- {
400
- 'react-spectrum-Table-cellWrapper': !reusableView.layoutInfo.estimatedSize,
401
- 'react-spectrum-Table-cellWrapper--dropTarget': isDropTarget || isRootDroptarget
402
- }
403
- )
404
- )
405
- }>
406
- {reusableView.rendered}
407
- </VirtualizerItem>
408
- );
409
- };
410
-
411
- let renderView = (type: string, item: GridNode<T>) => {
412
- switch (type) {
413
- case 'header':
414
- case 'rowgroup':
415
- case 'section':
416
- case 'row':
417
- case 'headerrow':
418
- return null;
419
- case 'cell': {
420
- if (item.props.isSelectionCell) {
421
- return <TableCheckboxCell cell={item} />;
422
- }
423
-
424
- if (item.props.isDragButtonCell) {
425
- return <TableDragCell cell={item} />;
426
- }
427
-
428
- return <TableCell cell={item} />;
429
- }
430
- case 'placeholder':
431
- // TODO: move to react-aria?
432
- return (
433
- <div
434
- role="gridcell"
435
- aria-colindex={item.index + 1}
436
- aria-colspan={item.colspan > 1 ? item.colspan : null} />
437
- );
438
- case 'column':
439
- if (item.props.isSelectionCell) {
440
- return <TableSelectAllCell column={item} />;
441
- }
442
-
443
- if (item.props.isDragButtonCell) {
444
- return <TableDragHeaderCell column={item} />;
445
- }
446
-
447
- // TODO: consider this case, what if we have hidden headers and a empty table
448
- if (item.props.hideHeader) {
449
- return (
450
- <TooltipTrigger placement="top" trigger="focus">
451
- <TableColumnHeader column={item} />
452
- <Tooltip placement="top">{item.rendered}</Tooltip>
453
- </TooltipTrigger>
454
- );
455
- }
456
-
457
- if (item.props.allowsResizing && !item.hasChildNodes) {
458
- return <ResizableTableColumnHeader tableRef={domRef} column={item} />;
459
- }
460
-
461
- return (
462
- <TableColumnHeader column={item} />
463
- );
464
- case 'loader':
465
- return (
466
- <CenteredWrapper>
467
- <ProgressCircle
468
- isIndeterminate
469
- aria-label={state.collection.size > 0 ? stringFormatter.format('loadingMore') : stringFormatter.format('loading')} />
470
- </CenteredWrapper>
471
- );
472
- case 'empty': {
473
- let emptyState = props.renderEmptyState ? props.renderEmptyState() : null;
474
- if (emptyState == null) {
475
- return null;
476
- }
477
-
478
- return (
479
- <CenteredWrapper>
480
- {emptyState}
481
- </CenteredWrapper>
482
- );
483
- }
484
- }
485
- };
486
-
487
- let [isVerticalScrollbarVisible, setVerticalScollbarVisible] = useState(false);
488
- let [isHorizontalScrollbarVisible, setHorizontalScollbarVisible] = useState(false);
489
- let viewport = useRef({x: 0, y: 0, width: 0, height: 0});
490
- let onVisibleRectChange = useCallback((e) => {
491
- if (viewport.current.width === e.width && viewport.current.height === e.height) {
492
- return;
493
- }
494
- viewport.current = e;
495
- if (bodyRef.current) {
496
- setVerticalScollbarVisible(bodyRef.current.clientWidth + 2 < bodyRef.current.offsetWidth);
497
- setHorizontalScollbarVisible(bodyRef.current.clientHeight + 2 < bodyRef.current.offsetHeight);
498
- }
499
- }, []);
500
- let {isFocusVisible, focusProps} = useFocusRing();
501
- let isEmpty = state.collection.size === 0;
502
-
503
- let onFocusedResizer = () => {
504
- bodyRef.current.scrollLeft = headerRef.current.scrollLeft;
505
- };
506
-
507
- let onResizeStart = useCallback((widths) => {
508
- setIsResizing(true);
509
- propsOnResizeStart?.(widths);
510
- }, [setIsResizing, propsOnResizeStart]);
511
- let onResizeEnd = useCallback((widths) => {
512
- setIsInResizeMode(false);
513
- setIsResizing(false);
514
- propsOnResizeEnd?.(widths);
515
- }, [propsOnResizeEnd, setIsInResizeMode, setIsResizing]);
516
-
517
- let focusedKey = state.selectionManager.focusedKey;
518
- if (dropState?.target?.type === 'item') {
519
- focusedKey = dropState.target.key;
520
- }
521
-
522
- let mergedProps = mergeProps(
523
- isTableDroppable && droppableCollection?.collectionProps,
524
- gridProps,
525
- focusProps,
526
- dragAndDropHooks?.isVirtualDragging() && {tabIndex: null}
527
- );
528
-
529
- return (
530
- <TableContext.Provider value={{state, dragState, dropState, dragAndDropHooks, isTableDraggable, isTableDroppable, layout, onResizeStart, onResize: props.onResize, onResizeEnd, headerRowHovered, isInResizeMode, setIsInResizeMode, isEmpty, onFocusedResizer, headerMenuOpen, setHeaderMenuOpen, shouldShowCheckboxes}}>
531
- <TableVirtualizer
532
- {...mergedProps}
533
- {...styleProps}
534
- className={
535
- classNames(
536
- styles,
537
- 'spectrum-Table',
538
- `spectrum-Table--${density}`,
539
- {
540
- 'spectrum-Table--quiet': isQuiet,
541
- 'spectrum-Table--wrap': props.overflowMode === 'wrap',
542
- 'spectrum-Table--loadingMore': state.collection.body.props.loadingState === 'loadingMore',
543
- 'spectrum-Table--isVerticalScrollbarVisible': isVerticalScrollbarVisible,
544
- 'spectrum-Table--isHorizontalScrollbarVisible': isHorizontalScrollbarVisible
545
- },
546
- classNames(
547
- stylesOverrides,
548
- 'react-spectrum-Table'
549
- ),
550
- styleProps.className
551
- )
552
- }
553
- layout={layout}
554
- collection={state.collection}
555
- focusedKey={focusedKey}
556
- renderView={renderView}
557
- renderWrapper={renderWrapper}
558
- onVisibleRectChange={onVisibleRectChange}
559
- domRef={domRef}
560
- headerRef={headerRef}
561
- bodyRef={bodyRef}
562
- isFocusVisible={isFocusVisible}
563
- isVirtualDragging={dragAndDropHooks?.isVirtualDragging()}
564
- isRootDropTarget={isRootDropTarget} />
565
- {DragPreview && isTableDraggable &&
566
- <DragPreview ref={preview}>
567
- {() => {
568
- if (dragAndDropHooks.renderPreview) {
569
- return dragAndDropHooks.renderPreview(dragState.draggingKeys, dragState.draggedKey);
570
- }
571
- let itemCount = dragState.draggingKeys.size;
572
- let maxWidth = bodyRef.current.getBoundingClientRect().width;
573
- let height = ROW_HEIGHTS[density][scale];
574
- let itemText = state.collection.getTextValue(dragState.draggedKey);
575
- return <SpectrumDragPreview itemText={itemText} itemCount={itemCount} height={height} maxWidth={maxWidth} />;
576
- }}
577
- </DragPreview>
578
- }
579
- </TableContext.Provider>
580
- );
581
- }
582
-
583
- // This is a custom Virtualizer that also has a header that syncs its scroll position with the body.
584
- function TableVirtualizer(props) {
585
- let {layout, collection, focusedKey, renderView, renderWrapper, domRef, bodyRef, headerRef, onVisibleRectChange: onVisibleRectChangeProp, isFocusVisible, isVirtualDragging, isRootDropTarget, ...otherProps} = props;
586
- let {direction} = useLocale();
587
- let loadingState = collection.body.props.loadingState;
588
- let isLoading = loadingState === 'loading' || loadingState === 'loadingMore';
589
- let onLoadMore = collection.body.props.onLoadMore;
590
- let transitionDuration = 220;
591
- if (isLoading) {
592
- transitionDuration = 160;
593
- }
594
- if (layout.resizingColumn != null) {
595
- // while resizing, prop changes should not cause animations
596
- transitionDuration = 0;
597
- }
598
- let state = useVirtualizerState<object, ReactNode, ReactNode>({
599
- layout,
600
- collection,
601
- renderView,
602
- renderWrapper,
603
- onVisibleRectChange(rect) {
604
- bodyRef.current.scrollTop = rect.y;
605
- setScrollLeft(bodyRef.current, direction, rect.x);
606
- },
607
- transitionDuration
608
- });
609
-
610
- let scrollToItem = useCallback((key) => {
611
- let item = collection.getItem(key);
612
- let column = collection.columns[0];
613
- let virtualizer = state.virtualizer;
614
-
615
- virtualizer.scrollToItem(key, {
616
- duration: 0,
617
- // Prevent scrolling to the top when clicking on column headers.
618
- shouldScrollY: item?.type !== 'column',
619
- // Offset scroll position by width of selection cell
620
- // (which is sticky and will overlap the cell we're scrolling to).
621
- offsetX: column.props.isSelectionCell || column.props.isDragButtonCell
622
- ? layout.getColumnWidth(column.key)
623
- : 0
624
- });
625
-
626
- // Sync the scroll positions of the column headers and the body so scrollIntoViewport can
627
- // properly decide if the column is outside the viewport or not
628
- headerRef.current.scrollLeft = bodyRef.current.scrollLeft;
629
- }, [collection, bodyRef, headerRef, layout, state.virtualizer]);
630
-
631
- let memoedVirtualizerProps = useMemo(() => ({
632
- tabIndex: otherProps.tabIndex,
633
- focusedKey,
634
- scrollToItem,
635
- isLoading,
636
- onLoadMore
637
- }), [otherProps.tabIndex, focusedKey, scrollToItem, isLoading, onLoadMore]);
638
-
639
- let {virtualizerProps, scrollViewProps: {onVisibleRectChange}} = useVirtualizer(memoedVirtualizerProps, state, domRef);
640
-
641
- // this effect runs whenever the contentSize changes, it doesn't matter what the content size is
642
- // only that it changes in a resize, and when that happens, we want to sync the body to the
643
- // header scroll position
644
- useEffect(() => {
645
- if (getInteractionModality() === 'keyboard' && headerRef.current.contains(document.activeElement)) {
646
- scrollIntoView(headerRef.current, document.activeElement as HTMLElement);
647
- scrollIntoViewport(document.activeElement, {containingElement: domRef.current});
648
- bodyRef.current.scrollLeft = headerRef.current.scrollLeft;
649
- }
650
- }, [state.contentSize, headerRef, bodyRef, domRef]);
651
-
652
- let headerHeight = layout.getLayoutInfo('header')?.rect.height || 0;
653
- let visibleRect = state.virtualizer.visibleRect;
654
-
655
- // Sync the scroll position from the table body to the header container.
656
- let onScroll = useCallback(() => {
657
- headerRef.current.scrollLeft = bodyRef.current.scrollLeft;
658
- }, [bodyRef, headerRef]);
659
-
660
- let resizerPosition = layout.getResizerPosition() - 2;
661
-
662
- let resizerAtEdge = resizerPosition > Math.max(state.virtualizer.contentSize.width, state.virtualizer.visibleRect.width) - 3;
663
- // this should be fine, every movement of the resizer causes a rerender
664
- // scrolling can cause it to lag for a moment, but it's always updated
665
- let resizerInVisibleRegion = resizerPosition < state.virtualizer.visibleRect.maxX;
666
- let shouldHardCornerResizeCorner = resizerAtEdge && resizerInVisibleRegion;
667
-
668
- // minimize re-render caused on Resizers by memoing this
669
- let resizingColumnWidth = layout.getColumnWidth(layout.resizingColumn);
670
- let resizingColumn = useMemo(() => ({
671
- width: resizingColumnWidth,
672
- key: layout.resizingColumn
673
- }), [resizingColumnWidth, layout.resizingColumn]);
674
- let mergedProps = mergeProps(
675
- otherProps,
676
- virtualizerProps,
677
- isVirtualDragging && {tabIndex: null}
678
- );
679
-
680
- return (
681
- <VirtualizerContext.Provider value={resizingColumn}>
682
- <FocusScope>
683
- <div
684
- {...mergedProps}
685
- ref={domRef}>
686
- <div
687
- role="presentation"
688
- className={classNames(styles, 'spectrum-Table-headWrapper')}
689
- style={{
690
- width: visibleRect.width,
691
- height: headerHeight,
692
- overflow: 'hidden',
693
- position: 'relative',
694
- willChange: state.isScrolling ? 'scroll-position' : undefined,
695
- transition: state.isAnimating ? `none ${state.virtualizer.transitionDuration}ms` : undefined
696
- }}
697
- ref={headerRef}>
698
- {state.visibleViews[0]}
699
- </div>
700
- <ScrollView
701
- role="presentation"
702
- className={
703
- classNames(
704
- styles,
705
- 'spectrum-Table-body',
706
- {
707
- 'focus-ring': isFocusVisible,
708
- 'spectrum-Table-body--resizerAtTableEdge': shouldHardCornerResizeCorner
709
- },
710
- classNames(
711
- stylesOverrides,
712
- 'react-spectrum-Table-body',
713
- {
714
- 'react-spectrum-Table-body--dropTarget': !!isRootDropTarget
715
- }
716
- )
717
- )
718
- }
719
- tabIndex={isVirtualDragging ? null : -1}
720
- style={{flex: 1}}
721
- innerStyle={{overflow: 'visible', transition: state.isAnimating ? `none ${state.virtualizer.transitionDuration}ms` : undefined}}
722
- ref={bodyRef}
723
- contentSize={state.contentSize}
724
- onVisibleRectChange={chain(onVisibleRectChange, onVisibleRectChangeProp)}
725
- onScrollStart={state.startScrolling}
726
- onScrollEnd={state.endScrolling}
727
- onScroll={onScroll}>
728
- {state.visibleViews[1]}
729
- <div
730
- className={classNames(styles, 'spectrum-Table-bodyResizeIndicator')}
731
- style={{[direction === 'ltr' ? 'left' : 'right']: `${resizerPosition}px`, height: `${Math.max(state.virtualizer.contentSize.height, state.virtualizer.visibleRect.height)}px`, display: layout.resizingColumn ? 'block' : 'none'}} />
732
- </ScrollView>
733
- </div>
734
- </FocusScope>
735
- </VirtualizerContext.Provider>
736
- );
737
- }
738
-
739
- function TableHeader({children, ...otherProps}) {
740
- let {rowGroupProps} = useTableRowGroup();
741
-
742
- return (
743
- <div {...rowGroupProps} {...otherProps} className={classNames(styles, 'spectrum-Table-head')}>
744
- {children}
745
- </div>
746
- );
747
- }
748
-
749
- function TableColumnHeader(props) {
750
- let {column} = props;
751
- let ref = useRef<HTMLDivElement>(null);
752
- let {state, isEmpty} = useTableContext();
753
- let {pressProps, isPressed} = usePress({isDisabled: isEmpty});
754
- let columnProps = column.props as SpectrumColumnProps<unknown>;
755
- useEffect(() => {
756
- if (column.hasChildNodes && columnProps.allowsResizing) {
757
- console.warn(`Column key: ${column.key}. Columns with child columns don't allow resizing.`);
758
- }
759
- }, [column.hasChildNodes, column.key, columnProps.allowsResizing]);
760
-
761
- let {columnHeaderProps} = useTableColumnHeader({
762
- node: column,
763
- isVirtualized: true
764
- }, state, ref);
765
-
766
- let {hoverProps, isHovered} = useHover({...props, isDisabled: isEmpty});
767
-
768
- const allProps = [columnHeaderProps, hoverProps, pressProps];
769
-
770
- return (
771
- <FocusRing focusRingClass={classNames(styles, 'focus-ring')}>
772
- <div
773
- {...mergeProps(...allProps)}
774
- ref={ref}
775
- className={
776
- classNames(
777
- styles,
778
- 'spectrum-Table-headCell',
779
- {
780
- 'is-active': isPressed,
781
- 'is-sortable': columnProps.allowsSorting,
782
- 'is-sorted-desc': state.sortDescriptor?.column === column.key && state.sortDescriptor?.direction === 'descending',
783
- 'is-sorted-asc': state.sortDescriptor?.column === column.key && state.sortDescriptor?.direction === 'ascending',
784
- 'is-hovered': isHovered,
785
- 'spectrum-Table-cell--hideHeader': columnProps.hideHeader
786
- },
787
- classNames(
788
- stylesOverrides,
789
- 'react-spectrum-Table-cell',
790
- {
791
- 'react-spectrum-Table-cell--alignCenter': columnProps.align === 'center' || column.colspan > 1,
792
- 'react-spectrum-Table-cell--alignEnd': columnProps.align === 'end'
793
- }
794
- )
795
- )
796
- }>
797
- {columnProps.allowsSorting &&
798
- <ArrowDownSmall UNSAFE_className={classNames(styles, 'spectrum-Table-sortedIcon')} />
799
- }
800
- {columnProps.hideHeader ?
801
- <VisuallyHidden>{column.rendered}</VisuallyHidden> :
802
- <div className={classNames(styles, 'spectrum-Table-headCellContents')}>{column.rendered}</div>
803
- }
804
- </div>
805
- </FocusRing>
806
- );
807
- }
808
-
809
- let _TableColumnHeaderButton = (props, ref: FocusableRef<HTMLDivElement>) => {
810
- let {focusProps, alignment, ...otherProps} = props;
811
- let {isEmpty} = useTableContext();
812
- let domRef = useFocusableRef(ref);
813
- let {buttonProps} = useButton({...otherProps, elementType: 'div', isDisabled: isEmpty}, domRef);
814
- let {hoverProps, isHovered} = useHover({...otherProps, isDisabled: isEmpty});
815
-
816
- return (
817
- <div
818
- className={
819
- classNames(
820
- styles,
821
- 'spectrum-Table-headCellContents',
822
- {
823
- 'is-hovered': isHovered
824
- }
825
- )
826
- }
827
- {...hoverProps}>
828
- <div
829
- className={
830
- classNames(
831
- styles,
832
- 'spectrum-Table-headCellButton',
833
- {
834
- 'spectrum-Table-headCellButton--alignStart': alignment === 'start',
835
- 'spectrum-Table-headCellButton--alignCenter': alignment === 'center',
836
- 'spectrum-Table-headCellButton--alignEnd': alignment === 'end'
837
- }
838
- )
839
- }
840
- {...mergeProps(buttonProps, focusProps)}
841
- ref={domRef}>
842
- {props.children}
843
- </div>
844
- </div>
845
- );
846
- };
847
- let TableColumnHeaderButton = React.forwardRef(_TableColumnHeaderButton);
848
-
849
- function ResizableTableColumnHeader(props) {
850
- let {column} = props;
851
- let ref = useRef(null);
852
- let triggerRef = useRef(null);
853
- let resizingRef = useRef(null);
854
- let {
855
- state,
856
- layout,
857
- onResizeStart,
858
- onResize,
859
- onResizeEnd,
860
- headerRowHovered,
861
- setIsInResizeMode,
862
- isEmpty,
863
- onFocusedResizer,
864
- isInResizeMode,
865
- headerMenuOpen,
866
- setHeaderMenuOpen
867
- } = useTableContext();
868
- let stringFormatter = useLocalizedStringFormatter(intlMessages);
869
- let {pressProps, isPressed} = usePress({isDisabled: isEmpty});
870
- let {columnHeaderProps} = useTableColumnHeader({
871
- node: column,
872
- isVirtualized: true
873
- }, state, ref);
874
-
875
- let {hoverProps, isHovered} = useHover({...props, isDisabled: isEmpty || headerMenuOpen});
876
-
877
- const allProps = [columnHeaderProps, pressProps, hoverProps];
878
-
879
- let columnProps = column.props as SpectrumColumnProps<unknown>;
880
-
881
- let {isFocusVisible, focusProps} = useFocusRing();
882
-
883
- const onMenuSelect = (key) => {
884
- switch (key) {
885
- case 'sort-asc':
886
- state.sort(column.key, 'ascending');
887
- break;
888
- case 'sort-desc':
889
- state.sort(column.key, 'descending');
890
- break;
891
- case 'resize':
892
- layout.startResize(column.key);
893
- setIsInResizeMode(true);
894
- state.setKeyboardNavigationDisabled(true);
895
- break;
896
- }
897
- };
898
- let allowsSorting = column.props?.allowsSorting;
899
- let items = useMemo(() => {
900
- let options = [
901
- allowsSorting ? {
902
- label: stringFormatter.format('sortAscending'),
903
- id: 'sort-asc'
904
- } : undefined,
905
- allowsSorting ? {
906
- label: stringFormatter.format('sortDescending'),
907
- id: 'sort-desc'
908
- } : undefined,
909
- {
910
- label: stringFormatter.format('resizeColumn'),
911
- id: 'resize'
912
- }
913
- ];
914
- return options;
915
- // eslint-disable-next-line react-hooks/exhaustive-deps
916
- }, [allowsSorting]);
917
- let isMobile = useIsMobileDevice();
918
-
919
- let resizingColumn = layout.resizingColumn;
920
- let prevResizingColumn = useRef(null);
921
- let timeout = useRef(null);
922
- useEffect(() => {
923
- if (prevResizingColumn.current !== resizingColumn &&
924
- resizingColumn != null &&
925
- resizingColumn === column.key) {
926
- if (timeout.current) {
927
- clearTimeout(timeout.current);
928
- }
929
- // focusSafely won't actually focus because the focus moves from the menuitem to the body during the after transition wait
930
- // without the immediate timeout, Android Chrome doesn't move focus to the resizer
931
- let focusResizer = () => {
932
- resizingRef.current.focus();
933
- onFocusedResizer();
934
- timeout.current = null;
935
- };
936
- if (isMobile) {
937
- timeout.current = setTimeout(focusResizer, 400);
938
- return;
939
- }
940
- timeout.current = setTimeout(focusResizer, 0);
941
- }
942
- prevResizingColumn.current = resizingColumn;
943
- }, [resizingColumn, column.key, isMobile, onFocusedResizer, resizingRef, prevResizingColumn, timeout]);
944
-
945
- // eslint-disable-next-line arrow-body-style
946
- useEffect(() => {
947
- return () => clearTimeout(timeout.current);
948
- }, []);
949
-
950
- let showResizer = !isEmpty && ((headerRowHovered && getInteractionModality() !== 'keyboard') || resizingColumn != null);
951
- let alignment = 'start';
952
- let menuAlign = 'start' as 'start' | 'end';
953
- if (columnProps.align === 'center' || column.colspan > 1) {
954
- alignment = 'center';
955
- } else if (columnProps.align === 'end') {
956
- alignment = 'end';
957
- menuAlign = 'end';
958
- }
959
-
960
41
  return (
961
- <FocusRing focusRingClass={classNames(styles, 'focus-ring')}>
962
- <div
963
- {...mergeProps(...allProps)}
964
- ref={ref}
965
- className={
966
- classNames(
967
- styles,
968
- 'spectrum-Table-headCell',
969
- {
970
- 'is-active': isPressed,
971
- 'is-resizable': columnProps.allowsResizing,
972
- 'is-sortable': columnProps.allowsSorting,
973
- 'is-sorted-desc': state.sortDescriptor?.column === column.key && state.sortDescriptor?.direction === 'descending',
974
- 'is-sorted-asc': state.sortDescriptor?.column === column.key && state.sortDescriptor?.direction === 'ascending',
975
- 'is-hovered': isHovered,
976
- 'focus-ring': isFocusVisible,
977
- 'spectrum-Table-cell--hideHeader': columnProps.hideHeader
978
- },
979
- classNames(
980
- stylesOverrides,
981
- 'react-spectrum-Table-cell',
982
- {
983
- 'react-spectrum-Table-cell--alignCenter': alignment === 'center',
984
- 'react-spectrum-Table-cell--alignEnd': alignment === 'end'
985
- }
986
- )
987
- )
988
- }>
989
- <MenuTrigger onOpenChange={setHeaderMenuOpen} align={menuAlign}>
990
- <TableColumnHeaderButton alignment={alignment} ref={triggerRef} focusProps={focusProps}>
991
- {columnProps.allowsSorting &&
992
- <ArrowDownSmall UNSAFE_className={classNames(styles, 'spectrum-Table-sortedIcon')} />
993
- }
994
- {columnProps.hideHeader ?
995
- <VisuallyHidden>{column.rendered}</VisuallyHidden> :
996
- <div className={classNames(styles, 'spectrum-Table-headerCellText')}>{column.rendered}</div>
997
- }
998
- {
999
- columnProps.allowsResizing && <ChevronDownMedium UNSAFE_className={classNames(styles, 'spectrum-Table-menuChevron')} />
1000
- }
1001
- </TableColumnHeaderButton>
1002
- <Menu onAction={onMenuSelect} minWidth="size-2000" items={items}>
1003
- {(item) => (
1004
- <Item>
1005
- {item.label}
1006
- </Item>
1007
- )}
1008
- </Menu>
1009
- </MenuTrigger>
1010
- <Resizer
1011
- ref={resizingRef}
1012
- column={column}
1013
- showResizer={showResizer}
1014
- onResizeStart={onResizeStart}
1015
- onResize={onResize}
1016
- onResizeEnd={onResizeEnd}
1017
- triggerRef={useUnwrapDOMRef(triggerRef)} />
1018
- <div
1019
- aria-hidden
1020
- className={classNames(
1021
- styles,
1022
- 'spectrum-Table-colResizeIndicator',
1023
- {
1024
- 'spectrum-Table-colResizeIndicator--visible': resizingColumn != null,
1025
- 'spectrum-Table-colResizeIndicator--resizing': resizingColumn === column.key
1026
- }
1027
- )}>
1028
- <div
1029
- className={classNames(
1030
- styles,
1031
- 'spectrum-Table-colResizeNubbin',
1032
- {
1033
- 'spectrum-Table-colResizeNubbin--visible': isInResizeMode && resizingColumn === column.key
1034
- }
1035
- )}>
1036
- <Nubbin />
1037
- </div>
1038
- </div>
1039
- </div>
1040
- </FocusRing>
42
+ <TableViewBase {...props} state={state} ref={ref} />
1041
43
  );
1042
44
  }
1043
45
 
1044
- function TableSelectAllCell({column}) {
1045
- let ref = useRef();
1046
- let {state} = useTableContext();
1047
- let isSingleSelectionMode = state.selectionManager.selectionMode === 'single';
1048
- let {columnHeaderProps} = useTableColumnHeader({
1049
- node: column,
1050
- isVirtualized: true
1051
- }, state, ref);
1052
-
1053
- let {checkboxProps} = useTableSelectAllCheckbox(state);
1054
- let {hoverProps, isHovered} = useHover({});
1055
-
1056
- return (
1057
- <FocusRing focusRingClass={classNames(styles, 'focus-ring')}>
1058
- <div
1059
- {...mergeProps(columnHeaderProps, hoverProps)}
1060
- ref={ref}
1061
- className={
1062
- classNames(
1063
- styles,
1064
- 'spectrum-Table-headCell',
1065
- 'spectrum-Table-checkboxCell',
1066
- {
1067
- 'is-hovered': isHovered
1068
- }
1069
- )
1070
- }>
1071
- {
1072
- /*
1073
- In single selection mode, the checkbox will be hidden.
1074
- So to avoid leaving a column header with no accessible content,
1075
- we use a VisuallyHidden component to include the aria-label from the checkbox,
1076
- which for single selection will be "Select."
1077
- */
1078
- isSingleSelectionMode &&
1079
- <VisuallyHidden>{checkboxProps['aria-label']}</VisuallyHidden>
1080
- }
1081
- <Checkbox
1082
- {...checkboxProps}
1083
- isEmphasized
1084
- UNSAFE_style={isSingleSelectionMode ? {visibility: 'hidden'} : undefined}
1085
- UNSAFE_className={classNames(styles, 'spectrum-Table-checkbox')} />
1086
- </div>
1087
- </FocusRing>
1088
- );
1089
- }
1090
-
1091
- function TableDragHeaderCell({column}) {
1092
- let ref = useRef();
1093
- let {state} = useTableContext();
1094
- let {columnHeaderProps} = useTableColumnHeader({
1095
- node: column,
1096
- isVirtualized: true
1097
- }, state, ref);
1098
- let stringFormatter = useLocalizedStringFormatter(intlMessages);
1099
-
1100
- return (
1101
- <FocusRing focusRingClass={classNames(styles, 'focus-ring')}>
1102
- <div
1103
- {...columnHeaderProps}
1104
- ref={ref}
1105
- className={
1106
- classNames(
1107
- styles,
1108
- 'spectrum-Table-headCell',
1109
- classNames(
1110
- stylesOverrides,
1111
- 'react-spectrum-Table-headCell',
1112
- 'react-spectrum-Table-dragButtonHeadCell'
1113
- )
1114
- )
1115
- }>
1116
- <VisuallyHidden>{stringFormatter.format('drag')}</VisuallyHidden>
1117
- </div>
1118
- </FocusRing>
1119
- );
1120
- }
1121
-
1122
- function TableRowGroup({children, ...otherProps}) {
1123
- let {rowGroupProps} = useTableRowGroup();
1124
-
1125
- return (
1126
- <div {...rowGroupProps} {...otherProps}>
1127
- {children}
1128
- </div>
1129
- );
1130
- }
1131
-
1132
- function DragButton() {
1133
- let {dragButtonProps, dragButtonRef, isFocusVisibleWithin} = useTableRowContext();
1134
- let {visuallyHiddenProps} = useVisuallyHidden();
1135
- return (
1136
- <FocusRing focusRingClass={classNames(stylesOverrides, 'focus-ring')}>
1137
- <div
1138
- {...dragButtonProps as React.HTMLAttributes<HTMLElement>}
1139
- className={
1140
- classNames(
1141
- stylesOverrides,
1142
- 'react-spectrum-Table-dragButton'
1143
- )
1144
- }
1145
- style={!isFocusVisibleWithin ? {...visuallyHiddenProps.style} : {}}
1146
- ref={dragButtonRef}
1147
- draggable="true">
1148
- <ListGripper UNSAFE_className={classNames(stylesOverrides)} />
1149
- </div>
1150
- </FocusRing>
1151
- );
1152
- }
1153
-
1154
- interface TableRowContextValue {
1155
- dragButtonProps: React.HTMLAttributes<HTMLDivElement>,
1156
- dragButtonRef: React.MutableRefObject<undefined>,
1157
- isFocusVisibleWithin: boolean
1158
- }
1159
-
1160
-
1161
- const TableRowContext = React.createContext<TableRowContextValue>(null);
1162
- export function useTableRowContext() {
1163
- return useContext(TableRowContext);
1164
- }
1165
-
1166
- function TableRow({item, children, hasActions, isTableDraggable, isTableDroppable, ...otherProps}) {
1167
- let ref = useRef();
1168
- let {state, layout, dragAndDropHooks, dragState, dropState} = useTableContext();
1169
- let allowsInteraction = state.selectionManager.selectionMode !== 'none' || hasActions;
1170
- let isDisabled = !allowsInteraction || state.disabledKeys.has(item.key);
1171
- let isDroppable = isTableDroppable && !isDisabled;
1172
- let isSelected = state.selectionManager.isSelected(item.key);
1173
- let {rowProps} = useTableRow({
1174
- node: item,
1175
- isVirtualized: true,
1176
- shouldSelectOnPressUp: isTableDraggable
1177
- }, state, ref);
1178
-
1179
- let {pressProps, isPressed} = usePress({isDisabled});
1180
-
1181
- // The row should show the focus background style when any cell inside it is focused.
1182
- // If the row itself is focused, then it should have a blue focus indicator on the left.
1183
- let {
1184
- isFocusVisible: isFocusVisibleWithin,
1185
- focusProps: focusWithinProps
1186
- } = useFocusRing({within: true});
1187
- let {isFocusVisible, focusProps} = useFocusRing();
1188
- let {hoverProps, isHovered} = useHover({isDisabled});
1189
- let isFirstRow = state.collection.rows.find(row => row.level === 1)?.key === item.key;
1190
- let isLastRow = item.nextKey == null;
1191
- // Figure out if the TableView content is equal or greater in height to the container. If so, we'll need to round the bottom
1192
- // border corners of the last row when selected.
1193
- let isFlushWithContainerBottom = false;
1194
- if (isLastRow) {
1195
- if (layout.getContentSize()?.height >= layout.virtualizer?.getVisibleRect().height) {
1196
- isFlushWithContainerBottom = true;
1197
- }
1198
- }
1199
-
1200
- let draggableItem: DraggableItemResult;
1201
- if (isTableDraggable) {
1202
- // eslint-disable-next-line react-hooks/rules-of-hooks
1203
- draggableItem = dragAndDropHooks.useDraggableItem({key: item.key, hasDragButton: true}, dragState);
1204
- if (isDisabled) {
1205
- draggableItem = null;
1206
- }
1207
- }
1208
- let droppableItem: DroppableItemResult;
1209
- let isDropTarget: boolean;
1210
- let dropIndicator: DropIndicatorAria;
1211
- let dropIndicatorRef = useRef();
1212
- if (isTableDroppable) {
1213
- let target = {type: 'item', key: item.key, dropPosition: 'on'} as DropTarget;
1214
- isDropTarget = dropState.isDropTarget(target);
1215
- // eslint-disable-next-line react-hooks/rules-of-hooks
1216
- dropIndicator = dragAndDropHooks.useDropIndicator({target}, dropState, dropIndicatorRef);
1217
- }
1218
-
1219
- let dragButtonRef = React.useRef();
1220
- let {buttonProps: dragButtonProps} = useButton({
1221
- ...draggableItem?.dragButtonProps,
1222
- elementType: 'div'
1223
- }, dragButtonRef);
1224
-
1225
- let props = mergeProps(
1226
- rowProps,
1227
- otherProps,
1228
- focusWithinProps,
1229
- focusProps,
1230
- hoverProps,
1231
- pressProps,
1232
- draggableItem?.dragProps,
1233
- // Remove tab index from list row if performing a screenreader drag. This prevents TalkBack from focusing the row,
1234
- // allowing for single swipe navigation between row drop indicator
1235
- dragAndDropHooks?.isVirtualDragging() && {tabIndex: null}
1236
- ) as HTMLAttributes<HTMLElement> & DOMAttributes<FocusableElement>;
1237
-
1238
- let dropProps = isDroppable ? droppableItem?.dropProps : {'aria-hidden': droppableItem?.dropProps['aria-hidden']};
1239
- let {visuallyHiddenProps} = useVisuallyHidden();
1240
-
1241
- return (
1242
- <TableRowContext.Provider value={{dragButtonProps, dragButtonRef, isFocusVisibleWithin}}>
1243
- {isTableDroppable && isFirstRow &&
1244
- <InsertionIndicator
1245
- rowProps={props}
1246
- key={`${item.key}-before`}
1247
- target={{key: item.key, type: 'item', dropPosition: 'before'}} />
1248
- }
1249
- {isTableDroppable && !dropIndicator?.isHidden &&
1250
- <div role="row" {...visuallyHiddenProps}>
1251
- <div role="gridcell">
1252
- <div role="button" {...dropIndicator?.dropIndicatorProps} ref={dropIndicatorRef} />
1253
- </div>
1254
- </div>
1255
- }
1256
- <div
1257
- {...mergeProps(props, dropProps)}
1258
- ref={ref}
1259
- className={
1260
- classNames(
1261
- styles,
1262
- 'spectrum-Table-row',
1263
- {
1264
- 'is-active': isPressed,
1265
- 'is-selected': isSelected,
1266
- 'spectrum-Table-row--highlightSelection': state.selectionManager.selectionBehavior === 'replace',
1267
- 'is-next-selected': state.selectionManager.isSelected(item.nextKey),
1268
- 'is-focused': isFocusVisibleWithin,
1269
- 'focus-ring': isFocusVisible,
1270
- 'is-hovered': isHovered,
1271
- 'is-disabled': isDisabled,
1272
- 'spectrum-Table-row--firstRow': isFirstRow,
1273
- 'spectrum-Table-row--lastRow': isLastRow,
1274
- 'spectrum-Table-row--isFlushBottom': isFlushWithContainerBottom
1275
- },
1276
- classNames(
1277
- stylesOverrides,
1278
- 'react-spectrum-Table-row',
1279
- {'react-spectrum-Table-row--dropTarget': isDropTarget}
1280
- )
1281
- )
1282
- }>
1283
- {children}
1284
- </div>
1285
- {isTableDroppable &&
1286
- <InsertionIndicator
1287
- rowProps={props}
1288
- key={`${item.key}-after`}
1289
- target={{key: item.key, type: 'item', dropPosition: 'after'}} />
1290
- }
1291
- </TableRowContext.Provider>
1292
- );
1293
- }
1294
-
1295
- function TableHeaderRow({item, children, style, ...props}) {
1296
- let {state, headerMenuOpen} = useTableContext();
1297
- let ref = useRef();
1298
- let {rowProps} = useTableHeaderRow({node: item, isVirtualized: true}, state, ref);
1299
- let {hoverProps} = useHover({...props, isDisabled: headerMenuOpen});
1300
-
1301
- return (
1302
- <div {...mergeProps(rowProps, hoverProps)} ref={ref} style={style}>
1303
- {children}
1304
- </div>
1305
- );
1306
- }
1307
-
1308
- function TableDragCell({cell}) {
1309
- let ref = useRef();
1310
- let {state, isTableDraggable} = useTableContext();
1311
- let isDisabled = state.disabledKeys.has(cell.parentKey);
1312
- let {gridCellProps} = useTableCell({
1313
- node: cell,
1314
- isVirtualized: true
1315
- }, state, ref);
1316
-
1317
-
1318
- return (
1319
- <FocusRing focusRingClass={classNames(styles, 'focus-ring')}>
1320
- <div
1321
- {...gridCellProps}
1322
- ref={ref}
1323
- className={
1324
- classNames(
1325
- styles,
1326
- 'spectrum-Table-cell',
1327
- {
1328
- 'is-disabled': isDisabled
1329
- },
1330
- classNames(
1331
- stylesOverrides,
1332
- 'react-spectrum-Table-cell',
1333
- 'react-spectrum-Table-dragButtonCell'
1334
- )
1335
- )
1336
- }>
1337
- {isTableDraggable && !isDisabled && <DragButton />}
1338
- </div>
1339
- </FocusRing>
1340
- );
1341
- }
1342
-
1343
- function TableCheckboxCell({cell}) {
1344
- let ref = useRef();
1345
- let {state} = useTableContext();
1346
- let isDisabled = state.disabledKeys.has(cell.parentKey);
1347
- let {gridCellProps} = useTableCell({
1348
- node: cell,
1349
- isVirtualized: true
1350
- }, state, ref);
1351
-
1352
- let {checkboxProps} = useTableSelectionCheckbox({key: cell.parentKey}, state);
1353
-
1354
- return (
1355
- <FocusRing focusRingClass={classNames(styles, 'focus-ring')}>
1356
- <div
1357
- {...gridCellProps}
1358
- ref={ref}
1359
- className={
1360
- classNames(
1361
- styles,
1362
- 'spectrum-Table-cell',
1363
- 'spectrum-Table-checkboxCell',
1364
- {
1365
- 'is-disabled': isDisabled
1366
- },
1367
- classNames(
1368
- stylesOverrides,
1369
- 'react-spectrum-Table-cell'
1370
- )
1371
- )
1372
- }>
1373
- {state.selectionManager.selectionMode !== 'none' &&
1374
- <Checkbox
1375
- {...checkboxProps}
1376
- isEmphasized
1377
- isDisabled={isDisabled}
1378
- UNSAFE_className={classNames(styles, 'spectrum-Table-checkbox')} />
1379
- }
1380
- </div>
1381
- </FocusRing>
1382
- );
1383
- }
1384
-
1385
- function TableCell({cell}) {
1386
- let {state} = useTableContext();
1387
- let ref = useRef();
1388
- let columnProps = cell.column.props as SpectrumColumnProps<unknown>;
1389
- let isDisabled = state.disabledKeys.has(cell.parentKey);
1390
- let {gridCellProps} = useTableCell({
1391
- node: cell,
1392
- isVirtualized: true
1393
- }, state, ref);
1394
-
1395
- return (
1396
- <FocusRing focusRingClass={classNames(styles, 'focus-ring')}>
1397
- <div
1398
- {...gridCellProps}
1399
- ref={ref}
1400
- className={
1401
- classNames(
1402
- styles,
1403
- 'spectrum-Table-cell',
1404
- {
1405
- 'spectrum-Table-cell--divider': columnProps.showDivider && cell.column.nextKey !== null,
1406
- 'spectrum-Table-cell--hideHeader': columnProps.hideHeader,
1407
- 'is-disabled': isDisabled
1408
- },
1409
- classNames(
1410
- stylesOverrides,
1411
- 'react-spectrum-Table-cell',
1412
- {
1413
- 'react-spectrum-Table-cell--alignStart': columnProps.align === 'start',
1414
- 'react-spectrum-Table-cell--alignCenter': columnProps.align === 'center',
1415
- 'react-spectrum-Table-cell--alignEnd': columnProps.align === 'end'
1416
- }
1417
- )
1418
- )
1419
- }>
1420
- <span
1421
- className={
1422
- classNames(
1423
- styles,
1424
- 'spectrum-Table-cellContents'
1425
- )
1426
- }>
1427
- {cell.rendered}
1428
- </span>
1429
- </div>
1430
- </FocusRing>
1431
- );
1432
- }
1433
-
1434
- function CenteredWrapper({children}) {
1435
- let {state} = useTableContext();
1436
- return (
1437
- <div
1438
- role="row"
1439
- aria-rowindex={state.collection.headerRows.length + state.collection.size + 1}
1440
- className={classNames(stylesOverrides, 'react-spectrum-Table-centeredWrapper')}>
1441
- <div role="rowheader" aria-colspan={state.collection.columns.length}>
1442
- {children}
1443
- </div>
1444
- </div>
1445
- );
1446
- }
1447
-
1448
- /**
1449
- * Tables are containers for displaying information. They allow users to quickly scan, sort, compare, and take action on large amounts of data.
1450
- */
1451
- const _TableView = React.forwardRef(TableView) as <T>(props: SpectrumTableProps<T> & {ref?: DOMRef<HTMLDivElement>}) => ReactElement;
1452
-
46
+ const _TableView = React.forwardRef(TableView) as <T>(props: TableProps<T> & {ref?: DOMRef<HTMLDivElement>}) => ReactElement;
1453
47
  export {_TableView as TableView};