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