@react-spectrum/table 3.12.11-nightly.4649 → 3.12.11-nightly.4654

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.
@@ -35,13 +35,13 @@ import {InsertionIndicator} from './InsertionIndicator';
35
35
  // @ts-ignore
36
36
  import intlMessages from '../intl/*.json';
37
37
  import {Item, Menu, MenuTrigger} from '@react-spectrum/menu';
38
+ import {LayoutInfo, ReusableView, useVirtualizerState} from '@react-stately/virtualizer';
38
39
  import {layoutInfoToStyle, ScrollView, setScrollLeft, useVirtualizer, VirtualizerItem} from '@react-aria/virtualizer';
39
40
  import ListGripper from '@spectrum-icons/ui/ListGripper';
40
41
  import {Nubbin} from './Nubbin';
41
42
  import {ProgressCircle} from '@react-spectrum/progress';
42
43
  import React, {DOMAttributes, HTMLAttributes, ReactElement, ReactNode, useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react';
43
44
  import {Resizer} from './Resizer';
44
- import {ReusableView, useVirtualizerState} from '@react-stately/virtualizer';
45
45
  import {RootDropIndicator} from './RootDropIndicator';
46
46
  import {DragPreview as SpectrumDragPreview} from './DragPreview';
47
47
  import {SpectrumTableProps} from './TableViewWrapper';
@@ -122,7 +122,8 @@ export interface TableContextValue<T> {
122
122
  onResize: (widths: Map<Key, ColumnSize>) => void,
123
123
  onResizeEnd: (widths: Map<Key, ColumnSize>) => void,
124
124
  headerMenuOpen: boolean,
125
- setHeaderMenuOpen: (val: boolean) => void
125
+ setHeaderMenuOpen: (val: boolean) => void,
126
+ renderEmptyState?: () => ReactElement
126
127
  }
127
128
 
128
129
  export const TableContext = React.createContext<TableContextValue<unknown>>(null);
@@ -166,7 +167,6 @@ function TableViewBase<T extends object>(props: TableBaseProps<T>, ref: DOMRef<H
166
167
  }, [isTableDraggable, isTableDroppable, state]);
167
168
 
168
169
  let {styleProps} = useStyleProps(props);
169
- let {direction} = useLocale();
170
170
  let {scale} = useProvider();
171
171
 
