diff --git a/packages/@react-aria/table/src/useTableColumnResize.ts b/packages/@react-aria/table/src/useTableColumnResize.ts index e09fb126377..bede08217cb 100644 --- a/packages/@react-aria/table/src/useTableColumnResize.ts +++ b/packages/@react-aria/table/src/useTableColumnResize.ts @@ -10,13 +10,24 @@ * governing permissions and limitations under the License. */ +import {ColumnResizeState} from '@react-stately/table'; import {focusSafely, useFocusable} from '@react-aria/focus'; +import {GridNode} from '@react-types/grid'; +import {HTMLAttributes, RefObject, useRef} from 'react'; import {mergeProps} from '@react-aria/utils'; import {useKeyboard, useMove} from '@react-aria/interactions'; import {useLocale} from '@react-aria/i18n'; -import {useRef} from 'react'; -export function useTableColumnResize(state, item, ref): any { +interface ResizerAria { + resizerProps: HTMLAttributes +} + +interface ResizerProps { + column: GridNode +} + +export function useTableColumnResize(props: ResizerProps, state: ColumnResizeState, ref: RefObject): ResizerAria { + let {column: item} = props; const stateRef = useRef(null); // keep track of what the cursor on the body is so it can be restored back to that when done resizing const cursor = useRef(null); @@ -32,7 +43,7 @@ export function useTableColumnResize(state, item, ref): any { } if (e.key === 'Escape' || e.key === 'Enter' || e.key === ' ') { // switch focus back to the column header on escape - const columnHeader = ref.current.previousSibling; + const columnHeader = ref.current.previousSibling as HTMLElement; if (columnHeader) { focusSafely(columnHeader); } diff --git a/packages/@react-spectrum/table/src/Resizer.tsx b/packages/@react-spectrum/table/src/Resizer.tsx index 14acf6da562..b42e3fad2f5 100644 --- a/packages/@react-spectrum/table/src/Resizer.tsx +++ b/packages/@react-spectrum/table/src/Resizer.tsx @@ -8,9 +8,9 @@ import {useTableContext} from './TableView'; function Resizer(props, ref) { - const {item} = props; - let {state} = useTableContext(); - let {resizerProps} = useTableColumnResize(state, item, ref); + const {column} = props; + let {columnState} = useTableContext(); + let {resizerProps} = useTableColumnResize({column}, columnState, ref); return (
+ aria-labelledby={column.key} + aria-valuenow={columnState.getColumnWidth(column.key)} + aria-valuemin={columnState.getColumnMinWidth(column.key)} + aria-valuemax={columnState.getColumnMaxWidth(column.key)} /> ); } diff --git a/packages/@react-spectrum/table/src/TableView.tsx b/packages/@react-spectrum/table/src/TableView.tsx index 711711e5bcb..3784c2193fa 100644 --- a/packages/@react-spectrum/table/src/TableView.tsx +++ b/packages/@react-spectrum/table/src/TableView.tsx @@ -14,6 +14,7 @@ import ArrowDownSmall from '@spectrum-icons/ui/ArrowDownSmall'; import {chain, mergeProps, useLayoutEffect} from '@react-aria/utils'; import {Checkbox} from '@react-spectrum/checkbox'; import {classNames, useDOMRef, useStyleProps} from '@react-spectrum/utils'; +import {ColumnResizeState, TableState, useTableColumnResizeState, useTableState} from '@react-stately/table'; import {DOMRef} from '@react-types/shared'; import {FocusRing, focusSafely, useFocusRing} from '@react-aria/focus'; import {GridNode} from '@react-types/grid'; @@ -29,8 +30,6 @@ import {SpectrumColumnProps, SpectrumTableProps} from '@react-types/table'; import styles from '@adobe/spectrum-css-temp/components/table/vars.css'; import stylesOverrides from './table.css'; import {TableLayout} from '@react-stately/layout'; -import {TableState, useTableState} from '@react-stately/table'; -import {TableView_DEPRECATED} from './TableView_DEPRECATED'; import {Tooltip, TooltipTrigger} from '@react-spectrum/tooltip'; import {useButton} from '@react-aria/button'; import {useHover} from '@react-aria/interactions'; @@ -81,7 +80,8 @@ const SELECTION_CELL_DEFAULT_WIDTH = { interface TableContextValue { state: TableState, - layout: TableLayout + layout: TableLayout, + columnState: ColumnResizeState } const TableContext = React.createContext>(null); @@ -95,6 +95,7 @@ function TableView(props: SpectrumTableProps, ref: DOMRef { @@ -109,10 +110,11 @@ function TableView(props: SpectrumTableProps, ref: DOMRef(props: SpectrumTableProps, ref: DOMRef(props: SpectrumTableProps, ref: DOMRef; + return ; } return ( @@ -303,7 +304,7 @@ function TableView(props: SpectrumTableProps, ref: DOMRef + (props: SpectrumTableProps, ref: DOMRef(props: SpectrumTableProps, ref: DOMRef + getColumnWidth={columnState.getColumnWidth} /> ); } @@ -391,10 +392,7 @@ function TableVirtualizer({layout, collection, focusedKey, renderView, renderWra }, [bodyRef]); let onVisibleRectChange = useCallback((rect: Rect) => { - // setting the table width will recalculate column widths which we only want to do once the virtualizer is done initializing - if (state.virtualizer.contentSize.height > 0) { - setTableWidth(rect.width); - } + setTableWidth(rect.width); state.setVisibleRect(rect); @@ -477,12 +475,14 @@ function TableColumnHeader(props) { } let {hoverProps, isHovered} = useHover({}); - - const allProps = [columnHeaderProps, hoverProps]; if (columnProps.allowsResizing) { - allProps.push(buttonProps); + // if we allow resizing, override the usePress that useTableColumnHeader generates so that clicking brings up the menu + // instead of sorting the column immediately + columnHeaderProps = {...columnHeaderProps, ...buttonProps}; } + const allProps = [columnHeaderProps, hoverProps]; + return (
{ switch (key) { case 'sort-asc': - state.sort(item.key, 'ascending'); + state.sort(column.key, 'ascending'); break; case 'sort-desc': - state.sort(item.key, 'descending'); + state.sort(column.key, 'descending'); break; case 'resize': // focusResizer, needs timeout so that it happens after the animation timeout for menu close @@ -542,28 +543,43 @@ function ResizableTableColumnHeader({item, state}) { break; } }; + let allowsSorting = column.props?.allowsSorting; + let items = useMemo(() => { + let options = { + sortAscending: allowsSorting && { + label: formatMessage('sortAscending'), + id: 'sort-asc' + }, + sortDescending: allowsSorting && { + label: formatMessage('sortDescending'), + id: 'sort-desc' + }, + resize: { + label: formatMessage('resizeColumn'), + id: 'resize' + } + }; + return Object.keys(options).reduce((acc, key) => { + if (options[key]) { + acc.push(options[key]); + } + return acc; + }, []); + }, [allowsSorting]); return ( <> - - - {item.props?.allowsSorting && - - {formatMessage('sortAscending')} + + + {(item) => ( + + {item.label} - } - {item.props?.allowsSorting && - - {formatMessage('sortDescending')} - - } - - {formatMessage('resizeColumn')} - + )} - + ); } @@ -815,27 +831,4 @@ function CenteredWrapper({children}) { */ const _TableView = React.forwardRef(TableView) as (props: SpectrumTableProps & {ref?: DOMRef}) => ReactElement; -/* - When ready to remove this feature flag, you can remove this whole section of code, delete the _DEPRECATED files, and just replace the export with the _TableView above. -*/ -function FeatureFlaggedTableView(props: SpectrumTableProps, ref: DOMRef) { - let state = useTableState({ - ...props - }); - - const someColumnsAllowResizing = state.collection.columns.some(c => c.props?.allowsResizing); - - if (someColumnsAllowResizing) { - return <_TableView {...props} ref={ref} />; - } else { - return ; - } -} - -/** - * Tables are containers for displaying information. They allow users to quickly scan, sort, compare, and take action on large amounts of data. - */ -const _FeatureFlaggedTableView = React.forwardRef(FeatureFlaggedTableView) as (props: SpectrumTableProps & {ref?: DOMRef}) => ReactElement; - - -export {_FeatureFlaggedTableView as TableView}; +export {_TableView as TableView}; diff --git a/packages/@react-spectrum/table/src/TableView_DEPRECATED.tsx b/packages/@react-spectrum/table/src/TableView_DEPRECATED.tsx deleted file mode 100644 index e8425ba1716..00000000000 --- a/packages/@react-spectrum/table/src/TableView_DEPRECATED.tsx +++ /dev/null @@ -1,728 +0,0 @@ -/* - * Copyright 2020 Adobe. All rights reserved. - * This file is licensed to you under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. You may obtain a copy - * of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under - * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS - * OF ANY KIND, either express or implied. See the License for the specific language - * governing permissions and limitations under the License. - */ - -import ArrowDownSmall from '@spectrum-icons/ui/ArrowDownSmall'; -import {chain, mergeProps, useLayoutEffect} from '@react-aria/utils'; -import {Checkbox} from '@react-spectrum/checkbox'; -import {classNames, useDOMRef, useStyleProps} from '@react-spectrum/utils'; -import {DOMRef} from '@react-types/shared'; -import {FocusRing, useFocusRing} from '@react-aria/focus'; -import {GridNode} from '@react-types/grid'; -// @ts-ignore -import intlMessages from '../intl/*.json'; -import {layoutInfoToStyle, ScrollView, setScrollLeft, useVirtualizer, VirtualizerItem} from '@react-aria/virtualizer'; -import {ProgressCircle} from '@react-spectrum/progress'; -import React, {ReactElement, useCallback, useContext, useMemo, useRef, useState} from 'react'; -import {Rect, ReusableView, useVirtualizerState} from '@react-stately/virtualizer'; -import {SpectrumColumnProps, SpectrumTableProps} from '@react-types/table'; -import styles from '@adobe/spectrum-css-temp/components/table/vars.css'; -import stylesOverrides from './table.css'; -import {TableLayout_DEPRECATED} from '@react-stately/layout'; -import {TableState, useTableState} from '@react-stately/table'; -import {Tooltip, TooltipTrigger} from '@react-spectrum/tooltip'; -import {useHover} from '@react-aria/interactions'; -import {useLocale, useMessageFormatter} from '@react-aria/i18n'; -import {usePress} from '@react-aria/interactions'; -import {useProvider, useProviderProps} from '@react-spectrum/provider'; -import { - useTable, - useTableCell, - useTableColumnHeader, - useTableHeaderRow, - useTableRow, - useTableRowGroup, - useTableSelectAllCheckbox, - useTableSelectionCheckbox -} from '@react-aria/table'; -import {VisuallyHidden} from '@react-aria/visually-hidden'; - -const DEFAULT_HEADER_HEIGHT = { - medium: 34, - large: 40 -}; - -const DEFAULT_HIDE_HEADER_CELL_WIDTH = { - medium: 36, - large: 44 -}; - -const ROW_HEIGHTS = { - compact: { - medium: 32, - large: 40 - }, - regular: { - medium: 40, - large: 50 - }, - spacious: { - medium: 48, - large: 60 - } -}; - -const SELECTION_CELL_DEFAULT_WIDTH = { - medium: 38, - large: 48 -}; - -interface TableContextValue { - state: TableState, - layout: TableLayout_DEPRECATED -} - -const TableContext = React.createContext>(null); -function useTableContext() { - return useContext(TableContext); -} - -function TableView_DEPRECATED(props: SpectrumTableProps, ref: DOMRef) { - props = useProviderProps(props); - let {isQuiet, onAction} = props; - let {styleProps} = useStyleProps(props); - - let [showSelectionCheckboxes, setShowSelectionCheckboxes] = useState(props.selectionStyle !== 'highlight'); - let state = useTableState({ - ...props, - showSelectionCheckboxes, - selectionBehavior: props.selectionStyle === 'highlight' ? 'replace' : 'toggle' - }); - - // If the selection behavior changes in state, we need to update showSelectionCheckboxes here due to the circular dependency... - let shouldShowCheckboxes = state.selectionManager.selectionBehavior !== 'replace'; - if (shouldShowCheckboxes !== showSelectionCheckboxes) { - setShowSelectionCheckboxes(shouldShowCheckboxes); - } - - let domRef = useDOMRef(ref); - let bodyRef = useRef(); - let formatMessage = useMessageFormatter(intlMessages); - - let {scale} = useProvider(); - let density = props.density || 'regular'; - let layout = useMemo(() => new TableLayout_DEPRECATED({ - // If props.rowHeight is auto, then use estimated heights based on scale, otherwise use fixed heights. - rowHeight: props.overflowMode === 'wrap' - ? null - : ROW_HEIGHTS[density][scale], - estimatedRowHeight: props.overflowMode === 'wrap' - ? ROW_HEIGHTS[density][scale] - : null, - headingHeight: props.overflowMode === 'wrap' - ? null - : DEFAULT_HEADER_HEIGHT[scale], - estimatedHeadingHeight: props.overflowMode === 'wrap' - ? DEFAULT_HEADER_HEIGHT[scale] - : null, - getDefaultWidth: ({hideHeader, isSelectionCell, showDivider}) => { - if (hideHeader) { - let width = DEFAULT_HIDE_HEADER_CELL_WIDTH[scale]; - return showDivider ? width + 1 : width; - } else if (isSelectionCell) { - return SELECTION_CELL_DEFAULT_WIDTH[scale]; - } - } - }), [props.overflowMode, scale, density]); - let {direction} = useLocale(); - layout.collection = state.collection; - - let {gridProps} = useTable({ - ...props, - isVirtualized: true, - layout, - onRowAction: onAction - }, state, domRef); - - // This overrides collection view's renderWrapper to support DOM heirarchy. - type View = ReusableView, unknown>; - let renderWrapper = (parent: View, reusableView: View, children: View[], renderChildren: (views: View[]) => ReactElement[]) => { - let style = layoutInfoToStyle(reusableView.layoutInfo, direction, parent && parent.layoutInfo); - if (style.overflow === 'hidden') { - style.overflow = 'visible'; // needed to support position: sticky - } - - if (reusableView.viewType === 'rowgroup') { - return ( - - {renderChildren(children)} - - ); - } - - if (reusableView.viewType === 'header') { - return ( - - {renderChildren(children)} - - ); - } - - if (reusableView.viewType === 'row') { - return ( - - {renderChildren(children)} - - ); - } - - if (reusableView.viewType === 'headerrow') { - return ( - - {renderChildren(children)} - - ); - } - - return ( - - ); - }; - - let renderView = (type: string, item: GridNode) => { - switch (type) { - case 'header': - case 'rowgroup': - case 'section': - case 'row': - case 'headerrow': - return null; - case 'cell': { - if (item.props.isSelectionCell) { - return ; - } - - return ; - } - case 'placeholder': - // TODO: move to react-aria? - return ( -
1 ? item.colspan : null} /> - ); - case 'column': - if (item.props.isSelectionCell) { - return ; - } - - if (item.props.hideHeader) { - return ( - - - {item.rendered} - - ); - } - - return ; - case 'loader': - return ( - - 0 ? formatMessage('loadingMore') : formatMessage('loading')} /> - - ); - case 'empty': { - let emptyState = props.renderEmptyState ? props.renderEmptyState() : null; - if (emptyState == null) { - return null; - } - - return ( - - {emptyState} - - ); - } - } - }; - - // whenever the viewport changes size, check if scroll bars are visible - // - let [isVerticalScrollbarVisible, setVerticalScollbarVisible] = useState(false); - let [isHorizontalScrollbarVisible, setHorizontalScollbarVisible] = useState(false); - let onVisibleRectChange = useCallback(() => { - if (bodyRef.current) { - setVerticalScollbarVisible(bodyRef.current.clientWidth + 2 < bodyRef.current.offsetWidth); - setHorizontalScollbarVisible(bodyRef.current.clientHeight + 2 < bodyRef.current.offsetHeight); - } - }, []); - - return ( - - - - ); -} - -// This is a custom Virtualizer that also has a header that syncs its scroll position with the body. -function TableVirtualizer({layout, collection, focusedKey, renderView, renderWrapper, domRef, bodyRef, onVisibleRectChange: onVisibleRectChangeProp, ...otherProps}) { - let {direction} = useLocale(); - let headerRef = useRef(); - let loadingState = collection.body.props.loadingState; - let isLoading = loadingState === 'loading' || loadingState === 'loadingMore'; - let onLoadMore = collection.body.props.onLoadMore; - let state = useVirtualizerState({ - layout, - collection, - renderView, - renderWrapper, - onVisibleRectChange(rect) { - bodyRef.current.scrollTop = rect.y; - setScrollLeft(bodyRef.current, direction, rect.x); - }, - transitionDuration: isLoading ? 160 : 220 - }); - - let {virtualizerProps} = useVirtualizer({ - focusedKey, - scrollToItem(key) { - let item = collection.getItem(key); - let column = collection.columns[0]; - state.virtualizer.scrollToItem(key, { - duration: 0, - // Prevent scrolling to the top when clicking on column headers. - shouldScrollY: item?.type !== 'column', - // Offset scroll position by width of selection cell - // (which is sticky and will overlap the cell we're scrolling to). - offsetX: column.props.isSelectionCell - ? layout.columnWidths.get(column.key) - : 0 - }); - } - }, state, domRef); - - let headerHeight = layout.getLayoutInfo('header')?.rect.height || 0; - let visibleRect = state.virtualizer.visibleRect; - - // Sync the scroll position from the table body to the header container. - let onScroll = useCallback(() => { - headerRef.current.scrollLeft = bodyRef.current.scrollLeft; - }, [bodyRef]); - - let onVisibleRectChange = useCallback((rect: Rect) => { - state.setVisibleRect(rect); - - if (!isLoading && onLoadMore) { - let scrollOffset = state.virtualizer.contentSize.height - rect.height * 2; - if (rect.y > scrollOffset) { - onLoadMore(); - } - } - }, [onLoadMore, isLoading, state.setVisibleRect, state.virtualizer]); - - useLayoutEffect(() => { - if (!isLoading && onLoadMore && !state.isAnimating) { - if (state.contentSize.height <= state.virtualizer.visibleRect.height) { - onLoadMore(); - } - } - }, [state.contentSize, state.virtualizer, state.isAnimating, onLoadMore, isLoading]); - - return ( -
-
- {state.visibleViews[0]} -
- - {state.visibleViews[1]} - -
- ); -} - -function TableHeader({children, ...otherProps}) { - let {rowGroupProps} = useTableRowGroup(); - - return ( -
- {children} -
- ); -} - -function TableColumnHeader({column}) { - let ref = useRef(); - let {state} = useTableContext(); - let {columnHeaderProps} = useTableColumnHeader({ - node: column, - isVirtualized: true - }, state, ref); - - let columnProps = column.props as SpectrumColumnProps; - let {hoverProps, isHovered} = useHover({}); - - return ( - -
1, - 'react-spectrum-Table-cell--alignEnd': columnProps.align === 'end' - } - ) - ) - }> - {columnProps.hideHeader ? - {column.rendered} : -
{column.rendered}
- } - {columnProps.allowsSorting && - - } - -
-
- ); -} - -function TableSelectAllCell({column}) { - let ref = useRef(); - let {state} = useTableContext(); - let isSingleSelectionMode = state.selectionManager.selectionMode === 'single'; - let {columnHeaderProps} = useTableColumnHeader({ - node: column, - isVirtualized: true - }, state, ref); - - let {checkboxProps} = useTableSelectAllCheckbox(state); - let {hoverProps, isHovered} = useHover({}); - - return ( - -
- { - /* - In single selection mode, the checkbox will be hidden. - So to avoid leaving a column header with no accessible content, - we use a VisuallyHidden component to include the aria-label from the checkbox, - which for single selection will be "Select." - */ - isSingleSelectionMode && - {checkboxProps['aria-label']} - } - -
-
- ); -} - -function TableRowGroup({children, ...otherProps}) { - let {rowGroupProps} = useTableRowGroup(); - - return ( -
- {children} -
- ); -} - -function TableRow({item, children, hasActions, ...otherProps}) { - let ref = useRef(); - let {state, layout} = useTableContext(); - let allowsInteraction = state.selectionManager.selectionMode !== 'none' || hasActions; - let isDisabled = !allowsInteraction || state.disabledKeys.has(item.key); - let isSelected = state.selectionManager.isSelected(item.key); - let {rowProps} = useTableRow({ - node: item, - isVirtualized: true - }, state, ref); - - let {pressProps, isPressed} = usePress({isDisabled}); - - // The row should show the focus background style when any cell inside it is focused. - // If the row itself is focused, then it should have a blue focus indicator on the left. - let { - isFocusVisible: isFocusVisibleWithin, - focusProps: focusWithinProps - } = useFocusRing({within: true}); - let {isFocusVisible, focusProps} = useFocusRing(); - let {hoverProps, isHovered} = useHover({isDisabled}); - let props = mergeProps( - rowProps, - otherProps, - focusWithinProps, - focusProps, - hoverProps, - pressProps - ); - let isFirstRow = state.collection.rows.find(row => row.level === 1)?.key === item.key; - let isLastRow = item.nextKey == null; - // Figure out if the TableView content is equal or greater in height to the container. If so, we'll need to round the bottom - // border corners of the last row when selected. - let isFlushWithContainerBottom = false; - if (isLastRow) { - if (layout.getContentSize()?.height >= layout.virtualizer?.getVisibleRect().height) { - isFlushWithContainerBottom = true; - } - } - - return ( -
- {children} -
- ); -} - -function TableHeaderRow({item, children, style}) { - let {state} = useTableContext(); - let ref = useRef(); - let {rowProps} = useTableHeaderRow({node: item, isVirtualized: true}, state, ref); - - return ( -
- {children} -
- ); -} - -function TableCheckboxCell({cell}) { - let ref = useRef(); - let {state} = useTableContext(); - let isDisabled = state.disabledKeys.has(cell.parentKey); - let {gridCellProps} = useTableCell({ - node: cell, - isVirtualized: true - }, state, ref); - - let {checkboxProps} = useTableSelectionCheckbox({key: cell.parentKey}, state); - - return ( - -
- {state.selectionManager.selectionMode !== 'none' && - - } -
-
- ); -} - -function TableCell({cell}) { - let {state} = useTableContext(); - let ref = useRef(); - let columnProps = cell.column.props as SpectrumColumnProps; - let isDisabled = state.disabledKeys.has(cell.parentKey); - let {gridCellProps} = useTableCell({ - node: cell, - isVirtualized: true - }, state, ref); - - return ( - -
- - {cell.rendered} - -
-
- ); -} - -function CenteredWrapper({children}) { - let {state} = useTableContext(); - return ( -
-
- {children} -
-
- ); -} - -/** - * Tables are containers for displaying information. They allow users to quickly scan, sort, compare, and take action on large amounts of data. - */ -const _TableView_DEPRECATED = React.forwardRef(TableView_DEPRECATED) as (props: SpectrumTableProps & {ref?: DOMRef}) => ReactElement; -export {_TableView_DEPRECATED as TableView_DEPRECATED}; diff --git a/packages/@react-spectrum/table/test/Table.test.js b/packages/@react-spectrum/table/test/Table.test.js index fb8849476f6..c91f866f78b 100644 --- a/packages/@react-spectrum/table/test/Table.test.js +++ b/packages/@react-spectrum/table/test/Table.test.js @@ -23,7 +23,6 @@ import {Dialog, DialogTrigger} from '@react-spectrum/dialog'; import {Divider} from '@react-spectrum/divider'; import {getFocusableTreeWalker} from '@react-aria/focus'; import {Heading} from '@react-spectrum/text'; -import {HidingColumns} from '../stories/HidingColumns'; import {Link} from '@react-spectrum/link'; import {Provider} from '@react-spectrum/provider'; import React from 'react'; @@ -109,7 +108,7 @@ describe('TableView', function () { beforeAll(function () { offsetWidth = jest.spyOn(window.HTMLElement.prototype, 'clientWidth', 'get').mockImplementation(() => 1000); offsetHeight = jest.spyOn(window.HTMLElement.prototype, 'clientHeight', 'get').mockImplementation(() => 1000); - jest.useFakeTimers('legacy'); + jest.useFakeTimers(); }); afterAll(function () { @@ -117,10 +116,6 @@ describe('TableView', function () { offsetHeight.mockReset(); }); - beforeEach(() => { - jest.spyOn(window, 'requestAnimationFrame').mockImplementation(cb => cb()); - }); - afterEach(() => { act(() => {jest.runAllTimers();}); }); @@ -2709,6 +2704,8 @@ describe('TableView', function () { let onSelectionChange = jest.fn(); let onAction = jest.fn(); let tree = renderTable({onSelectionChange, selectionStyle: 'highlight', onAction}); + + act(() => {jest.runAllTimers();}); userEvent.click(document.body); fireEvent.pointerDown(getCell(tree, 'Baz 5'), {pointerType: 'touch'}); @@ -2719,7 +2716,7 @@ describe('TableView', function () { expect(onAction).not.toHaveBeenCalled(); expect(tree.queryByLabelText('Select All')).toBeNull(); - act(() => jest.advanceTimersByTime(800)); + act(() => {jest.advanceTimersByTime(800);}); expect(announce).toHaveBeenLastCalledWith('Foo 5 selected.'); expect(announce).toHaveBeenCalledTimes(1); @@ -2729,22 +2726,27 @@ describe('TableView', function () { fireEvent.pointerUp(getCell(tree, 'Baz 5'), {pointerType: 'touch'}); onSelectionChange.mockReset(); + act(() => {jest.runAllTimers();}); userEvent.click(getCell(tree, 'Foo 10'), {pointerType: 'touch'}); + act(() => {jest.runAllTimers();}); expect(announce).toHaveBeenLastCalledWith('Foo 10 selected. 2 items selected.'); expect(announce).toHaveBeenCalledTimes(2); checkSelection(onSelectionChange, ['Foo 5', 'Foo 10']); // Deselect all to exit selection mode userEvent.click(getCell(tree, 'Foo 10'), {pointerType: 'touch'}); + act(() => {jest.runAllTimers();}); expect(announce).toHaveBeenLastCalledWith('Foo 10 not selected. 1 item selected.'); expect(announce).toHaveBeenCalledTimes(3); onSelectionChange.mockReset(); + userEvent.click(getCell(tree, 'Baz 5'), {pointerType: 'touch'}); + act(() => {jest.runAllTimers();}); expect(announce).toHaveBeenLastCalledWith('Foo 5 not selected.'); expect(announce).toHaveBeenCalledTimes(4); - act(() => jest.runAllTimers()); + act(() => {jest.runAllTimers();}); checkSelection(onSelectionChange, []); expect(onAction).not.toHaveBeenCalled(); expect(tree.queryByLabelText('Select All')).toBeNull(); @@ -3267,9 +3269,6 @@ describe('TableView', function () { let menuItems = within(menu).getAllByRole('menuitem'); expect(menuItems.length).toBe(2); - // Need requestAnimationFrame to actually be async for this test to work. - jest.spyOn(window, 'requestAnimationFrame').mockImplementation(cb => setTimeout(cb, 0)); - triggerPress(menuItems[1]); act(() => jest.runAllTimers()); expect(menu).not.toBeInTheDocument(); @@ -3392,7 +3391,7 @@ describe('TableView', function () { expect(dialog).not.toBeInTheDocument(); - act(() => jest.runAllTimers()); + act(() => {jest.runAllTimers();}); let rowHeaders = within(rows[2]).getAllByRole('rowheader'); expect(rowHeaders[0]).toHaveTextContent('Jessica'); @@ -3407,6 +3406,7 @@ describe('TableView', function () { expect(rows).toHaveLength(3); act(() => within(rows[1]).getAllByRole('gridcell').pop().focus()); + act(() => {jest.runAllTimers();}); expect(document.activeElement).toBe(within(rows[1]).getByRole('button')); fireEvent.keyDown(document.activeElement, {key: 'ArrowDown'}); @@ -3435,6 +3435,7 @@ describe('TableView', function () { expect(rows).toHaveLength(3); act(() => within(rows[1]).getAllByRole('gridcell').pop().focus()); + act(() => {jest.runAllTimers();}); expect(document.activeElement).toBe(within(rows[1]).getByRole('button')); fireEvent.keyDown(document.activeElement, {key: 'ArrowDown', altKey: true}); @@ -3452,6 +3453,7 @@ describe('TableView', function () { expect(rows).toHaveLength(3); act(() => within(rows[1]).getAllByRole('gridcell').pop().focus()); + act(() => {jest.runAllTimers();}); expect(document.activeElement).toBe(within(rows[1]).getByRole('button')); fireEvent.keyDown(document.activeElement, {key: 'ArrowUp', altKey: true}); @@ -3469,6 +3471,7 @@ describe('TableView', function () { expect(rows).toHaveLength(3); act(() => within(rows[1]).getAllByRole('gridcell').pop().focus()); + act(() => {jest.runAllTimers();}); expect(document.activeElement).toBe(within(rows[1]).getByRole('button')); fireEvent.keyDown(document.activeElement, {key: 'ArrowDown', altKey: true}); @@ -3482,9 +3485,6 @@ describe('TableView', function () { fireEvent.keyUp(document.activeElement, {key: 'ArrowDown'}); expect(document.activeElement).toBe(within(menu).getAllByRole('menuitem')[1]); - // Need requestAnimationFrame to actually be async for this test to work. - jest.spyOn(window, 'requestAnimationFrame').mockImplementation(cb => setTimeout(cb, 0)); - fireEvent.keyDown(document.activeElement, {key: 'Escape'}); fireEvent.keyUp(document.activeElement, {key: 'Escape'}); @@ -3723,14 +3723,17 @@ describe('TableView', function () { scrollView.scrollTop = 250; fireEvent.scroll(scrollView); + act(() => {jest.runAllTimers();}); scrollView.scrollTop = 1500; fireEvent.scroll(scrollView); + act(() => {jest.runAllTimers();}); scrollView.scrollTop = 2800; fireEvent.scroll(scrollView); + act(() => {jest.runAllTimers();}); - expect(onLoadMore).toHaveBeenCalledTimes(1); + expect(onLoadMore).toHaveBeenCalledTimes(3); }); it('should automatically fire onLoadMore if there aren\'t enough items to fill the Table', function () { @@ -3755,8 +3758,10 @@ describe('TableView', function () { render(); act(() => jest.runAllTimers()); - // first loadMore triggered by onVisibleRectChange, other 2 by useLayoutEffect - expect(onLoadMoreSpy).toHaveBeenCalledTimes(3); + // first loadMore triggered by onVisibleRectChange, other 3 by useLayoutEffect + // we can't get better results than that without mocking every single clientHeight/Width + // this is a good candidate for storybook interactions test + expect(onLoadMoreSpy).toHaveBeenCalledTimes(4); }); it('should display an empty state when there are no items', function () { @@ -4093,772 +4098,4 @@ describe('TableView', function () { expect(onSortChange).toHaveBeenCalledWith({column: 'bar', direction: 'ascending'}); }); }); - - describe('layout', function () { - describe('row heights', function () { - let renderTable = (props, scale) => render( - - - {column => {column.name}} - - - {item => - ( - {key => {item[key]}} - ) - } - - - , scale); - - it('should layout rows with default height', function () { - let tree = renderTable(); - let rows = tree.getAllByRole('row'); - expect(rows).toHaveLength(3); - - expect(rows[0].style.top).toBe('0px'); - expect(rows[0].style.height).toBe('34px'); - expect(rows[1].style.top).toBe('0px'); - expect(rows[1].style.height).toBe('41px'); - expect(rows[2].style.top).toBe('41px'); - expect(rows[2].style.height).toBe('41px'); - - for (let cell of [...rows[1].childNodes, ...rows[2].childNodes]) { - expect(cell.style.top).toBe('0px'); - expect(cell.style.height).toBe('40px'); - } - }); - - it('should layout rows with default height in large scale', function () { - let tree = renderTable({}, 'large'); - let rows = tree.getAllByRole('row'); - expect(rows).toHaveLength(3); - - expect(rows[0].style.top).toBe('0px'); - expect(rows[0].style.height).toBe('40px'); - expect(rows[1].style.top).toBe('0px'); - expect(rows[1].style.height).toBe('51px'); - expect(rows[2].style.top).toBe('51px'); - expect(rows[2].style.height).toBe('51px'); - - for (let cell of [...rows[1].childNodes, ...rows[2].childNodes]) { - expect(cell.style.top).toBe('0px'); - expect(cell.style.height).toBe('50px'); - } - }); - - it('should layout rows with density="compact"', function () { - let tree = renderTable({density: 'compact'}); - let rows = tree.getAllByRole('row'); - expect(rows).toHaveLength(3); - - expect(rows[0].style.top).toBe('0px'); - expect(rows[0].style.height).toBe('34px'); - expect(rows[1].style.top).toBe('0px'); - expect(rows[1].style.height).toBe('33px'); - expect(rows[2].style.top).toBe('33px'); - expect(rows[2].style.height).toBe('33px'); - - for (let cell of [...rows[1].childNodes, ...rows[2].childNodes]) { - expect(cell.style.top).toBe('0px'); - expect(cell.style.height).toBe('32px'); - } - }); - - it('should layout rows with density="compact" in large scale', function () { - let tree = renderTable({density: 'compact'}, 'large'); - let rows = tree.getAllByRole('row'); - expect(rows).toHaveLength(3); - - expect(rows[0].style.top).toBe('0px'); - expect(rows[0].style.height).toBe('40px'); - expect(rows[1].style.top).toBe('0px'); - expect(rows[1].style.height).toBe('41px'); - expect(rows[2].style.top).toBe('41px'); - expect(rows[2].style.height).toBe('41px'); - - for (let cell of [...rows[1].childNodes, ...rows[2].childNodes]) { - expect(cell.style.top).toBe('0px'); - expect(cell.style.height).toBe('40px'); - } - }); - - it('should layout rows with density="spacious"', function () { - let tree = renderTable({density: 'spacious'}); - let rows = tree.getAllByRole('row'); - expect(rows).toHaveLength(3); - - expect(rows[0].style.top).toBe('0px'); - expect(rows[0].style.height).toBe('34px'); - expect(rows[1].style.top).toBe('0px'); - expect(rows[1].style.height).toBe('49px'); - expect(rows[2].style.top).toBe('49px'); - expect(rows[2].style.height).toBe('49px'); - - for (let cell of [...rows[1].childNodes, ...rows[2].childNodes]) { - expect(cell.style.top).toBe('0px'); - expect(cell.style.height).toBe('48px'); - } - }); - - it('should layout rows with density="spacious" in large scale', function () { - let tree = renderTable({density: 'spacious'}, 'large'); - let rows = tree.getAllByRole('row'); - expect(rows).toHaveLength(3); - - expect(rows[0].style.top).toBe('0px'); - expect(rows[0].style.height).toBe('40px'); - expect(rows[1].style.top).toBe('0px'); - expect(rows[1].style.height).toBe('61px'); - expect(rows[2].style.top).toBe('61px'); - expect(rows[2].style.height).toBe('61px'); - - for (let cell of [...rows[1].childNodes, ...rows[2].childNodes]) { - expect(cell.style.top).toBe('0px'); - expect(cell.style.height).toBe('60px'); - } - }); - - it('should support variable row heights with overflowMode="wrap"', function () { - let scrollHeight = jest.spyOn(window.HTMLElement.prototype, 'scrollHeight', 'get') - .mockImplementation(function () { - return this.textContent === 'Foo 1' ? 64 : 48; - }); - - let tree = renderTable({overflowMode: 'wrap'}); - let rows = tree.getAllByRole('row'); - expect(rows).toHaveLength(3); - - expect(rows[1].style.top).toBe('0px'); - expect(rows[1].style.height).toBe('65px'); - expect(rows[2].style.top).toBe('65px'); - expect(rows[2].style.height).toBe('49px'); - - for (let cell of rows[1].childNodes) { - expect(cell.style.top).toBe('0px'); - expect(cell.style.height).toBe('64px'); - } - - for (let cell of rows[2].childNodes) { - expect(cell.style.top).toBe('0px'); - expect(cell.style.height).toBe('48px'); - } - - scrollHeight.mockRestore(); - }); - - it('should support variable column header heights with overflowMode="wrap"', function () { - let scrollHeight = jest.spyOn(window.HTMLElement.prototype, 'scrollHeight', 'get') - .mockImplementation(function () { - return this.textContent === 'Tier Two Header B' ? 48 : 34; - }); - - let tree = render( - - - {column => {column.name}} - - - {item => - ( - {key => {item[key]}} - ) - } - - - ); - let rows = tree.getAllByRole('row'); - expect(rows).toHaveLength(5); - - expect(rows[0].style.top).toBe('0px'); - expect(rows[0].style.height).toBe('34px'); - expect(rows[1].style.top).toBe('34px'); - expect(rows[1].style.height).toBe('48px'); - expect(rows[2].style.top).toBe('82px'); - expect(rows[2].style.height).toBe('34px'); - - for (let cell of rows[0].childNodes) { - expect(cell.style.top).toBe('0px'); - expect(cell.style.height).toBe('34px'); - } - - for (let cell of rows[1].childNodes) { - expect(cell.style.top).toBe('0px'); - expect(cell.style.height).toBe('48px'); - } - - for (let cell of rows[2].childNodes) { - expect(cell.style.top).toBe('0px'); - expect(cell.style.height).toBe('34px'); - } - - scrollHeight.mockRestore(); - }); - - // To test https://github.com/adobe/react-spectrum/issues/1885 - it('should not throw error if selection mode changes with overflowMode="wrap" and selection was controlled', function () { - function ControlledSelection(props) { - let [selectedKeys, setSelectedKeys] = React.useState(new Set([])); - return ( - - - {column => {column.name}} - - - {item => - ( - {key => {item[key]}} - ) - } - - - ); - } - - let tree = render(); - let row = tree.getAllByRole('row')[2]; - expect(row).toHaveAttribute('aria-selected', 'false'); - userEvent.click(within(row).getByRole('checkbox')); - expect(row).toHaveAttribute('aria-selected', 'true'); - - // Without ListLayout fix, throws here with "TypeError: Cannot set property 'estimatedSize' of undefined" - rerender(tree, ); - act(() => jest.runAllTimers()); - expect(tree.queryByRole('checkbox')).toBeNull(); - }); - - it('should return the proper cell z-indexes for overflowMode="wrap"', function () { - let tree = renderTable({overflowMode: 'wrap', selectionMode: 'multiple'}); - let rows = tree.getAllByRole('row'); - expect(rows).toHaveLength(3); - - for (let row of rows) { - for (let [index, cell] of row.childNodes.entries()) { - if (index === 0) { - expect(cell.style.zIndex).toBe('2'); - } else { - expect(cell.style.zIndex).toBe('1'); - } - } - } - }); - }); - - describe('column widths', function () { - it('should divide the available width by default if no defaultWidth is provided', function () { - let tree = render( - - - {column => {column.name}} - - - {item => - ( - {key => {item[key]}} - ) - } - - - ); - - let rows = tree.getAllByRole('row'); - - for (let row of rows) { - expect(row.childNodes[0].style.width).toBe('38px'); - expect(row.childNodes[1].style.width).toBe('320px'); - expect(row.childNodes[2].style.width).toBe('321px'); - expect(row.childNodes[3].style.width).toBe('321px'); - } - }); - - it('should divide the available width by default in large scale', function () { - let tree = render(( - - - {column => {column.name}} - - - {item => - ( - {key => {item[key]}} - ) - } - - - ), 'large'); - - let rows = tree.getAllByRole('row'); - - for (let row of rows) { - expect(row.childNodes[0].style.width).toBe('48px'); - expect(row.childNodes[1].style.width).toBe('317px'); - expect(row.childNodes[2].style.width).toBe('317px'); - expect(row.childNodes[3].style.width).toBe('318px'); - } - }); - - it('should support explicitly sized columns', function () { - let tree = render( - - - Foo - Bar - Baz - - - {item => - ( - {key => {item[key]}} - ) - } - - - ); - - let rows = tree.getAllByRole('row'); - - for (let row of rows) { - expect(row.childNodes[0].style.width).toBe('200px'); - expect(row.childNodes[1].style.width).toBe('500px'); - expect(row.childNodes[2].style.width).toBe('300px'); - } - }); - - it('should divide remaining width among remaining columns', function () { - let tree = render( - - - Foo - Bar - Baz - - - {item => - ( - {key => {item[key]}} - ) - } - - - ); - - let rows = tree.getAllByRole('row'); - - for (let row of rows) { - expect(row.childNodes[0].style.width).toBe('38px'); - expect(row.childNodes[1].style.width).toBe('200px'); - expect(row.childNodes[2].style.width).toBe('381px'); - expect(row.childNodes[3].style.width).toBe('381px'); - } - }); - - it('should support percentage widths', function () { - let tree = render( - - - Foo - Bar - Baz - - - {item => - ( - {key => {item[key]}} - ) - } - - - ); - - let rows = tree.getAllByRole('row'); - - for (let row of rows) { - expect(row.childNodes[0].style.width).toBe('100px'); - expect(row.childNodes[1].style.width).toBe('500px'); - expect(row.childNodes[2].style.width).toBe('400px'); - } - }); - - it('should support minWidth', function () { - let tree = render( - - - Foo - Bar - Baz - - - {item => - ( - {key => {item[key]}} - ) - } - - - ); - - let rows = tree.getAllByRole('row'); - - for (let row of rows) { - expect(row.childNodes[0].style.width).toBe('38px'); - expect(row.childNodes[1].style.width).toBe('200px'); - expect(row.childNodes[2].style.width).toBe('500px'); - expect(row.childNodes[3].style.width).toBe('262px'); - } - }); - - it('should support maxWidth', function () { - let tree = render( - - - Foo - Bar - Baz - - - {item => - ( - {key => {item[key]}} - ) - } - - - ); - - let rows = tree.getAllByRole('row'); - - for (let row of rows) { - expect(row.childNodes[0].style.width).toBe('200px'); - expect(row.childNodes[1].style.width).toBe('300px'); - expect(row.childNodes[2].style.width).toBe('500px'); - } - }); - - describe('bounded constraint on columns where dynamic columns exist before the bounded columns', () => { - it('should fulfill the constraints of the static columns and give remaining width to previously defined dynamic columns', () => { - let tree = render( - - - Foo - Bar - Baz - - - {item => - ( - {key => {item[key]}} - ) - } - - - ); - - let rows = tree.getAllByRole('row'); - - for (let row of rows) { - expect(row.childNodes[0].style.width).toBe('600px'); - expect(row.childNodes[1].style.width).toBe('200px'); - expect(row.childNodes[2].style.width).toBe('200px'); - } - }); - }); - - describe("mutiple columns are bounded but earlier columns are 'less bounded' than future columns", () => { - it("should satisfy the conditions of all columns but also allocate remaining space to the 'less bounded' previous columns", () => { - let tree = render( - - - Foo - Bar - Baz - - - {item => - ( - {key => {item[key]}} - ) - } - - - ); - - let rows = tree.getAllByRole('row'); - - for (let row of rows) { - expect(row.childNodes[0].style.width).toBe('300px'); - expect(row.childNodes[1].style.width).toBe('500px'); - expect(row.childNodes[2].style.width).toBe('200px'); - } - }); - }); - - it('should compute the correct widths for tiered headings with selection', function () { - let tree = render( - - - {column => {column.name}} - - - {item => - ( - {key => {item[key]}} - ) - } - - - ); - - let rows = tree.getAllByRole('row'); - - expect(rows[0].childNodes[0].style.width).toBe('230px'); - expect(rows[0].childNodes[1].style.width).toBe('770px'); - - expect(rows[1].childNodes[0].style.width).toBe('230px'); - expect(rows[1].childNodes[1].style.width).toBe('384px'); - expect(rows[1].childNodes[2].style.width).toBe('193px'); - expect(rows[1].childNodes[3].style.width).toBe('193px'); - - for (let row of rows.slice(2)) { - expect(row.childNodes[0].style.width).toBe('38px'); - expect(row.childNodes[1].style.width).toBe('192px'); - expect(row.childNodes[2].style.width).toBe('192px'); - expect(row.childNodes[3].style.width).toBe('192px'); - expect(row.childNodes[4].style.width).toBe('193px'); - expect(row.childNodes[5].style.width).toBe('193px'); - } - }); - }); - }); - - describe('updating columns', function () { - it('should support removing columns', function () { - let tree = render(); - - let checkbox = tree.getByLabelText('Net Budget'); - expect(checkbox.checked).toBe(true); - - let table = tree.getByRole('grid'); - let columns = within(table).getAllByRole('columnheader'); - expect(columns).toHaveLength(6); - expect(columns[1]).toHaveTextContent('Plan Name'); - expect(columns[2]).toHaveTextContent('Audience Type'); - expect(columns[3]).toHaveTextContent('Net Budget'); - expect(columns[4]).toHaveTextContent('Target OTP'); - expect(columns[5]).toHaveTextContent('Reach'); - - for (let row of within(table).getAllByRole('row').slice(1)) { - expect(within(row).getAllByRole('rowheader')).toHaveLength(1); - expect(within(row).getAllByRole('gridcell')).toHaveLength(5); - } - - userEvent.click(checkbox); - expect(checkbox.checked).toBe(false); - - act(() => jest.runAllTimers()); - - columns = within(table).getAllByRole('columnheader'); - expect(columns).toHaveLength(5); - expect(columns[1]).toHaveTextContent('Plan Name'); - expect(columns[2]).toHaveTextContent('Audience Type'); - expect(columns[3]).toHaveTextContent('Target OTP'); - expect(columns[4]).toHaveTextContent('Reach'); - - for (let row of within(table).getAllByRole('row').slice(1)) { - expect(within(row).getAllByRole('rowheader')).toHaveLength(1); - expect(within(row).getAllByRole('gridcell')).toHaveLength(4); - } - }); - - it('should support adding columns', function () { - let tree = render(); - - let checkbox = tree.getByLabelText('Net Budget'); - expect(checkbox.checked).toBe(true); - - userEvent.click(checkbox); - expect(checkbox.checked).toBe(false); - - act(() => jest.runAllTimers()); - - let table = tree.getByRole('grid'); - let columns = within(table).getAllByRole('columnheader'); - expect(columns).toHaveLength(5); - - userEvent.click(checkbox); - expect(checkbox.checked).toBe(true); - - act(() => jest.runAllTimers()); - - columns = within(table).getAllByRole('columnheader'); - expect(columns).toHaveLength(6); - expect(columns[1]).toHaveTextContent('Plan Name'); - expect(columns[2]).toHaveTextContent('Audience Type'); - expect(columns[3]).toHaveTextContent('Net Budget'); - expect(columns[4]).toHaveTextContent('Target OTP'); - expect(columns[5]).toHaveTextContent('Reach'); - - for (let row of within(table).getAllByRole('row').slice(1)) { - expect(within(row).getAllByRole('rowheader')).toHaveLength(1); - expect(within(row).getAllByRole('gridcell')).toHaveLength(5); - } - }); - - it('should update the row widths when removing and adding columns', function () { - function compareWidths(row, b) { - let newWidth = row.childNodes[1].style.width; - expect(parseInt(newWidth, 10)).toBeGreaterThan(parseInt(b, 10)); - return newWidth; - } - - let tree = render(); - let table = tree.getByRole('grid'); - let columns = within(table).getAllByRole('columnheader'); - expect(columns).toHaveLength(6); - - let rows = tree.getAllByRole('row'); - let oldWidth = rows[1].childNodes[1].style.width; - - let audienceCheckbox = tree.getByLabelText('Audience Type'); - let budgetCheckbox = tree.getByLabelText('Net Budget'); - let targetCheckbox = tree.getByLabelText('Target OTP'); - let reachCheckbox = tree.getByLabelText('Reach'); - - userEvent.click(audienceCheckbox); - expect(audienceCheckbox.checked).toBe(false); - act(() => jest.runAllTimers()); - oldWidth = compareWidths(rows[1], oldWidth); - - userEvent.click(budgetCheckbox); - expect(budgetCheckbox.checked).toBe(false); - act(() => jest.runAllTimers()); - oldWidth = compareWidths(rows[1], oldWidth); - - userEvent.click(targetCheckbox); - expect(targetCheckbox.checked).toBe(false); - act(() => jest.runAllTimers()); - oldWidth = compareWidths(rows[1], oldWidth); - - // This previously failed, the first column wouldn't update its width - // when the 2nd to last column was removed - userEvent.click(reachCheckbox); - expect(reachCheckbox.checked).toBe(false); - act(() => jest.runAllTimers()); - oldWidth = compareWidths(rows[1], oldWidth); - columns = within(table).getAllByRole('columnheader'); - expect(columns).toHaveLength(2); - - // Readd the column and check that the width decreases - userEvent.click(audienceCheckbox); - expect(audienceCheckbox.checked).toBe(true); - act(() => jest.runAllTimers()); - expect(parseInt(rows[1].childNodes[1].style.width, 10)).toBeLessThan(parseInt(oldWidth, 10)); - }); - }); - - describe('headerless columns', function () { - - let renderTable = (props, scale, showDivider = false) => render( - - - Foo - - Add Item - - - - - Foo 1 - - - - - - - - - , scale); - - it('renders table with headerless column with default scale', function () { - let {getByRole} = renderTable(); - let grid = getByRole('grid'); - expect(grid).toBeVisible(); - expect(grid).toHaveAttribute('aria-label', 'Table'); - expect(grid).toHaveAttribute('data-testid', 'test'); - - expect(grid).toHaveAttribute('aria-rowcount', '2'); - expect(grid).toHaveAttribute('aria-colcount', '2'); - let rowgroups = within(grid).getAllByRole('rowgroup'); - expect(rowgroups).toHaveLength(2); - - let headerRows = within(rowgroups[0]).getAllByRole('row'); - expect(headerRows).toHaveLength(1); - expect(headerRows[0]).toHaveAttribute('aria-rowindex', '1'); - - let headers = within(grid).getAllByRole('columnheader'); - expect(headers).toHaveLength(2); - let className = headers[1].className; - expect(className.includes('spectrum-Table-cell--hideHeader')).toBeTruthy(); - expect(headers[0]).toHaveTextContent('Foo'); - // visually hidden syle - expect(headers[1].childNodes[0].style.clipPath).toBe('inset(50%)'); - expect(headers[1].childNodes[0].style.width).toBe('1px'); - expect(headers[1].childNodes[0].style.height).toBe('1px'); - expect(headers[1]).not.toBeEmptyDOMElement(); - - - let rows = within(rowgroups[1]).getAllByRole('row'); - expect(rows).toHaveLength(1); - // The width of headerless column - expect(rows[0].childNodes[1].style.width).toBe('36px'); - let rowheader = within(rows[0]).getByRole('rowheader'); - expect(rowheader).toHaveTextContent('Foo 1'); - let actionCell = within(rows[0]).getAllByRole('gridcell'); - expect(actionCell).toHaveLength(1); - let buttons = within(actionCell[0]).getAllByRole('button'); - expect(buttons).toHaveLength(1); - className = actionCell[0].className; - expect(className.includes('spectrum-Table-cell--hideHeader')).toBeTruthy(); - }); - - it('renders table with headerless column with large scale', function () { - let {getByRole} = renderTable({}, 'large'); - let grid = getByRole('grid'); - expect(grid).toBeVisible(); - expect(grid).toHaveAttribute('aria-label', 'Table'); - expect(grid).toHaveAttribute('data-testid', 'test'); - let rowgroups = within(grid).getAllByRole('rowgroup'); - let rows = within(rowgroups[1]).getAllByRole('row'); - expect(rows).toHaveLength(1); - // The width of headerless column - expect(rows[0].childNodes[1].style.width).toBe('44px'); - }); - - it('renders table with headerless column and divider', function () { - let {getByRole} = renderTable({}, undefined, true); - let grid = getByRole('grid'); - expect(grid).toBeVisible(); - let rowgroups = within(grid).getAllByRole('rowgroup'); - expect(rowgroups).toHaveLength(2); - let rows = within(rowgroups[1]).getAllByRole('row'); - expect(rows).toHaveLength(1); - // The width of headerless column with divider - expect(rows[0].childNodes[1].style.width).toBe('37px'); - }); - - it('renders table with headerless column with tooltip', function () { - let {getByRole} = renderTable({}, 'large'); - let grid = getByRole('grid'); - expect(grid).toBeVisible(); - expect(grid).toHaveAttribute('aria-label', 'Table'); - expect(grid).toHaveAttribute('data-testid', 'test'); - let headers = within(grid).getAllByRole('columnheader'); - let headerlessColumn = headers[1]; - act(() => { - headerlessColumn.focus(); - }); - let tooltip = getByRole('tooltip'); - expect(tooltip).toBeVisible(); - }); - - }); }); diff --git a/packages/@react-spectrum/table/test/TableSizing.test.js b/packages/@react-spectrum/table/test/TableSizing.test.js new file mode 100644 index 00000000000..90daf92e8bb --- /dev/null +++ b/packages/@react-spectrum/table/test/TableSizing.test.js @@ -0,0 +1,857 @@ +/* + * Copyright 2020 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +jest.mock('@react-aria/live-announcer'); +import {act, render as renderComponent, within} from '@testing-library/react'; +import {ActionButton} from '@react-spectrum/button'; +import Add from '@spectrum-icons/workflow/Add'; +import {Cell, Column, Row, TableBody, TableHeader, TableView} from '../'; +import {HidingColumns} from '../stories/HidingColumns'; +import {Provider} from '@react-spectrum/provider'; +import React from 'react'; +import {theme} from '@react-spectrum/theme-default'; +import userEvent from '@testing-library/user-event'; + +let columns = [ + {name: 'Foo', key: 'foo'}, + {name: 'Bar', key: 'bar'}, + {name: 'Baz', key: 'baz'} +]; + +let nestedColumns = [ + {name: 'Test', key: 'test'}, + {name: 'Tiered One Header', key: 'tier1', children: [ + {name: 'Tier Two Header A', key: 'tier2a', children: [ + {name: 'Foo', key: 'foo'}, + {name: 'Bar', key: 'bar'} + ]}, + {name: 'Yay', key: 'yay'}, + {name: 'Tier Two Header B', key: 'tier2b', children: [ + {name: 'Baz', key: 'baz'} + ]} + ]} +]; + +let items = [ + {test: 'Test 1', foo: 'Foo 1', bar: 'Bar 1', yay: 'Yay 1', baz: 'Baz 1'}, + {test: 'Test 2', foo: 'Foo 2', bar: 'Bar 2', yay: 'Yay 2', baz: 'Baz 2'} +]; + + +let manyItems = []; +for (let i = 1; i <= 100; i++) { + manyItems.push({id: i, foo: 'Foo ' + i, bar: 'Bar ' + i, baz: 'Baz ' + i}); +} + +describe('TableViewSizing', function () { + let offsetWidth, offsetHeight; + + beforeAll(function () { + offsetWidth = jest.spyOn(window.HTMLElement.prototype, 'clientWidth', 'get').mockImplementation(() => 1000); + offsetHeight = jest.spyOn(window.HTMLElement.prototype, 'clientHeight', 'get').mockImplementation(() => 1000); + jest.useFakeTimers(); + }); + + afterAll(function () { + offsetWidth.mockReset(); + offsetHeight.mockReset(); + }); + + afterEach(() => { + act(() => {jest.runAllTimers();}); + }); + + let render = (children, scale = 'medium') => renderComponent( + + {children} + + ); + + let rerender = (tree, children, scale = 'medium') => tree.rerender( + + {children} + + ); + + describe('layout', function () { + describe('row heights', function () { + let renderTable = (props, scale) => render( + + + {column => {column.name}} + + + {item => + ( + {key => {item[key]}} + ) + } + + + , scale); + + it('should layout rows with default height', function () { + let tree = renderTable(); + let rows = tree.getAllByRole('row'); + expect(rows).toHaveLength(3); + + expect(rows[0].style.top).toBe('0px'); + expect(rows[0].style.height).toBe('34px'); + expect(rows[1].style.top).toBe('0px'); + expect(rows[1].style.height).toBe('41px'); + expect(rows[2].style.top).toBe('41px'); + expect(rows[2].style.height).toBe('41px'); + + for (let cell of [...rows[1].childNodes, ...rows[2].childNodes]) { + expect(cell.style.top).toBe('0px'); + expect(cell.style.height).toBe('40px'); + } + }); + + it('should layout rows with default height in large scale', function () { + let tree = renderTable({}, 'large'); + let rows = tree.getAllByRole('row'); + expect(rows).toHaveLength(3); + + expect(rows[0].style.top).toBe('0px'); + expect(rows[0].style.height).toBe('40px'); + expect(rows[1].style.top).toBe('0px'); + expect(rows[1].style.height).toBe('51px'); + expect(rows[2].style.top).toBe('51px'); + expect(rows[2].style.height).toBe('51px'); + + for (let cell of [...rows[1].childNodes, ...rows[2].childNodes]) { + expect(cell.style.top).toBe('0px'); + expect(cell.style.height).toBe('50px'); + } + }); + + it('should layout rows with density="compact"', function () { + let tree = renderTable({density: 'compact'}); + let rows = tree.getAllByRole('row'); + expect(rows).toHaveLength(3); + + expect(rows[0].style.top).toBe('0px'); + expect(rows[0].style.height).toBe('34px'); + expect(rows[1].style.top).toBe('0px'); + expect(rows[1].style.height).toBe('33px'); + expect(rows[2].style.top).toBe('33px'); + expect(rows[2].style.height).toBe('33px'); + + for (let cell of [...rows[1].childNodes, ...rows[2].childNodes]) { + expect(cell.style.top).toBe('0px'); + expect(cell.style.height).toBe('32px'); + } + }); + + it('should layout rows with density="compact" in large scale', function () { + let tree = renderTable({density: 'compact'}, 'large'); + let rows = tree.getAllByRole('row'); + expect(rows).toHaveLength(3); + + expect(rows[0].style.top).toBe('0px'); + expect(rows[0].style.height).toBe('40px'); + expect(rows[1].style.top).toBe('0px'); + expect(rows[1].style.height).toBe('41px'); + expect(rows[2].style.top).toBe('41px'); + expect(rows[2].style.height).toBe('41px'); + + for (let cell of [...rows[1].childNodes, ...rows[2].childNodes]) { + expect(cell.style.top).toBe('0px'); + expect(cell.style.height).toBe('40px'); + } + }); + + it('should layout rows with density="spacious"', function () { + let tree = renderTable({density: 'spacious'}); + let rows = tree.getAllByRole('row'); + expect(rows).toHaveLength(3); + + expect(rows[0].style.top).toBe('0px'); + expect(rows[0].style.height).toBe('34px'); + expect(rows[1].style.top).toBe('0px'); + expect(rows[1].style.height).toBe('49px'); + expect(rows[2].style.top).toBe('49px'); + expect(rows[2].style.height).toBe('49px'); + + for (let cell of [...rows[1].childNodes, ...rows[2].childNodes]) { + expect(cell.style.top).toBe('0px'); + expect(cell.style.height).toBe('48px'); + } + }); + + it('should layout rows with density="spacious" in large scale', function () { + let tree = renderTable({density: 'spacious'}, 'large'); + let rows = tree.getAllByRole('row'); + expect(rows).toHaveLength(3); + + expect(rows[0].style.top).toBe('0px'); + expect(rows[0].style.height).toBe('40px'); + expect(rows[1].style.top).toBe('0px'); + expect(rows[1].style.height).toBe('61px'); + expect(rows[2].style.top).toBe('61px'); + expect(rows[2].style.height).toBe('61px'); + + for (let cell of [...rows[1].childNodes, ...rows[2].childNodes]) { + expect(cell.style.top).toBe('0px'); + expect(cell.style.height).toBe('60px'); + } + }); + + it('should support variable row heights with overflowMode="wrap"', function () { + let scrollHeight = jest.spyOn(window.HTMLElement.prototype, 'scrollHeight', 'get') + .mockImplementation(function () { + return this.textContent === 'Foo 1' ? 64 : 48; + }); + + let tree = renderTable({overflowMode: 'wrap'}); + act(() => {jest.runAllTimers();}); + let rows = tree.getAllByRole('row'); + expect(rows).toHaveLength(3); + + expect(rows[1].style.top).toBe('0px'); + expect(rows[1].style.height).toBe('65px'); + expect(rows[2].style.top).toBe('65px'); + expect(rows[2].style.height).toBe('49px'); + + for (let cell of rows[1].childNodes) { + expect(cell.style.top).toBe('0px'); + expect(cell.style.height).toBe('64px'); + } + + for (let cell of rows[2].childNodes) { + expect(cell.style.top).toBe('0px'); + expect(cell.style.height).toBe('48px'); + } + + scrollHeight.mockRestore(); + }); + + it('should support variable column header heights with overflowMode="wrap"', function () { + let scrollHeight = jest.spyOn(window.HTMLElement.prototype, 'scrollHeight', 'get') + .mockImplementation(function () { + return this.textContent === 'Tier Two Header B' ? 48 : 34; + }); + + let tree = render( + + + {column => {column.name}} + + + {item => + ( + {key => {item[key]}} + ) + } + + + ); + act(() => {jest.runAllTimers();}); + let rows = tree.getAllByRole('row'); + expect(rows).toHaveLength(5); + + expect(rows[0].style.top).toBe('0px'); + expect(rows[0].style.height).toBe('34px'); + expect(rows[1].style.top).toBe('34px'); + expect(rows[1].style.height).toBe('48px'); + expect(rows[2].style.top).toBe('82px'); + expect(rows[2].style.height).toBe('34px'); + + for (let cell of rows[0].childNodes) { + expect(cell.style.top).toBe('0px'); + expect(cell.style.height).toBe('34px'); + } + + for (let cell of rows[1].childNodes) { + expect(cell.style.top).toBe('0px'); + expect(cell.style.height).toBe('48px'); + } + + for (let cell of rows[2].childNodes) { + expect(cell.style.top).toBe('0px'); + expect(cell.style.height).toBe('34px'); + } + + scrollHeight.mockRestore(); + }); + + // To test https://github.com/adobe/react-spectrum/issues/1885 + it('should not throw error if selection mode changes with overflowMode="wrap" and selection was controlled', function () { + function ControlledSelection(props) { + let [selectedKeys, setSelectedKeys] = React.useState(new Set([])); + return ( + + + {column => {column.name}} + + + {item => + ( + {key => {item[key]}} + ) + } + + + ); + } + + let tree = render(); + act(() => {jest.runAllTimers();}); + let row = tree.getAllByRole('row')[2]; + expect(row).toHaveAttribute('aria-selected', 'false'); + userEvent.click(within(row).getByRole('checkbox')); + act(() => {jest.runAllTimers();}); + expect(row).toHaveAttribute('aria-selected', 'true'); + + // Without ListLayout fix, throws here with "TypeError: Cannot set property 'estimatedSize' of undefined" + rerender(tree, ); + act(() => {jest.runAllTimers();}); + expect(tree.queryByRole('checkbox')).toBeNull(); + }); + + it('should return the proper cell z-indexes for overflowMode="wrap"', function () { + let tree = renderTable({overflowMode: 'wrap', selectionMode: 'multiple'}); + let rows = tree.getAllByRole('row'); + expect(rows).toHaveLength(3); + + for (let row of rows) { + for (let [index, cell] of row.childNodes.entries()) { + if (index === 0) { + expect(cell.style.zIndex).toBe('2'); + } else { + expect(cell.style.zIndex).toBe('1'); + } + } + } + }); + }); + + describe('column widths', function () { + it('should divide the available width by default if no defaultWidth is provided', function () { + let tree = render( + + + {column => {column.name}} + + + {item => + ( + {key => {item[key]}} + ) + } + + + ); + + let rows = tree.getAllByRole('row'); + + for (let row of rows) { + expect(row.childNodes[0].style.width).toBe('38px'); + expect(row.childNodes[1].style.width).toBe('320px'); + expect(row.childNodes[2].style.width).toBe('321px'); + expect(row.childNodes[3].style.width).toBe('321px'); + } + }); + + it('should divide the available width by default in large scale', function () { + let tree = render(( + + + {column => {column.name}} + + + {item => + ( + {key => {item[key]}} + ) + } + + + ), 'large'); + + let rows = tree.getAllByRole('row'); + + for (let row of rows) { + expect(row.childNodes[0].style.width).toBe('48px'); + expect(row.childNodes[1].style.width).toBe('317px'); + expect(row.childNodes[2].style.width).toBe('317px'); + expect(row.childNodes[3].style.width).toBe('318px'); + } + }); + + it('should support explicitly sized columns', function () { + let tree = render( + + + Foo + Bar + Baz + + + {item => + ( + {key => {item[key]}} + ) + } + + + ); + + let rows = tree.getAllByRole('row'); + + for (let row of rows) { + expect(row.childNodes[0].style.width).toBe('200px'); + expect(row.childNodes[1].style.width).toBe('500px'); + expect(row.childNodes[2].style.width).toBe('300px'); + } + }); + + it('should divide remaining width among remaining columns', function () { + let tree = render( + + + Foo + Bar + Baz + + + {item => + ( + {key => {item[key]}} + ) + } + + + ); + + let rows = tree.getAllByRole('row'); + + for (let row of rows) { + expect(row.childNodes[0].style.width).toBe('38px'); + expect(row.childNodes[1].style.width).toBe('200px'); + expect(row.childNodes[2].style.width).toBe('381px'); + expect(row.childNodes[3].style.width).toBe('381px'); + } + }); + + it('should support percentage widths', function () { + let tree = render( + + + Foo + Bar + Baz + + + {item => + ( + {key => {item[key]}} + ) + } + + + ); + + let rows = tree.getAllByRole('row'); + + for (let row of rows) { + expect(row.childNodes[0].style.width).toBe('100px'); + expect(row.childNodes[1].style.width).toBe('500px'); + expect(row.childNodes[2].style.width).toBe('400px'); + } + }); + + it('should support minWidth', function () { + let tree = render( + + + Foo + Bar + Baz + + + {item => + ( + {key => {item[key]}} + ) + } + + + ); + + let rows = tree.getAllByRole('row'); + + for (let row of rows) { + expect(row.childNodes[0].style.width).toBe('38px'); + expect(row.childNodes[1].style.width).toBe('200px'); + expect(row.childNodes[2].style.width).toBe('500px'); + expect(row.childNodes[3].style.width).toBe('262px'); + } + }); + + it('should support maxWidth', function () { + let tree = render( + + + Foo + Bar + Baz + + + {item => + ( + {key => {item[key]}} + ) + } + + + ); + + let rows = tree.getAllByRole('row'); + + for (let row of rows) { + expect(row.childNodes[0].style.width).toBe('200px'); + expect(row.childNodes[1].style.width).toBe('300px'); + expect(row.childNodes[2].style.width).toBe('500px'); + } + }); + + describe('bounded constraint on columns where dynamic columns exist before the bounded columns', () => { + it('should fulfill the constraints of the static columns and give remaining width to previously defined dynamic columns', () => { + let tree = render( + + + Foo + Bar + Baz + + + {item => + ( + {key => {item[key]}} + ) + } + + + ); + + let rows = tree.getAllByRole('row'); + + for (let row of rows) { + expect(row.childNodes[0].style.width).toBe('600px'); + expect(row.childNodes[1].style.width).toBe('200px'); + expect(row.childNodes[2].style.width).toBe('200px'); + } + }); + }); + + describe("mutiple columns are bounded but earlier columns are 'less bounded' than future columns", () => { + it("should satisfy the conditions of all columns but also allocate remaining space to the 'less bounded' previous columns", () => { + let tree = render( + + + Foo + Bar + Baz + + + {item => + ( + {key => {item[key]}} + ) + } + + + ); + + let rows = tree.getAllByRole('row'); + + for (let row of rows) { + expect(row.childNodes[0].style.width).toBe('300px'); + expect(row.childNodes[1].style.width).toBe('500px'); + expect(row.childNodes[2].style.width).toBe('200px'); + } + }); + }); + + it('should compute the correct widths for tiered headings with selection', function () { + let tree = render( + + + {column => {column.name}} + + + {item => + ( + {key => {item[key]}} + ) + } + + + ); + + let rows = tree.getAllByRole('row'); + + expect(rows[0].childNodes[0].style.width).toBe('230px'); + expect(rows[0].childNodes[1].style.width).toBe('770px'); + + expect(rows[1].childNodes[0].style.width).toBe('230px'); + expect(rows[1].childNodes[1].style.width).toBe('384px'); + expect(rows[1].childNodes[2].style.width).toBe('193px'); + expect(rows[1].childNodes[3].style.width).toBe('193px'); + + for (let row of rows.slice(2)) { + expect(row.childNodes[0].style.width).toBe('38px'); + expect(row.childNodes[1].style.width).toBe('192px'); + expect(row.childNodes[2].style.width).toBe('192px'); + expect(row.childNodes[3].style.width).toBe('192px'); + expect(row.childNodes[4].style.width).toBe('193px'); + expect(row.childNodes[5].style.width).toBe('193px'); + } + }); + }); + }); + + describe('updating columns', function () { + it('should support removing columns', function () { + let tree = render(); + + let checkbox = tree.getByLabelText('Net Budget'); + expect(checkbox.checked).toBe(true); + + let table = tree.getByRole('grid'); + let columns = within(table).getAllByRole('columnheader'); + expect(columns).toHaveLength(6); + expect(columns[1]).toHaveTextContent('Plan Name'); + expect(columns[2]).toHaveTextContent('Audience Type'); + expect(columns[3]).toHaveTextContent('Net Budget'); + expect(columns[4]).toHaveTextContent('Target OTP'); + expect(columns[5]).toHaveTextContent('Reach'); + + for (let row of within(table).getAllByRole('row').slice(1)) { + expect(within(row).getAllByRole('rowheader')).toHaveLength(1); + expect(within(row).getAllByRole('gridcell')).toHaveLength(5); + } + + userEvent.click(checkbox); + expect(checkbox.checked).toBe(false); + + act(() => jest.runAllTimers()); + + columns = within(table).getAllByRole('columnheader'); + expect(columns).toHaveLength(5); + expect(columns[1]).toHaveTextContent('Plan Name'); + expect(columns[2]).toHaveTextContent('Audience Type'); + expect(columns[3]).toHaveTextContent('Target OTP'); + expect(columns[4]).toHaveTextContent('Reach'); + + for (let row of within(table).getAllByRole('row').slice(1)) { + expect(within(row).getAllByRole('rowheader')).toHaveLength(1); + expect(within(row).getAllByRole('gridcell')).toHaveLength(4); + } + }); + + it('should support adding columns', function () { + let tree = render(); + + let checkbox = tree.getByLabelText('Net Budget'); + expect(checkbox.checked).toBe(true); + + userEvent.click(checkbox); + expect(checkbox.checked).toBe(false); + + act(() => jest.runAllTimers()); + + let table = tree.getByRole('grid'); + let columns = within(table).getAllByRole('columnheader'); + expect(columns).toHaveLength(5); + + userEvent.click(checkbox); + expect(checkbox.checked).toBe(true); + + act(() => jest.runAllTimers()); + + columns = within(table).getAllByRole('columnheader'); + expect(columns).toHaveLength(6); + expect(columns[1]).toHaveTextContent('Plan Name'); + expect(columns[2]).toHaveTextContent('Audience Type'); + expect(columns[3]).toHaveTextContent('Net Budget'); + expect(columns[4]).toHaveTextContent('Target OTP'); + expect(columns[5]).toHaveTextContent('Reach'); + + for (let row of within(table).getAllByRole('row').slice(1)) { + expect(within(row).getAllByRole('rowheader')).toHaveLength(1); + expect(within(row).getAllByRole('gridcell')).toHaveLength(5); + } + }); + + it('should update the row widths when removing and adding columns', function () { + function compareWidths(row, b) { + let newWidth = row.childNodes[1].style.width; + expect(parseInt(newWidth, 10)).toBeGreaterThan(parseInt(b, 10)); + return newWidth; + } + + let tree = render(); + act(() => jest.runAllTimers()); + let table = tree.getByRole('grid'); + let columns = within(table).getAllByRole('columnheader'); + expect(columns).toHaveLength(6); + + let rows = tree.getAllByRole('row'); + let oldWidth = rows[1].childNodes[1].style.width; + + let audienceCheckbox = tree.getByLabelText('Audience Type'); + let budgetCheckbox = tree.getByLabelText('Net Budget'); + let targetCheckbox = tree.getByLabelText('Target OTP'); + let reachCheckbox = tree.getByLabelText('Reach'); + + userEvent.click(audienceCheckbox); + expect(audienceCheckbox.checked).toBe(false); + act(() => jest.runAllTimers()); + oldWidth = compareWidths(rows[1], oldWidth); + + userEvent.click(budgetCheckbox); + expect(budgetCheckbox.checked).toBe(false); + act(() => jest.runAllTimers()); + oldWidth = compareWidths(rows[1], oldWidth); + + userEvent.click(targetCheckbox); + expect(targetCheckbox.checked).toBe(false); + act(() => jest.runAllTimers()); + oldWidth = compareWidths(rows[1], oldWidth); + + // This previously failed, the first column wouldn't update its width + // when the 2nd to last column was removed + userEvent.click(reachCheckbox); + expect(reachCheckbox.checked).toBe(false); + act(() => jest.runAllTimers()); + oldWidth = compareWidths(rows[1], oldWidth); + columns = within(table).getAllByRole('columnheader'); + expect(columns).toHaveLength(2); + + // Re-add the column and check that the width decreases + userEvent.click(audienceCheckbox); + expect(audienceCheckbox.checked).toBe(true); + act(() => jest.runAllTimers()); + expect(parseInt(rows[1].childNodes[1].style.width, 10)).toBeLessThan(parseInt(oldWidth, 10)); + }); + }); + + describe('headerless columns', function () { + + let renderTable = (props, scale, showDivider = false) => render( + + + Foo + + Add Item + + + + + Foo 1 + + + + + + + + + , scale); + + it('renders table with headerless column with default scale', function () { + let {getByRole} = renderTable(); + let grid = getByRole('grid'); + expect(grid).toBeVisible(); + expect(grid).toHaveAttribute('aria-label', 'Table'); + expect(grid).toHaveAttribute('data-testid', 'test'); + + expect(grid).toHaveAttribute('aria-rowcount', '2'); + expect(grid).toHaveAttribute('aria-colcount', '2'); + let rowgroups = within(grid).getAllByRole('rowgroup'); + expect(rowgroups).toHaveLength(2); + + let headerRows = within(rowgroups[0]).getAllByRole('row'); + expect(headerRows).toHaveLength(1); + expect(headerRows[0]).toHaveAttribute('aria-rowindex', '1'); + + let headers = within(grid).getAllByRole('columnheader'); + expect(headers).toHaveLength(2); + let className = headers[1].className; + expect(className.includes('spectrum-Table-cell--hideHeader')).toBeTruthy(); + expect(headers[0]).toHaveTextContent('Foo'); + // visually hidden syle + expect(headers[1].childNodes[0].style.clipPath).toBe('inset(50%)'); + expect(headers[1].childNodes[0].style.width).toBe('1px'); + expect(headers[1].childNodes[0].style.height).toBe('1px'); + expect(headers[1]).not.toBeEmptyDOMElement(); + + + let rows = within(rowgroups[1]).getAllByRole('row'); + expect(rows).toHaveLength(1); + // The width of headerless column + expect(rows[0].childNodes[1].style.width).toBe('36px'); + let rowheader = within(rows[0]).getByRole('rowheader'); + expect(rowheader).toHaveTextContent('Foo 1'); + let actionCell = within(rows[0]).getAllByRole('gridcell'); + expect(actionCell).toHaveLength(1); + let buttons = within(actionCell[0]).getAllByRole('button'); + expect(buttons).toHaveLength(1); + className = actionCell[0].className; + expect(className.includes('spectrum-Table-cell--hideHeader')).toBeTruthy(); + }); + + it('renders table with headerless column with large scale', function () { + let {getByRole} = renderTable({}, 'large'); + let grid = getByRole('grid'); + expect(grid).toBeVisible(); + expect(grid).toHaveAttribute('aria-label', 'Table'); + expect(grid).toHaveAttribute('data-testid', 'test'); + let rowgroups = within(grid).getAllByRole('rowgroup'); + let rows = within(rowgroups[1]).getAllByRole('row'); + expect(rows).toHaveLength(1); + // The width of headerless column + expect(rows[0].childNodes[1].style.width).toBe('44px'); + }); + + it('renders table with headerless column and divider', function () { + let {getByRole} = renderTable({}, undefined, true); + let grid = getByRole('grid'); + expect(grid).toBeVisible(); + let rowgroups = within(grid).getAllByRole('rowgroup'); + expect(rowgroups).toHaveLength(2); + let rows = within(rowgroups[1]).getAllByRole('row'); + expect(rows).toHaveLength(1); + // The width of headerless column with divider + expect(rows[0].childNodes[1].style.width).toBe('37px'); + }); + + it('renders table with headerless column with tooltip', function () { + let {getByRole} = renderTable({}, 'large'); + let grid = getByRole('grid'); + expect(grid).toBeVisible(); + expect(grid).toHaveAttribute('aria-label', 'Table'); + expect(grid).toHaveAttribute('data-testid', 'test'); + let headers = within(grid).getAllByRole('columnheader'); + let headerlessColumn = headers[1]; + act(() => { + headerlessColumn.focus(); + }); + let tooltip = getByRole('tooltip'); + expect(tooltip).toBeVisible(); + }); + + }); +}); diff --git a/packages/@react-stately/layout/src/TableLayout_DEPRECATED.ts b/packages/@react-stately/layout/src/TableLayout_DEPRECATED.ts deleted file mode 100644 index 5e090bfe31a..00000000000 --- a/packages/@react-stately/layout/src/TableLayout_DEPRECATED.ts +++ /dev/null @@ -1,426 +0,0 @@ -/* - * Copyright 2020 Adobe. All rights reserved. - * This file is licensed to you under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. You may obtain a copy - * of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under - * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS - * OF ANY KIND, either express or implied. See the License for the specific language - * governing permissions and limitations under the License. - */ - -import {ColumnProps, TableCollection} from '@react-types/table'; -import {GridNode} from '@react-types/grid'; -import {Key} from 'react'; -import {LayoutInfo, Point, Rect, Size} from '@react-stately/virtualizer'; -import {LayoutNode, ListLayout, ListLayoutOptions} from './ListLayout'; - - -type TableLayoutOptions = ListLayoutOptions & { - getDefaultWidth: (props) => string | number -} - -export class TableLayout_DEPRECATED extends ListLayout { - collection: TableCollection; - lastCollection: TableCollection; - columnWidths: Map; - stickyColumnIndices: number[]; - getDefaultWidth: (props) => string | number; - wasLoading = false; - isLoading = false; - - constructor(options: TableLayoutOptions) { - super(options); - this.getDefaultWidth = options.getDefaultWidth; - } - - - buildCollection(): LayoutNode[] { - // If columns changed, clear layout cache. - if ( - !this.lastCollection || - this.collection.columns.length !== this.lastCollection.columns.length || - this.collection.columns.some((c, i) => c.key !== this.lastCollection.columns[i].key) - ) { - // Invalidate everything in this layout pass. Will be reset in ListLayout on the next pass. - this.invalidateEverything = true; - } - - // Track whether we were previously loading. This is used to adjust the animations of async loading vs inserts. - let loadingState = this.collection.body.props.loadingState; - this.wasLoading = this.isLoading; - this.isLoading = loadingState === 'loading' || loadingState === 'loadingMore'; - - this.buildColumnWidths(); - let header = this.buildHeader(); - let body = this.buildBody(0); - body.layoutInfo.rect.width = Math.max(header.layoutInfo.rect.width, body.layoutInfo.rect.width); - this.contentSize = new Size(body.layoutInfo.rect.width, body.layoutInfo.rect.maxY); - return [ - header, - body - ]; - } - - buildColumnWidths() { - this.columnWidths = new Map(); - this.stickyColumnIndices = []; - - // Pass 1: set widths for all explicitly defined columns. - let remainingColumns = new Set>(); - let remainingSpace = this.virtualizer.visibleRect.width; - for (let column of this.collection.columns) { - let props = column.props as ColumnProps; - let width = props.width ?? this.getDefaultWidth(props); - if (width != null) { - let w = this.parseWidth(width); - this.columnWidths.set(column.key, w); - remainingSpace -= w; - } else { - remainingColumns.add(column); - } - - // The selection cell and any other sticky columns always need to be visible. - // In addition, row headers need to be in the DOM for accessibility labeling. - if (column.props.isSelectionCell || this.collection.rowHeaderColumnKeys.has(column.key)) { - this.stickyColumnIndices.push(column.index); - } - } - - // Pass 2: if there are remaining columns, then distribute the remaining space evenly. - if (remainingColumns.size > 0) { - let columnWidth = remainingSpace / (this.collection.columns.length - this.columnWidths.size); - - for (let column of remainingColumns) { - let props = column.props as ColumnProps; - let minWidth = props.minWidth != null ? this.parseWidth(props.minWidth) : 75; - let maxWidth = props.maxWidth != null ? this.parseWidth(props.maxWidth) : Infinity; - let width = Math.floor(Math.max(minWidth, Math.min(maxWidth, columnWidth))); - - this.columnWidths.set(column.key, width); - remainingSpace -= width; - if (width !== columnWidth) { - columnWidth = remainingSpace / (this.collection.columns.length - this.columnWidths.size); - } - } - } - } - - parseWidth(width: number | string): number { - if (typeof width === 'string') { - let match = width.match(/^(\d+)%$/); - if (!match) { - throw new Error('Only percentages are supported as column widths'); - } - - return this.virtualizer.visibleRect.width * (parseInt(match[1], 10) / 100); - } - - return width; - } - - buildHeader(): LayoutNode { - let rect = new Rect(0, 0, 0, 0); - let layoutInfo = new LayoutInfo('header', 'header', rect); - - let y = 0; - let width = 0; - let children: LayoutNode[] = []; - for (let headerRow of this.collection.headerRows) { - let layoutNode = this.buildChild(headerRow, 0, y); - layoutNode.layoutInfo.parentKey = 'header'; - y = layoutNode.layoutInfo.rect.maxY; - width = Math.max(width, layoutNode.layoutInfo.rect.width); - children.push(layoutNode); - } - - rect.width = width; - rect.height = y; - - this.layoutInfos.set('header', layoutInfo); - - return { - layoutInfo, - children - }; - } - - buildHeaderRow(headerRow: GridNode, x: number, y: number) { - let rect = new Rect(0, y, 0, 0); - let row = new LayoutInfo('headerrow', headerRow.key, rect); - - let height = 0; - let columns: LayoutNode[] = []; - for (let cell of headerRow.childNodes) { - let layoutNode = this.buildChild(cell, x, y); - layoutNode.layoutInfo.parentKey = row.key; - x = layoutNode.layoutInfo.rect.maxX; - height = Math.max(height, layoutNode.layoutInfo.rect.height); - columns.push(layoutNode); - } - - this.setChildHeights(columns, height); - - rect.height = height; - rect.width = x; - - return { - layoutInfo: row, - children: columns - }; - } - - setChildHeights(children: LayoutNode[], height: number) { - for (let child of children) { - if (child.layoutInfo.rect.height !== height) { - // Need to copy the layout info before we mutate it. - child.layoutInfo = child.layoutInfo.copy(); - this.layoutInfos.set(child.layoutInfo.key, child.layoutInfo); - - child.layoutInfo.rect.height = height; - } - } - } - - getColumnWidth(node: GridNode) { - let colspan = node.colspan ?? 1; - let width = 0; - for (let i = 0; i < colspan; i++) { - let column = this.collection.columns[node.index + i]; - width += this.columnWidths.get(column.key); - } - - return width; - } - - getEstimatedHeight(node: GridNode, width: number, height: number, estimatedHeight: number) { - let isEstimated = false; - - // If no explicit height is available, use an estimated height. - if (height == null) { - // If a previous version of this layout info exists, reuse its height. - // Mark as estimated if the size of the overall collection view changed, - // or the content of the item changed. - let previousLayoutNode = this.layoutNodes.get(node.key); - if (previousLayoutNode) { - let curNode = this.collection.getItem(node.key); - let lastNode = this.lastCollection ? this.lastCollection.getItem(node.key) : null; - height = previousLayoutNode.layoutInfo.rect.height; - isEstimated = curNode !== lastNode || width !== previousLayoutNode.layoutInfo.rect.width || previousLayoutNode.layoutInfo.estimatedSize; - } else { - height = estimatedHeight; - isEstimated = true; - } - } - - return {height, isEstimated}; - } - - buildColumn(node: GridNode, x: number, y: number): LayoutNode { - let width = this.getColumnWidth(node); - let {height, isEstimated} = this.getEstimatedHeight(node, width, this.headingHeight, this.estimatedHeadingHeight); - let rect = new Rect(x, y, width, height); - let layoutInfo = new LayoutInfo(node.type, node.key, rect); - layoutInfo.isSticky = node.props?.isSelectionCell; - layoutInfo.zIndex = layoutInfo.isSticky ? 2 : 1; - layoutInfo.estimatedSize = isEstimated; - - return { - layoutInfo - }; - } - - buildBody(y: number): LayoutNode { - let rect = new Rect(0, y, 0, 0); - let layoutInfo = new LayoutInfo('rowgroup', 'body', rect); - - let startY = y; - let width = 0; - let children: LayoutNode[] = []; - for (let node of this.collection.body.childNodes) { - let layoutNode = this.buildChild(node, 0, y); - layoutNode.layoutInfo.parentKey = 'body'; - y = layoutNode.layoutInfo.rect.maxY; - width = Math.max(width, layoutNode.layoutInfo.rect.width); - children.push(layoutNode); - } - - if (this.isLoading) { - let rect = new Rect(0, y, width || this.virtualizer.visibleRect.width, children.length === 0 ? this.virtualizer.visibleRect.height : 60); - let loader = new LayoutInfo('loader', 'loader', rect); - loader.parentKey = 'body'; - loader.isSticky = children.length === 0; - this.layoutInfos.set('loader', loader); - children.push({layoutInfo: loader}); - y = loader.rect.maxY; - width = Math.max(width, rect.width); - } else if (children.length === 0) { - let rect = new Rect(0, y, this.virtualizer.visibleRect.width, this.virtualizer.visibleRect.height); - let empty = new LayoutInfo('empty', 'empty', rect); - empty.parentKey = 'body'; - empty.isSticky = true; - this.layoutInfos.set('empty', empty); - children.push({layoutInfo: empty}); - y = empty.rect.maxY; - width = Math.max(width, rect.width); - } - - rect.width = width; - rect.height = y - startY; - - this.layoutInfos.set('body', layoutInfo); - - return { - layoutInfo, - children - }; - } - - buildNode(node: GridNode, x: number, y: number): LayoutNode { - switch (node.type) { - case 'headerrow': - return this.buildHeaderRow(node, x, y); - case 'item': - return this.buildRow(node, x, y); - case 'column': - case 'placeholder': - return this.buildColumn(node, x, y); - case 'cell': - return this.buildCell(node, x, y); - default: - throw new Error('Unknown node type ' + node.type); - } - } - - buildRow(node: GridNode, x: number, y: number): LayoutNode { - let rect = new Rect(x, y, 0, 0); - let layoutInfo = new LayoutInfo('row', node.key, rect); - - let children: LayoutNode[] = []; - let height = 0; - for (let child of node.childNodes) { - let layoutNode = this.buildChild(child, x, y); - x = layoutNode.layoutInfo.rect.maxX; - height = Math.max(height, layoutNode.layoutInfo.rect.height); - children.push(layoutNode); - } - - this.setChildHeights(children, height); - - rect.width = x; - rect.height = height + 1; // +1 for bottom border - - return { - layoutInfo, - children - }; - } - - buildCell(node: GridNode, x: number, y: number): LayoutNode { - let width = this.getColumnWidth(node); - let {height, isEstimated} = this.getEstimatedHeight(node, width, this.rowHeight, this.estimatedRowHeight); - let rect = new Rect(x, y, width, height); - let layoutInfo = new LayoutInfo(node.type, node.key, rect); - layoutInfo.isSticky = node.props?.isSelectionCell; - layoutInfo.zIndex = layoutInfo.isSticky ? 2 : 1; - layoutInfo.estimatedSize = isEstimated; - - return { - layoutInfo - }; - } - - getVisibleLayoutInfos(rect: Rect) { - let res: LayoutInfo[] = []; - - for (let node of this.rootNodes) { - res.push(node.layoutInfo); - this.addVisibleLayoutInfos(res, node, rect); - } - - return res; - } - - addVisibleLayoutInfos(res: LayoutInfo[], node: LayoutNode, rect: Rect) { - if (!node.children || node.children.length === 0) { - return; - } - - switch (node.layoutInfo.type) { - case 'header': { - for (let child of node.children) { - res.push(child.layoutInfo); - this.addVisibleLayoutInfos(res, child, rect); - } - break; - } - case 'rowgroup': { - let firstVisibleRow = this.binarySearch(node.children, rect.topLeft, 'y'); - let lastVisibleRow = this.binarySearch(node.children, rect.bottomRight, 'y'); - for (let i = firstVisibleRow; i <= lastVisibleRow; i++) { - res.push(node.children[i].layoutInfo); - this.addVisibleLayoutInfos(res, node.children[i], rect); - } - break; - } - case 'headerrow': - case 'row': { - let firstVisibleCell = this.binarySearch(node.children, rect.topLeft, 'x'); - let lastVisibleCell = this.binarySearch(node.children, rect.topRight, 'x'); - let stickyIndex = 0; - for (let i = firstVisibleCell; i <= lastVisibleCell; i++) { - // Sticky columns and row headers are always in the DOM. Interleave these - // with the visible range so that they are in the right order. - if (stickyIndex < this.stickyColumnIndices.length) { - let idx = this.stickyColumnIndices[stickyIndex]; - while (idx < i) { - res.push(node.children[idx].layoutInfo); - idx = this.stickyColumnIndices[stickyIndex++]; - } - } - - res.push(node.children[i].layoutInfo); - } - - while (stickyIndex < this.stickyColumnIndices.length) { - let idx = this.stickyColumnIndices[stickyIndex++]; - res.push(node.children[idx].layoutInfo); - } - break; - } - default: - throw new Error('Unknown node type ' + node.layoutInfo.type); - } - } - - binarySearch(items: LayoutNode[], point: Point, axis: 'x' | 'y') { - let low = 0; - let high = items.length - 1; - while (low <= high) { - let mid = (low + high) >> 1; - let item = items[mid]; - - if ((axis === 'x' && item.layoutInfo.rect.maxX < point.x) || (axis === 'y' && item.layoutInfo.rect.maxY < point.y)) { - low = mid + 1; - } else if ((axis === 'x' && item.layoutInfo.rect.x > point.x) || (axis === 'y' && item.layoutInfo.rect.y > point.y)) { - high = mid - 1; - } else { - return mid; - } - } - - return Math.max(0, Math.min(items.length - 1, low)); - } - - getInitialLayoutInfo(layoutInfo: LayoutInfo) { - let res = super.getInitialLayoutInfo(layoutInfo); - - // If this insert was the result of async loading, remove the zoom effect and just keep the fade in. - if (this.wasLoading) { - res.transform = null; - } - - return res; - } -} diff --git a/packages/@react-stately/layout/src/index.ts b/packages/@react-stately/layout/src/index.ts index 6efaae84499..5a355160c5d 100644 --- a/packages/@react-stately/layout/src/index.ts +++ b/packages/@react-stately/layout/src/index.ts @@ -12,4 +12,3 @@ export * from './ListLayout'; export * from './TableLayout'; -export * from './TableLayout_DEPRECATED'; diff --git a/packages/@react-stately/table/package.json b/packages/@react-stately/table/package.json index 824a31ccaf2..c8f8fab46dd 100644 --- a/packages/@react-stately/table/package.json +++ b/packages/@react-stately/table/package.json @@ -18,7 +18,6 @@ }, "dependencies": { "@babel/runtime": "^7.6.2", - "@react-aria/utils": "^3.13.0", "@react-stately/collections": "^3.4.0", "@react-stately/grid": "^3.2.0", "@react-stately/selection": "^3.10.0", diff --git a/packages/@react-stately/table/src/TableCollection.ts b/packages/@react-stately/table/src/TableCollection.ts index 8cce09d61a3..ba42b28b541 100644 --- a/packages/@react-stately/table/src/TableCollection.ts +++ b/packages/@react-stately/table/src/TableCollection.ts @@ -18,7 +18,6 @@ interface GridCollectionOptions { } const ROW_HEADER_COLUMN_KEY = 'row-header-column-' + Math.random().toString(36).slice(2); -const RESIZE_BUFFER_COLUMN_KEY = 'resize-buffer-column' + Math.random().toString(36).slice(2); function buildHeaderRows(keyMap: Map>, columnNodes: GridNode[]): GridNode[] { let columns = []; @@ -218,37 +217,6 @@ export class TableCollection extends GridCollection { visit(node); } - if (Array.from(nodes).some(node => node.props?.allowsResizing)) { - /* - If the table content width > table width, a horizontal scroll bar is present. - If a user tries to resize a column, making it smaller while they are scrolled to the - end of the content horizontally, it shrinks the total table content width, causing - things to snap around and breaks the resize behavior. - - To fix this, we add a resize buffer column (aka "spooky column") to the end of the table. - The width of this column defaults to 0. If you try and shrink a column and the width of the - table contents > table width, then the "spooky column" will grow to take up the difference - so that the total table content width remains constant while you are resizing. Once you - finish resizing, the "spooky column" snaps back to 0. - */ - let resizeBufferColumn: GridNode = { - type: 'column', - key: RESIZE_BUFFER_COLUMN_KEY, - value: null, - textValue: '', - level: 0, - index: columns.length, - hasChildNodes: false, - rendered: null, - childNodes: [], - props: { - isResizeBuffer: true, - defaultWidth: 0 - } - }; - columns.push(resizeBufferColumn); - } - let headerRows = buildHeaderRows(columnKeyMap, columns) as GridNode[]; headerRows.forEach((row, i) => rows.splice(i, 0, row)); diff --git a/packages/@react-stately/table/src/useTableColumnResizeState.ts b/packages/@react-stately/table/src/useTableColumnResizeState.ts index 6ebf85d8e43..9f30bbf3a67 100644 --- a/packages/@react-stately/table/src/useTableColumnResizeState.ts +++ b/packages/@react-stately/table/src/useTableColumnResizeState.ts @@ -65,7 +65,7 @@ export function useTableColumnResizeState(props: ColumnResizeStateProps): setColumnWidths(newWidths); } /* - returns the resolved column width in this order: + returns the resolved column width in this order: previously calculated width -> controlled width prop -> uncontrolled defaultWidth prop -> dev assigned width -> default dynamic width */ let getResolvedColumnWidth = useCallback((column: GridNode): (number | string) => { @@ -75,7 +75,7 @@ export function useTableColumnResizeState(props: ColumnResizeStateProps): let getStaticAndDynamicColumns = useCallback((columns: GridNode[]) : { staticColumns: GridNode[], dynamicColumns: GridNode[] } => columns.reduce((acc, column) => { let width = getResolvedColumnWidth(column); - return isStatic(width) ? {...acc, staticColumns: [...acc.staticColumns, column]} : {...acc, dynamicColumns: [...acc.dynamicColumns, column]}; + return isStatic(width) ? {...acc, staticColumns: [...acc.staticColumns, column]} : {...acc, dynamicColumns: [...acc.dynamicColumns, column]}; }, {staticColumns: [], dynamicColumns: []}), [getResolvedColumnWidth]); let buildColumnWidths = useCallback((affectedColumns: GridNode[], availableSpace: number): Map => { @@ -106,7 +106,7 @@ export function useTableColumnResizeState(props: ColumnResizeStateProps): const prevColKeys = columnsRef.current.map(col => col.key); const colKeys = columns.map(col => col.key); // if the columns change, need to rebuild widths. - if (!colKeys.every((col, i) => col === prevColKeys[i])) { + if (prevColKeys.length !== colKeys.length || !colKeys.every((col, i) => col === prevColKeys[i])) { columnsRef.current = columns; const widths = buildColumnWidths(columns, tableWidth.current); setColumnWidthsForRef(widths); @@ -139,9 +139,6 @@ export function useTableColumnResizeState(props: ColumnResizeStateProps): affectedColumnWidthsRef.current = []; let widths = new Map(columnWidthsRef.current); - // Need to set the resizeBufferColumn or "spooky column" back to 0 since done resizing; - const bufferColumnKey = columnsRef.current[columnsRef.current.length - 1].key; - widths.set(bufferColumnKey, 0); setColumnWidthsForRef(widths); } @@ -152,7 +149,6 @@ export function useTableColumnResizeState(props: ColumnResizeStateProps): // copy the columnWidths map and set the new width for the column being resized let widths = new Map(columnWidthsRef.current); - widths.set(columnsRef.current[columnsRef.current.length - 1].key, 0); widths.set(column.key, boundedWidth); // keep track of all columns that have been sized @@ -173,14 +169,11 @@ export function useTableColumnResizeState(props: ColumnResizeStateProps): } return acc; }, tableWidth.current); - + // merge the unaffected column widths and the recalculated column widths let recalculatedColumnWidths = buildColumnWidths(dynamicColumns, availableSpace); widths = new Map([...widths, ...recalculatedColumnWidths]); - if (startResizeContentWidth.current > tableWidth.current) { - widths.set(columnsRef.current[columnsRef.current.length - 1].key, Math.max(0, startResizeContentWidth.current - getContentWidth(widths))); - } setColumnWidthsForRef(widths); /* diff --git a/packages/@react-stately/table/src/useTableState.ts b/packages/@react-stately/table/src/useTableState.ts index 61fc3e2c24f..7255637e72a 100644 --- a/packages/@react-stately/table/src/useTableState.ts +++ b/packages/@react-stately/table/src/useTableState.ts @@ -10,12 +10,10 @@ * governing permissions and limitations under the License. */ -import {AffectedColumnWidths, useTableColumnResizeState} from './useTableColumnResizeState'; import {CollectionBase, Node, SelectionMode, Sortable, SortDescriptor, SortDirection} from '@react-types/shared'; -import {GridNode} from '@react-types/grid'; import {GridState, useGridState} from '@react-stately/grid'; import {TableCollection as ITableCollection} from '@react-types/table'; -import {Key, MutableRefObject, useMemo} from 'react'; +import {Key, useMemo} from 'react'; import {MultipleSelectionStateProps} from '@react-stately/selection'; import {TableCollection} from './TableCollection'; import {useCollection} from '@react-stately/collections'; @@ -28,25 +26,7 @@ export interface TableState extends GridState> { /** The current sorted column and direction. */ sortDescriptor: SortDescriptor, /** Calls the provided onSortChange handler with the provided column key and sort direction. */ - sort(columnKey: Key, direction?: 'ascending' | 'descending'): void, - /** A map of all the column widths by key. */ - columnWidths: MutableRefObject>, - /** Boolean for if a column is being resized. */ - isResizingColumn: boolean, - /** Getter for column width. */ - getColumnWidth(key: Key): number, - /** Getter for column min width. */ - getColumnMinWidth(key: Key): number, - /** Getter for column max widths. */ - getColumnMaxWidth(key: Key): number, - /** Trigger a resize and recalc. */ - onColumnResize: (column: GridNode, width: number) => void, - /** Runs at the start of resizing. */ - onColumnResizeStart: () => void, - /** Triggers the onColumnResizeEnd prop. */ - onColumnResizeEnd: () => void, - /** Need to be able to set the table width so that it can be used to calculate the column widths, this will trigger a recalc. */ - setTableWidth: (width: number) => void + sort(columnKey: Key, direction?: 'ascending' | 'descending'): void } export interface CollectionBuilderContext { @@ -57,13 +37,7 @@ export interface CollectionBuilderContext { export interface TableStateProps extends CollectionBase, MultipleSelectionStateProps, Sortable { /** Whether the row selection checkboxes should be displayed. */ - showSelectionCheckboxes?: boolean, - /** Function for determining the default width of columns. */ - getDefaultWidth?: (props) => string | number, - /** Callback that is invoked during the entirety of the resize event. */ - onColumnResize?: (affectedColumnWidths: AffectedColumnWidths) => void, - /** Callback that is invoked when the resize event is ended. */ - onColumnResizeEnd?: (affectedColumnWidths: AffectedColumnWidths) => void + showSelectionCheckboxes?: boolean } const OPPOSITE_SORT_DIRECTION = { @@ -90,9 +64,6 @@ export function useTableState(props: TableStateProps): Tabl context ); let {disabledKeys, selectionManager} = useGridState({...props, collection}); - - const tableColumnResizeState = useTableColumnResizeState({columns: collection.columns, getDefaultWidth: props.getDefaultWidth, onColumnResize: props.onColumnResize, onColumnResizeEnd: props.onColumnResizeEnd}); - return { collection, @@ -107,7 +78,6 @@ export function useTableState(props: TableStateProps): Tabl ? OPPOSITE_SORT_DIRECTION[props.sortDescriptor.direction] : 'ascending') }); - }, - ...tableColumnResizeState + } }; } diff --git a/packages/@react-stately/virtualizer/src/Virtualizer.ts b/packages/@react-stately/virtualizer/src/Virtualizer.ts index 959686674a2..bdb1843d61a 100644 --- a/packages/@react-stately/virtualizer/src/Virtualizer.ts +++ b/packages/@react-stately/virtualizer/src/Virtualizer.ts @@ -1164,7 +1164,7 @@ export class Virtualizer { for (let [key, view] of this._visibleViews) { // If an item has a width of 0, there is no need to remove it from the _visibleViews. // Removing an item with width of 0 can cause a loop where the item gets added, removed, - // added, removed... etc in a loop. The resize buffer ("spooky column") often has a width of 0. + // added, removed... etc in a loop. if (!finalMap.has(key) && view.layoutInfo.rect.width > 0) { transaction.removed.set(key, view); this._visibleViews.delete(key);