172
172
  const getDefaultWidth = useCallback(({props: {hideHeader, isSelectionCell, showDivider, isDragButtonCell}}: GridNode<T>): ColumnSize | null | undefined => {
@@ -203,7 +203,6 @@ function TableViewBase<T extends object>(props: TableBaseProps<T>, ref: DOMRef<H
203
203
  let domRef = useDOMRef(ref);
204
204
  let headerRef = useRef<HTMLDivElement>();
205
205
  let bodyRef = useRef<HTMLDivElement>();
206
- let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-spectrum/table');
207
206
 
208
207
  let density = props.density || 'regular';
209
208
  let columnLayout = useMemo(
@@ -278,25 +277,21 @@ function TableViewBase<T extends object>(props: TableBaseProps<T>, ref: DOMRef<H
278
277
  ...props,
279
278
  isVirtualized: true,
280
279
  layout,
281
- onRowAction: onAction
280
+ onRowAction: onAction,
281
+ scrollRef: bodyRef
282
282
  }, state, domRef);
283
283
  let [headerMenuOpen, setHeaderMenuOpen] = useState(false);
284
284
  let [headerRowHovered, setHeaderRowHovered] = useState(false);
285
285
 
286
286
  // This overrides collection view's renderWrapper to support DOM hierarchy.
287
287
  type View = ReusableView<GridNode<T>, ReactNode>;
288
- let renderWrapper = (parent: View, reusableView: View, children: View[], renderChildren: (views: View[]) => ReactElement[]) => {
289
- let style = layoutInfoToStyle(reusableView.layoutInfo, direction, parent && parent.layoutInfo);
290
- if (style.overflow === 'hidden') {
291
- style.overflow = 'visible'; // needed to support position: sticky
292
- }
293
-
288
+ let renderWrapper = useCallback((parent: View, reusableView: View, children: View[], renderChildren: (views: View[]) => ReactElement[]) => {
294
289
  if (reusableView.viewType === 'rowgroup') {
295
290
  return (
296
- <TableRowGroup key={reusableView.key} style={style}>
297
- {isTableDroppable &&
298
- <RootDropIndicator key="root" />
299
- }
291
+ <TableRowGroup
292
+ key={reusableView.key}
293
+ layoutInfo={reusableView.layoutInfo}
294
+ parent={parent?.layoutInfo}>
300
295
  {renderChildren(children)}
301
296
  </TableRowGroup>
302
297
  );
@@ -306,7 +301,8 @@ function TableViewBase<T extends object>(props: TableBaseProps<T>, ref: DOMRef<H
306
301
  return (
307
302
  <TableHeader
308
303
  key={reusableView.key}
309
- style={style}>
304
+ layoutInfo={reusableView.layoutInfo}
305
+ parent={parent?.layoutInfo}>
310
306
  {renderChildren(children)}
311
307
  </TableHeader>
312
308
  );
@@ -317,10 +313,8 @@ function TableViewBase<T extends object>(props: TableBaseProps<T>, ref: DOMRef<H
317
313
  <TableRow
318
314
  key={reusableView.key}
319
315
  item={reusableView.content}
320
- style={style}
321
- hasActions={onAction}
322
- isTableDroppable={isTableDroppable}
323
- isTableDraggable={isTableDraggable}>
316
+ layoutInfo={reusableView.layoutInfo}
317
+ parent={parent?.layoutInfo}>
324
318
  {renderChildren(children)}
325
319
  </TableRow>
326
320
  );
@@ -331,46 +325,26 @@ function TableViewBase<T extends object>(props: TableBaseProps<T>, ref: DOMRef<H
331
325
  <TableHeaderRow
332
326
  onHoverChange={setHeaderRowHovered}
333
327
  key={reusableView.key}
334
- style={style}
328
+ layoutInfo={reusableView.layoutInfo}
329
+ parent={parent?.layoutInfo}
335
330
  item={reusableView.content}>
336
331
  {renderChildren(children)}
337
332
  </TableHeaderRow>
338
333
  );
339
334
  }
340
- let isDropTarget: boolean;
341
- let isRootDroptarget: boolean;
342
- if (isTableDroppable) {
343
- if (parent.content) {
344
- isDropTarget = dropState.isDropTarget({type: 'item', dropPosition: 'on', key: parent.content.key});
345
- }
346
- isRootDroptarget = dropState.isDropTarget({type: 'root'});
347
- }
348
-
335
+
349
336
  return (
350
- <VirtualizerItem
337
+ <TableCellWrapper
351
338
  key={reusableView.key}
352
339
  layoutInfo={reusableView.layoutInfo}
353
340
  virtualizer={reusableView.virtualizer}
354
- parent={parent?.layoutInfo}
355
- className={
356
- classNames(
357
- styles,
358
- 'spectrum-Table-cellWrapper',
359
- classNames(
360
- stylesOverrides,
361
- {
362
- 'react-spectrum-Table-cellWrapper': !reusableView.layoutInfo.estimatedSize,
363
- 'react-spectrum-Table-cellWrapper--dropTarget': isDropTarget || isRootDroptarget
364
- }
365
- )
366
- )
367
- }>
341
+ parent={parent}>
368
342
  {reusableView.rendered}
369
- </VirtualizerItem>
343
+ </TableCellWrapper>
370
344
  );
371
- };
345
+ }, []);
372
346
 
373
- let renderView = (type: string, item: GridNode<T>) => {
347
+ let renderView = useCallback((type: string, item: GridNode<T>) => {
374
348
  switch (type) {
375
349
  case 'header':
376
350
  case 'rowgroup':
@@ -417,34 +391,19 @@ function TableViewBase<T extends object>(props: TableBaseProps<T>, ref: DOMRef<H
417
391
  }
418
392
 
419
393
  if (item.props.allowsResizing && !item.hasChildNodes) {
420
- return <ResizableTableColumnHeader tableRef={domRef} column={item} />;
394
+ return <ResizableTableColumnHeader column={item} />;
421
395
  }
422
396
 
423
397
  return (
424
398
  <TableColumnHeader column={item} />
425
399
  );
426
400
  case 'loader':
427
- return (
428
- <CenteredWrapper>
429
- <ProgressCircle
430
- isIndeterminate
431
- aria-label={state.collection.size > 0 ? stringFormatter.format('loadingMore') : stringFormatter.format('loading')} />
432
- </CenteredWrapper>
433
- );
401
+ return <LoadingState />;
434
402
  case 'empty': {
435
- let emptyState = props.renderEmptyState ? props.renderEmptyState() : null;
436
- if (emptyState == null) {
437
- return null;
438
- }
439
-
440
- return (
441
- <CenteredWrapper>
442
- {emptyState}
443
- </CenteredWrapper>
444
- );
403
+ return <EmptyState />;
445
404
  }
446
405
  }
447
- };
406
+ }, []);
448
407
 
449
408
  let [isVerticalScrollbarVisible, setVerticalScollbarVisible] = useState(false);
450
409
  let [isHorizontalScrollbarVisible, setHorizontalScollbarVisible] = useState(false);
@@ -489,7 +448,27 @@ function TableViewBase<T extends object>(props: TableBaseProps<T>, ref: DOMRef<H
489
448
  );
490
449
 
491
450
  return (
492
- <TableContext.Provider value={{state, dragState, dropState, dragAndDropHooks, isTableDraggable, isTableDroppable, layout, onResizeStart, onResize: props.onResize, onResizeEnd, headerRowHovered, isInResizeMode, setIsInResizeMode, isEmpty, onFocusedResizer, headerMenuOpen, setHeaderMenuOpen}}>
451
+ <TableContext.Provider
452
+ value={{
453
+ state,
454
+ dragState,
455
+ dropState,
456
+ dragAndDropHooks,
457
+ isTableDraggable,
458
+ isTableDroppable,
459
+ layout,
460
+ onResizeStart,
461
+ onResize: props.onResize,
462
+ onResizeEnd,
463
+ headerRowHovered,
464
+ isInResizeMode,
465
+ setIsInResizeMode,
466
+ isEmpty,
467
+ onFocusedResizer,
468
+ headerMenuOpen,
469
+ setHeaderMenuOpen,
470
+ renderEmptyState: props.renderEmptyState
471
+ }}>
493
472
  <TableVirtualizer
494
473
  {...mergedProps}
495
474
  {...styleProps}
@@ -512,7 +491,9 @@ function TableViewBase<T extends object>(props: TableBaseProps<T>, ref: DOMRef<H
512
491
  styleProps.className
513
492
  )
514
493
  }
515
- layout={layout}
494
+ // This should be `tableLayout` rather than `layout` so it doesn't
495
+ // change objects and invalidate virtualizer.
496
+ layout={tableLayout}
516
497
  collection={state.collection}
517
498
  focusedKey={focusedKey}
518
499
  renderView={renderView}
@@ -549,14 +530,7 @@ function TableVirtualizer(props) {
549
530
  let loadingState = collection.body.props.loadingState;
550
531
  let isLoading = loadingState === 'loading' || loadingState === 'loadingMore';
551
532
  let onLoadMore = collection.body.props.onLoadMore;
552
- let transitionDuration = 220;
553
- if (isLoading) {
554
- transitionDuration = 160;
555
- }
556
- if (layout.resizingColumn != null) {
557
- // while resizing, prop changes should not cause animations
558
- transitionDuration = 0;
559
- }
533
+
560
534
  let state = useVirtualizerState<object, ReactNode, ReactNode>({
561
535
  layout,
562
536
  collection,
@@ -566,39 +540,18 @@ function TableVirtualizer(props) {
566
540
  bodyRef.current.scrollTop = rect.y;
567
541
  setScrollLeft(bodyRef.current, direction, rect.x);
568
542
  },
569
- transitionDuration
543
+ persistedKeys: useMemo(() => focusedKey ? new Set([focusedKey]) : new Set(), [focusedKey])
570
544
  });
571
545
 
572
- let scrollToItem = useCallback((key) => {
573
- let item = collection.getItem(key);
574
- let column = collection.columns[0];
575
- let virtualizer = state.virtualizer;
576
-
577
- virtualizer.scrollToItem(key, {
578
- duration: 0,
579
- // Prevent scrolling to the top when clicking on column headers.
580
- shouldScrollY: item?.type !== 'column',
581
- // Offset scroll position by width of selection cell
582
- // (which is sticky and will overlap the cell we're scrolling to).
583
- offsetX: column.props.isSelectionCell || column.props.isDragButtonCell
584
- ? layout.getColumnWidth(column.key)
585
- : 0
586
- });
587
-
588
- // Sync the scroll positions of the column headers and the body so scrollIntoViewport can
589
- // properly decide if the column is outside the viewport or not
590
- headerRef.current.scrollLeft = bodyRef.current.scrollLeft;
591
- }, [collection, bodyRef, headerRef, layout, state.virtualizer]);
592
-
593
546
  let memoedVirtualizerProps = useMemo(() => ({
594
547
  tabIndex: otherProps.tabIndex,
595
548
  focusedKey,
596
- scrollToItem,
597
549
  isLoading,
598
550
  onLoadMore
599
- }), [otherProps.tabIndex, focusedKey, scrollToItem, isLoading, onLoadMore]);
551
+ }), [otherProps.tabIndex, focusedKey, isLoading, onLoadMore]);
600
552
 
601
553
  let {virtualizerProps, scrollViewProps: {onVisibleRectChange}} = useVirtualizer(memoedVirtualizerProps, state, domRef);
554
+ let onVisibleRectChangeMemo = useMemo(() => chain(onVisibleRectChange, onVisibleRectChangeProp), [onVisibleRectChange, onVisibleRectChangeProp]);
602
555
 
603
556
  // this effect runs whenever the contentSize changes, it doesn't matter what the content size is
604
557
  // only that it changes in a resize, and when that happens, we want to sync the body to the
@@ -638,6 +591,12 @@ function TableVirtualizer(props) {
638
591
  isVirtualDragging && {tabIndex: null}
639
592
  );
640
593
 
594
+ let firstColumn = collection.columns[0];
595
+ let scrollPadding = 0;
596
+ if (firstColumn.props.isSelectionCell || firstColumn.props.isDragButtonCell) {
597
+ scrollPadding = layout.getColumnWidth(firstColumn.key);
598
+ }
599
+
641
600
  return (
642
601
  <VirtualizerContext.Provider value={resizingColumn}>
643
602
  <FocusScope>
@@ -652,7 +611,7 @@ function TableVirtualizer(props) {
652
611
  overflow: 'hidden',
653
612
  position: 'relative',
654
613
  willChange: state.isScrolling ? 'scroll-position' : undefined,
655
- transition: state.isAnimating ? `none ${state.virtualizer.transitionDuration}ms` : undefined
614
+ scrollPaddingInlineStart: scrollPadding
656
615
  }}
657
616
  ref={headerRef}>
658
617
  {state.visibleViews[0]}
@@ -677,11 +636,14 @@ function TableVirtualizer(props) {
677
636
  )
678
637
  }
679
638
  tabIndex={isVirtualDragging ? null : -1}
680
- style={{flex: 1}}
681
- innerStyle={{overflow: 'visible', transition: state.isAnimating ? `none ${state.virtualizer.transitionDuration}ms` : undefined}}
639
+ style={{
640
+ flex: 1,
641
+ scrollPaddingInlineStart: scrollPadding
642
+ }}
643
+ innerStyle={{overflow: 'visible'}}
682
644
  ref={bodyRef}
683
645
  contentSize={state.contentSize}
684
- onVisibleRectChange={chain(onVisibleRectChange, onVisibleRectChangeProp)}
646
+ onVisibleRectChange={onVisibleRectChangeMemo}
685
647
  onScrollStart={state.startScrolling}
686
648
  onScrollEnd={state.endScrolling}
687
649
  onScroll={onScroll}>
@@ -696,11 +658,21 @@ function TableVirtualizer(props) {
696
658
  );
697
659
  }
698
660
 
699
- function TableHeader({children, ...otherProps}) {
661
+ function useStyle(layoutInfo: LayoutInfo, parent: LayoutInfo | null) {
662
+ let {direction} = useLocale();
663
+ let style = layoutInfoToStyle(layoutInfo, direction, parent);
664
+ if (style.overflow === 'hidden') {
665
+ style.overflow = 'visible'; // needed to support position: sticky
666
+ }
667
+ return style;
668
+ }
669
+
670
+ function TableHeader({children, layoutInfo, parent, ...otherProps}) {
700
671
  let {rowGroupProps} = useTableRowGroup();
672
+ let style = useStyle(layoutInfo, parent);
701
673
 
702
674
  return (
703
- <div {...rowGroupProps} {...otherProps} className={classNames(styles, 'spectrum-Table-head')}>
675
+ <div {...rowGroupProps} {...otherProps} className={classNames(styles, 'spectrum-Table-head')} style={style}>
704
676
  {children}
705
677
  </div>
706
678
  );
@@ -1047,11 +1019,16 @@ function TableDragHeaderCell({column}) {
1047
1019
  );
1048
1020
  }
1049
1021
 
1050
- function TableRowGroup({children, ...otherProps}) {
1022
+ function TableRowGroup({children, layoutInfo, parent, ...otherProps}) {
1051
1023
  let {rowGroupProps} = useTableRowGroup();
1024
+ let {isTableDroppable} = useContext(TableContext);
1025
+ let style = useStyle(layoutInfo, parent);
1052
1026
 
1053
1027
  return (
1054
- <div {...rowGroupProps} {...otherProps}>
1028
+ <div {...rowGroupProps} style={style} {...otherProps}>
1029
+ {isTableDroppable &&
1030
+ <RootDropIndicator key="root" />
1031
+ }
1055
1032
  {children}
1056
1033
  </div>
1057
1034
  );
@@ -1091,19 +1068,18 @@ export function useTableRowContext() {
1091
1068
  return useContext(TableRowContext);
1092
1069
  }
1093
1070
 
1094
- function TableRow({item, children, hasActions, isTableDraggable, isTableDroppable, ...otherProps}) {
1071
+ function TableRow({item, children, layoutInfo, parent, ...otherProps}) {
1095
1072
  let ref = useRef();
1096
- let {state, layout, dragAndDropHooks, dragState, dropState} = useTableContext();
1097
- let allowsInteraction = state.selectionManager.selectionMode !== 'none' || hasActions;
1098
- let isDisabled = !allowsInteraction || state.disabledKeys.has(item.key);
1099
- let isDroppable = isTableDroppable && !isDisabled;
1073
+ let {state, layout, dragAndDropHooks, isTableDraggable, isTableDroppable, dragState, dropState} = useTableContext();
1100
1074
  let isSelected = state.selectionManager.isSelected(item.key);
1101
- let {rowProps} = useTableRow({
1075
+ let {rowProps, hasAction, allowsSelection} = useTableRow({
1102
1076
  node: item,
1103
1077
  isVirtualized: true,
1104
1078
  shouldSelectOnPressUp: isTableDraggable
1105
1079
  }, state, ref);
1106
1080
 
1081
+ let isDisabled = !allowsSelection && !hasAction;
1082
+ let isDroppable = isTableDroppable && !isDisabled;
1107
1083
  let {pressProps, isPressed} = usePress({isDisabled});
1108
1084
 
1109
1085
  // The row should show the focus background style when any cell inside it is focused.
@@ -1120,7 +1096,7 @@ function TableRow({item, children, hasActions, isTableDraggable, isTableDroppabl
1120
1096
  // border corners of the last row when selected.
1121
1097
  let isFlushWithContainerBottom = false;
1122
1098
  if (isLastRow) {
1123
- if (layout.getContentSize()?.height >= layout.virtualizer?.getVisibleRect().height) {
1099
+ if (layout.getContentSize()?.height >= layout.virtualizer?.visibleRect.height) {
1124
1100
  isFlushWithContainerBottom = true;
1125
1101
  }
1126
1102
  }
@@ -1150,9 +1126,12 @@ function TableRow({item, children, hasActions, isTableDraggable, isTableDroppabl
1150
1126
  elementType: 'div'
1151
1127
  }, dragButtonRef);
1152
1128
 
1129
+ let style = useStyle(layoutInfo, parent);
1130
+
1153
1131
  let props = mergeProps(
1154
1132
  rowProps,
1155
1133
  otherProps,
1134
+ {style},
1156
1135
  focusWithinProps,
1157
1136
  focusProps,
1158
1137
  hoverProps,
@@ -1220,11 +1199,12 @@ function TableRow({item, children, hasActions, isTableDraggable, isTableDroppabl
1220
1199
  );
1221
1200
  }
1222
1201
 
1223
- function TableHeaderRow({item, children, style, ...props}) {
1202
+ function TableHeaderRow({item, children, layoutInfo, parent, ...props}) {
1224
1203
  let {state, headerMenuOpen} = useTableContext();
1225
1204
  let ref = useRef();
1226
1205
  let {rowProps} = useTableHeaderRow({node: item, isVirtualized: true}, state, ref);
1227
1206
  let {hoverProps} = useHover({...props, isDisabled: headerMenuOpen});
1207
+ let style = useStyle(layoutInfo, parent);
1228
1208
 
1229
1209
  return (
1230
1210
  <div {...mergeProps(rowProps, hoverProps)} ref={ref} style={style}>
@@ -1378,6 +1358,40 @@ function TableCell({cell}) {
1378
1358
  );
1379
1359
  }
1380
1360
 
1361
+ function TableCellWrapper({layoutInfo, virtualizer, parent, children}) {
1362
+ let {isTableDroppable, dropState} = useContext(TableContext);
1363
+ let isDropTarget: boolean;
1364
+ let isRootDroptarget: boolean;
1365
+ if (isTableDroppable) {
1366
+ if (parent.content) {
1367
+ isDropTarget = dropState.isDropTarget({type: 'item', dropPosition: 'on', key: parent.content.key});
1368
+ }
1369
+ isRootDroptarget = dropState.isDropTarget({type: 'root'});
1370
+ }
1371
+
1372
+ return (
1373
+ <VirtualizerItem
1374
+ layoutInfo={layoutInfo}
1375
+ virtualizer={virtualizer}
1376
+ parent={parent?.layoutInfo}
1377
+ className={
1378
+ useMemo(() => classNames(
1379
+ styles,
1380
+ 'spectrum-Table-cellWrapper',
1381
+ classNames(
1382
+ stylesOverrides,
1383
+ {
1384
+ 'react-spectrum-Table-cellWrapper': !layoutInfo.estimatedSize,
1385
+ 'react-spectrum-Table-cellWrapper--dropTarget': isDropTarget || isRootDroptarget
1386
+ }
1387
+ )
1388
+ ), [layoutInfo.estimatedSize, isDropTarget, isRootDroptarget])
1389
+ }>
1390
+ {children}
1391
+ </VirtualizerItem>
1392
+ );
1393
+ }
1394
+
1381
1395
  function ExpandableRowChevron({cell}) {
1382
1396
  // TODO: move some/all of the chevron button setup into a separate hook?
1383
1397
  let {direction} = useLocale();
@@ -1424,6 +1438,32 @@ function ExpandableRowChevron({cell}) {
1424
1438
  );
1425
1439
  }
1426
1440
 
1441
+ function LoadingState() {
1442
+ let {state} = useContext(TableContext);
1443
+ let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-spectrum/table');
1444
+ return (
1445
+ <CenteredWrapper>
1446
+ <ProgressCircle
1447
+ isIndeterminate
1448
+ aria-label={state.collection.size > 0 ? stringFormatter.format('loadingMore') : stringFormatter.format('loading')} />
1449
+ </CenteredWrapper>
1450
+ );
1451
+ }
1452
+
1453
+ function EmptyState() {
1454
+ let {renderEmptyState} = useContext(TableContext);
1455
+ let emptyState = renderEmptyState ? renderEmptyState() : null;
1456
+ if (emptyState == null) {
1457
+ return null;
1458
+ }
1459
+
1460
+ return (
1461
+ <CenteredWrapper>
1462
+ {emptyState}
1463
+ </CenteredWrapper>
1464
+ );
1465
+ }
1466
+
1427
1467
  function CenteredWrapper({children}) {
1428
1468
  let {state} = useTableContext();
1429
1469
  let rowProps;