diff --git a/packages/@adobe/spectrum-css-temp/components/table/index.css b/packages/@adobe/spectrum-css-temp/components/table/index.css index 635cd6796a7..66d28948fd7 100644 --- a/packages/@adobe/spectrum-css-temp/components/table/index.css +++ b/packages/@adobe/spectrum-css-temp/components/table/index.css @@ -21,6 +21,7 @@ governing permissions and limitations under the License. } .spectrum-Table { + position: relative; border-collapse: separate; border-spacing: 0; outline: none; @@ -45,6 +46,9 @@ svg.spectrum-Table-sortedIcon { border-right-width: 1px; border-right-style: solid; flex: 0 0 auto; + padding-bottom: 1px; + margin-bottom: -1px; + z-index: 1; } .spectrum-Table-headCellContents { display: inline-block; @@ -94,17 +98,24 @@ svg.spectrum-Table-sortedIcon { } .spectrum-Table-headCellButton { box-sizing: border-box; - padding: var(--spectrum-table-header-padding-y) var(--spectrum-table-header-padding-x); + padding: var(--spectrum-table-header-padding-y) 0 var(--spectrum-table-header-padding-y) var(--spectrum-table-header-padding-x); + + /* truncate text with ellipsis */ + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; } } } .spectrum-Table-columnResizer { display: flex; - flex: 0 0 auto; - justify-content: flex-end; + position: absolute; + /* -9 aligns us with already existing cell dividers inside the table itself */ + inset-inline-end: -9px; + justify-content: center; box-sizing: border-box; - inline-size: 10px; + inline-size: 20px; block-size: 100%; user-select: none; @@ -119,11 +130,22 @@ svg.spectrum-Table-sortedIcon { &:active, &.focus-ring { outline: none; - &::after { - inline-size: 2px; - } } } +.spectrum-Table-columnResizerPlaceholder { + flex: 0 0 auto; + box-sizing: border-box; + inline-size: 10px; + block-size: 100%; + user-select: none; +} +.spectrum-Table-bodyResizeIndicator { + display: none; + position: absolute; + width: 2px; + height: 100%; + top: 0px; +} .spectrum-Table-cell--alignCenter { text-align: center; @@ -160,6 +182,9 @@ svg.spectrum-Table-sortedIcon { &.is-drop-target { @inherit: %drop-target; } + &.spectrum-Table-body--resizerAtTableEdge { + border-start-end-radius: 0; + } } /* The tbody tag doesn't allow setting a border-radius, so these hacks are to make that work @@ -404,3 +429,37 @@ svg.spectrum-Table-sortedIcon { .spectrum-Table-checkbox { vertical-align: super; } + +.spectrum-Table-colResizeIndicator { + display: none; + height: calc(100% + 1px); + width: 2px; + position: absolute; + top: 0; + inset-inline-end: 0; + pointer-events: none; + &.spectrum-Table-colResizeIndicator--visible { + display: block; + } +} +.spectrum-Table-colResizeNubbin { + display: none; + position: absolute; + top: 0px; + width: 16px; + height: 16px; + inset-inline-start: -7px; + &.spectrum-Table-colResizeNubbin--visible { + display: block; + } +} + +.resize-ew * { + cursor: col-resize !important; +} +.resize-e * { + cursor: e-resize !important; +} +.resize-w * { + cursor: w-resize !important; +} diff --git a/packages/@adobe/spectrum-css-temp/components/table/skin.css b/packages/@adobe/spectrum-css-temp/components/table/skin.css index 67f307319ee..a1996e47700 100644 --- a/packages/@adobe/spectrum-css-temp/components/table/skin.css +++ b/packages/@adobe/spectrum-css-temp/components/table/skin.css @@ -288,10 +288,21 @@ tbody.spectrum-Table-body { background-color: var(--spectrum-table-divider-border-color); } + /* don't want the divider to add to the resizer's width since it's */ &:active, &:focus-ring { &::after { - background-color: var(--spectrum-global-color-blue-400); + background-color: unset; } } } + +.spectrum-Table-colResizeIndicator { + &.spectrum-Table-colResizeIndicator--resizing { + background-color: var(--spectrum-global-color-blue-600); + } +} +.spectrum-Table-colResizeNubbin {} +.spectrum-Table-bodyResizeIndicator { + background-color: var(--spectrum-global-color-blue-600); +} diff --git a/packages/@react-aria/table/src/useTableColumnResize.ts b/packages/@react-aria/table/src/useTableColumnResize.ts index 7054c98a53a..bba0c8bf28e 100644 --- a/packages/@react-aria/table/src/useTableColumnResize.ts +++ b/packages/@react-aria/table/src/useTableColumnResize.ts @@ -11,7 +11,7 @@ */ import {ChangeEvent, RefObject, useCallback, useRef} from 'react'; -import {DOMAttributes} from '@react-types/shared'; +import {DOMAttributes, MoveEndEvent, MoveMoveEvent} from '@react-types/shared'; import {focusSafely} from '@react-aria/focus'; import {focusWithoutScrolling, mergeProps, useId} from '@react-aria/utils'; import {getColumnHeaderId} from './utils'; @@ -31,14 +31,14 @@ export interface AriaTableColumnResizeProps { column: GridNode, label: string, triggerRef: RefObject, - isDisabled?: boolean + isDisabled?: boolean, + onMove: (e: MoveMoveEvent) => void, + onMoveEnd: (e: MoveEndEvent) => void } export function useTableColumnResize(props: AriaTableColumnResizeProps, state: TableState, columnState: TableColumnResizeState, ref: RefObject): TableColumnResizeAria { let {column: item, triggerRef, isDisabled} = 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); stateRef.current = columnState; const stringFormatter = useLocalizedStringFormatter(intlMessages); let id = useId(); @@ -58,31 +58,33 @@ export function useTableColumnResize(props: AriaTableColumnResizeProps, st const {moveProps} = useMove({ onMoveStart() { columnResizeWidthRef.current = stateRef.current.getColumnWidth(item.key); - cursor.current = document.body.style.cursor; + stateRef.current.onColumnResizeStart(item); }, - onMove({deltaX, pointerType}) { + onMove(e) { + let {deltaX, deltaY, pointerType} = e; if (direction === 'rtl') { deltaX *= -1; } + if (pointerType === 'keyboard') { + if (deltaY !== 0 && deltaX === 0) { + deltaX = deltaY * -1; + } + deltaX *= 10; + } // if moving up/down only, no need to resize if (deltaX !== 0) { - if (pointerType === 'keyboard') { - deltaX *= 10; - } columnResizeWidthRef.current += deltaX; stateRef.current.onColumnResize(item, columnResizeWidthRef.current); - if (stateRef.current.getColumnMinWidth(item.key) >= stateRef.current.getColumnWidth(item.key)) { - document.body.style.setProperty('cursor', direction === 'rtl' ? 'w-resize' : 'e-resize'); - } else if (stateRef.current.getColumnMaxWidth(item.key) <= stateRef.current.getColumnWidth(item.key)) { - document.body.style.setProperty('cursor', direction === 'rtl' ? 'e-resize' : 'w-resize'); - } else { - document.body.style.setProperty('cursor', 'col-resize'); - } + props.onMove(e); } }, - onMoveEnd() { + onMoveEnd(e) { + let {pointerType} = e; columnResizeWidthRef.current = 0; - document.body.style.cursor = cursor.current; + props.onMoveEnd(e); + if (pointerType === 'mouse') { + stateRef.current.onColumnResizeEnd(item); + } } }); let min = Math.floor(stateRef.current.getColumnMinWidth(item.key)); diff --git a/packages/@react-spectrum/table/src/Nubbin.tsx b/packages/@react-spectrum/table/src/Nubbin.tsx new file mode 100644 index 00000000000..4800aac19ee --- /dev/null +++ b/packages/@react-spectrum/table/src/Nubbin.tsx @@ -0,0 +1,30 @@ +/* + * Copyright 2022 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 React from 'react'; + + +// TODO resize with scale? colors should be variables +export function Nubbin() { + return ( + + + + + + + + + + + ); +} diff --git a/packages/@react-spectrum/table/src/Resizer.tsx b/packages/@react-spectrum/table/src/Resizer.tsx index 910b432bf61..46e69d429ad 100644 --- a/packages/@react-spectrum/table/src/Resizer.tsx +++ b/packages/@react-spectrum/table/src/Resizer.tsx @@ -4,8 +4,10 @@ import {FocusRing} from '@react-aria/focus'; import {GridNode} from '@react-types/grid'; // @ts-ignore import intlMessages from '../intl/*.json'; -import React, {RefObject} from 'react'; +import {MoveMoveEvent} from '@react-types/shared'; +import React, {RefObject, useRef} from 'react'; import styles from '@adobe/spectrum-css-temp/components/table/vars.css'; +import {TableColumnResizeState} from '@react-stately/table'; import {useLocale, useLocalizedStringFormatter} from '@react-aria/i18n'; import {useTableColumnResize} from '@react-aria/table'; import {useTableContext} from './TableView'; @@ -14,7 +16,8 @@ import {VisuallyHidden} from '@react-aria/visually-hidden'; interface ResizerProps { column: GridNode, showResizer: boolean, - triggerRef: RefObject + triggerRef: RefObject, + onMoveResizer: (e: MoveMoveEvent) => void } function Resizer(props: ResizerProps, ref: RefObject) { @@ -22,8 +25,32 @@ function Resizer(props: ResizerProps, ref: RefObject) { let {state, columnState, isEmpty} = useTableContext(); let stringFormatter = useLocalizedStringFormatter(intlMessages); let {direction} = useLocale(); + const stateRef = useRef>(null); + stateRef.current = columnState; - let {inputProps, resizerProps} = useTableColumnResize({...props, label: stringFormatter.format('columnResizer'), isDisabled: isEmpty}, state, columnState, ref); + let {inputProps, resizerProps} = useTableColumnResize({ + ...props, + label: stringFormatter.format('columnResizer'), + isDisabled: isEmpty, + onMove: (e) => { + document.body.classList.remove(classNames(styles, 'resize-ew')); + document.body.classList.remove(classNames(styles, 'resize-e')); + document.body.classList.remove(classNames(styles, 'resize-w')); + if (stateRef.current.getColumnMinWidth(column.key) >= stateRef.current.getColumnWidth(column.key)) { + document.body.classList.add(direction === 'rtl' ? classNames(styles, 'resize-w') : classNames(styles, 'resize-e')); + } else if (stateRef.current.getColumnMaxWidth(column.key) <= stateRef.current.getColumnWidth(column.key)) { + document.body.classList.add(direction === 'rtl' ? classNames(styles, 'resize-e') : classNames(styles, 'resize-w')); + } else { + document.body.classList.add(classNames(styles, 'resize-ew')); + } + props.onMoveResizer(e); + }, + onMoveEnd: () => { + document.body.classList.remove(classNames(styles, 'resize-ew')); + document.body.classList.remove(classNames(styles, 'resize-e')); + document.body.classList.remove(classNames(styles, 'resize-w')); + } + }, state, columnState, ref); let style = { cursor: undefined, @@ -40,20 +67,27 @@ function Resizer(props: ResizerProps, ref: RefObject) { } return ( - + <> + +
+ + + +
+
+ {/* Placeholder so that the title doesn't intersect with space reserved by the resizer when it appears. */}
- - - -
-
+ className={classNames(styles, 'spectrum-Table-columnResizerPlaceholder')} /> + ); } diff --git a/packages/@react-spectrum/table/src/TableView.tsx b/packages/@react-spectrum/table/src/TableView.tsx index 6d1b901db18..fb1a215d53f 100644 --- a/packages/@react-spectrum/table/src/TableView.tsx +++ b/packages/@react-spectrum/table/src/TableView.tsx @@ -14,13 +14,15 @@ import ArrowDownSmall from '@spectrum-icons/ui/ArrowDownSmall'; import {chain, mergeProps, useLayoutEffect} from '@react-aria/utils'; import {Checkbox} from '@react-spectrum/checkbox'; import {classNames, useDOMRef, useFocusableRef, useStyleProps, useUnwrapDOMRef} from '@react-spectrum/utils'; -import {DOMRef, FocusableRef} from '@react-types/shared'; +import {DOMRef, FocusableRef, MoveMoveEvent} from '@react-types/shared'; import {FocusRing, FocusScope, useFocusRing} from '@react-aria/focus'; +import {getInteractionModality, useHover, usePress} from '@react-aria/interactions'; import {GridNode} from '@react-types/grid'; // @ts-ignore import intlMessages from '../intl/*.json'; import {Item, Menu, MenuTrigger} from '@react-spectrum/menu'; import {layoutInfoToStyle, ScrollView, setScrollLeft, useVirtualizer, VirtualizerItem} from '@react-aria/virtualizer'; +import {Nubbin} from './Nubbin'; import {ProgressCircle} from '@react-spectrum/progress'; import React, {ReactElement, useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react'; import {Rect, ReusableView, useVirtualizerState} from '@react-stately/virtualizer'; @@ -32,7 +34,6 @@ import {TableColumnResizeState, TableState, useTableColumnResizeState, useTableS import {TableLayout} from '@react-stately/layout'; import {Tooltip, TooltipTrigger} from '@react-spectrum/tooltip'; import {useButton} from '@react-aria/button'; -import {useHover, usePress} from '@react-aria/interactions'; import {useLocale, useLocalizedStringFormatter} from '@react-aria/i18n'; import {useProvider, useProviderProps} from '@react-spectrum/provider'; import { @@ -82,9 +83,14 @@ interface TableContextValue { layout: TableLayout, columnState: TableColumnResizeState, headerRowHovered: boolean, - isEmpty: boolean + isInResizeMode: boolean, + setIsInResizeMode: (val: boolean) => void, + isEmpty: boolean, + onFocusedResizer: () => void, + onMoveResizer: (e: MoveMoveEvent) => void } + const TableContext = React.createContext>(null); export function useTableContext() { return useContext(TableContext); @@ -108,13 +114,16 @@ function TableView(props: SpectrumTableProps, ref: DOMRef { + setIsInResizeMode(false); + }}), getDefaultWidth}, state.collection); // If the selection behavior changes in state, we need to update showSelectionCheckboxes here due to the circular dependency... let shouldShowCheckboxes = state.selectionManager.selectionBehavior !== 'replace'; @@ -123,6 +132,7 @@ function TableView(props: SpectrumTableProps, ref: DOMRef(); let bodyRef = useRef(); let stringFormatter = useLocalizedStringFormatter(intlMessages); @@ -309,8 +319,21 @@ function TableView(props: SpectrumTableProps, ref: DOMRef { + bodyRef.current.scrollLeft = headerRef.current.scrollLeft; + }; + + let lastResizeInteractionModality = useRef(undefined); + let onMoveResizer = (e) => { + if (e.pointerType === 'keyboard') { + lastResizeInteractionModality.current = e.pointerType; + } else { + lastResizeInteractionModality.current = undefined; + } + }; + return ( - + (props: SpectrumTableProps, ref: DOMRef @@ -348,9 +373,9 @@ function TableView(props: SpectrumTableProps, ref: DOMRef(); + let {state: tableState, columnState} = useTableContext(); let loadingState = collection.body.props.loadingState; let isLoading = loadingState === 'loading' || loadingState === 'loadingMore'; let onLoadMore = collection.body.props.onLoadMore; @@ -389,13 +414,20 @@ function TableVirtualizer({layout, collection, focusedKey, renderView, renderWra state.virtualizer.relayoutNow({sizeChanged: true}); }, [getColumnWidth, state.virtualizer]); + useEffect(() => { + if (lastResizeInteractionModality.current === 'keyboard' && headerRef.current.contains(document.activeElement)) { + document.activeElement?.scrollIntoView?.(false); + bodyRef.current.scrollLeft = headerRef.current.scrollLeft; + } + }, [state.contentSize, headerRef, bodyRef, lastResizeInteractionModality]); + 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]); + }, [bodyRef, headerRef]); let onVisibleRectChange = useCallback((rect: Rect) => { setTableWidth(rect.width); @@ -418,10 +450,23 @@ function TableVirtualizer({layout, collection, focusedKey, renderView, renderWra } }, [state.contentSize, state.virtualizer, state.isAnimating, onLoadMore, isLoading]); + let keysBefore = []; + let key = columnState.currentlyResizingColumn; + do { + keysBefore.push(key); + key = tableState.collection.getKeyBefore(key); + } while (key != null); + let resizerPosition = keysBefore.reduce((acc, key) => acc + columnState.getColumnWidth(key), 0) - 2; + let resizerAtEdge = resizerPosition > Math.max(state.virtualizer.contentSize.width, state.virtualizer.visibleRect.width) - 3; + // this should be fine, every movement of the resizer causes a rerender + // scrolling can cause it to lag for a moment, but it's always updated + let resizerInVisibleRegion = resizerPosition < state.virtualizer.visibleRect.width + (isNaN(bodyRef.current?.scrollLeft) ? 0 : bodyRef.current?.scrollLeft); + let shouldHardCornerResizeCorner = resizerAtEdge && resizerInVisibleRegion; + return (
{state.visibleViews[1]} +
@@ -550,7 +599,7 @@ function ResizableTableColumnHeader(props) { let ref = useRef(null); let triggerRef = useRef(null); let resizingRef = useRef(null); - let {state, columnState, headerRowHovered, isEmpty} = useTableContext(); + let {state, columnState, headerRowHovered, setIsInResizeMode, isInResizeMode, isEmpty, onFocusedResizer, onMoveResizer} = useTableContext(); let stringFormatter = useLocalizedStringFormatter(intlMessages); let {pressProps, isPressed} = usePress({isDisabled: isEmpty}); let {columnHeaderProps} = useTableColumnHeader({ @@ -578,6 +627,7 @@ function ResizableTableColumnHeader(props) { break; case 'resize': columnState.onColumnResizeStart(column); + setIsInResizeMode(true); break; } }; @@ -606,11 +656,12 @@ function ResizableTableColumnHeader(props) { // without the immediate timeout, Android Chrome doesn't move focus to the resizer setTimeout(() => { resizingRef.current.focus(); + onFocusedResizer(); }, 0); } }, [columnState.currentlyResizingColumn, column.key]); - let showResizer = !isEmpty && (headerRowHovered || columnState.currentlyResizingColumn != null); + let showResizer = !isEmpty && ((headerRowHovered && getInteractionModality() !== 'keyboard') || columnState.currentlyResizingColumn != null); return ( @@ -662,7 +713,29 @@ function ResizableTableColumnHeader(props) { ref={resizingRef} column={column} showResizer={showResizer} - triggerRef={useUnwrapDOMRef(triggerRef)} /> + triggerRef={useUnwrapDOMRef(triggerRef)} + onMoveResizer={onMoveResizer} /> +
+
+ +
+
); diff --git a/packages/@react-spectrum/table/stories/Table.stories.tsx b/packages/@react-spectrum/table/stories/Table.stories.tsx index 9a1c5db5a28..12abd791d9b 100644 --- a/packages/@react-spectrum/table/stories/Table.stories.tsx +++ b/packages/@react-spectrum/table/stories/Table.stories.tsx @@ -1333,7 +1333,7 @@ storiesOf('TableView', module) () => ( - File Name + File name for reference Type Size Weight diff --git a/packages/@react-spectrum/table/test/TableSizing.test.js b/packages/@react-spectrum/table/test/TableSizing.test.js index e0e8342bd78..e58296d9a17 100644 --- a/packages/@react-spectrum/table/test/TableSizing.test.js +++ b/packages/@react-spectrum/table/test/TableSizing.test.js @@ -324,10 +324,15 @@ describe('TableViewSizing', function () { 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); + let [headerRow, ...bodyRows] = tree.getAllByRole('row'); + expect(bodyRows).toHaveLength(2); - for (let row of rows) { + for (let [index, cell] of headerRow.childNodes.entries()) { + // 4 because there is a checkbox column + expect(Number(cell.style.zIndex)).toBe(4 - index + 1); + } + + for (let row of bodyRows) { for (let [index, cell] of row.childNodes.entries()) { if (index === 0) { expect(cell.style.zIndex).toBe('2'); @@ -631,8 +636,9 @@ describe('TableViewSizing', function () { it('dragging the resizer works - desktop', () => { jest.spyOn(window.screen, 'width', 'get').mockImplementation(() => 1024); + let onColumnResizeEnd = jest.fn(); let tree = render( - + Foo Bar @@ -679,6 +685,19 @@ describe('TableViewSizing', function () { expect(row.childNodes[1].style.width).toBe('200px'); expect(row.childNodes[2].style.width).toBe('200px'); } + expect(onColumnResizeEnd).toHaveBeenCalledTimes(1); + expect(onColumnResizeEnd).toHaveBeenCalledWith([ + { + key: 'foo', + width: 595 + }, { + key: 'bar', + width: 200 + }, { + key: 'baz', + width: 200 + } + ]); // actual locations do not matter, the delta matters between events for the calculation of useMove fireEvent.pointerDown(resizer, {pointerType: 'mouse', pointerId: 1, pageX: 595, pageY: 30}); @@ -691,6 +710,19 @@ describe('TableViewSizing', function () { expect(row.childNodes[1].style.width).toBe('190px'); expect(row.childNodes[2].style.width).toBe('190px'); } + expect(onColumnResizeEnd).toHaveBeenCalledTimes(2); + expect(onColumnResizeEnd).toHaveBeenCalledWith([ + { + key: 'foo', + width: 620 + }, { + key: 'bar', + width: 190 + }, { + key: 'baz', + width: 190 + } + ]); fireEvent.pointerLeave(resizer, {pointerType: 'mouse', pointerId: 1}); fireEvent.pointerLeave(resizableHeader, {pointerType: 'mouse', pointerId: 1}); @@ -700,8 +732,9 @@ describe('TableViewSizing', function () { }); it('dragging the resizer works - mobile', () => { + let onColumnResizeEnd = jest.fn(); let tree = render( - + Foo Bar @@ -748,6 +781,19 @@ describe('TableViewSizing', function () { expect(row.childNodes[1].style.width).toBe('200px'); expect(row.childNodes[2].style.width).toBe('200px'); } + expect(onColumnResizeEnd).toHaveBeenCalledTimes(1); + expect(onColumnResizeEnd).toHaveBeenCalledWith([ + { + key: 'foo', + width: 595 + }, { + key: 'bar', + width: 200 + }, { + key: 'baz', + width: 200 + } + ]); // actual locations do not matter, the delta matters between events for the calculation of useMove fireEvent.pointerDown(resizer, {pointerType: 'mouse', pointerId: 1, pageX: 595, pageY: 30}); @@ -760,6 +806,19 @@ describe('TableViewSizing', function () { expect(row.childNodes[1].style.width).toBe('190px'); expect(row.childNodes[2].style.width).toBe('190px'); } + expect(onColumnResizeEnd).toHaveBeenCalledTimes(2); + expect(onColumnResizeEnd).toHaveBeenCalledWith([ + { + key: 'foo', + width: 620 + }, { + key: 'bar', + width: 190 + }, { + key: 'baz', + width: 190 + } + ]); fireEvent.pointerLeave(resizer, {pointerType: 'mouse', pointerId: 1}); fireEvent.pointerLeave(resizableHeader, {pointerType: 'mouse', pointerId: 1}); @@ -775,8 +834,9 @@ describe('TableViewSizing', function () { it('dragging the resizer works - desktop', () => { setInteractionModality('pointer'); jest.spyOn(window.screen, 'width', 'get').mockImplementation(() => 1024); + let onColumnResizeEnd = jest.fn(); let tree = render( - + Foo Bar @@ -843,17 +903,32 @@ describe('TableViewSizing', function () { expect(row.childNodes[2].style.width).toBe('190px'); } - // tapping on the document.body doesn't cause a blur because the body isn't focusable, so just call blur + // tapping on the document.body doesn't cause a blur in jest because the body isn't focusable, so just call blur act(() => resizer.blur()); act(() => {jest.runAllTimers();}); + expect(onColumnResizeEnd).toHaveBeenCalledTimes(1); + expect(onColumnResizeEnd).toHaveBeenCalledWith([ + { + key: 'foo', + width: 620 + }, { + key: 'bar', + width: 190 + }, { + key: 'baz', + width: 190 + } + ]); + expect(tree.queryByRole('slider')).toBeNull(); }); it('dragging the resizer works - mobile', () => { setInteractionModality('pointer'); + let onColumnResizeEnd = jest.fn(); let tree = render( - + Foo Bar @@ -924,10 +999,24 @@ describe('TableViewSizing', function () { expect(row.childNodes[2].style.width).toBe('190px'); } - // tapping on the document.body doesn't cause a blur because the body isn't focusable, so just call blur + // tapping on the document.body doesn't cause a blur in jest because the body isn't focusable, so just call blur act(() => resizer.blur()); act(() => {jest.runAllTimers();}); + expect(onColumnResizeEnd).toHaveBeenCalledTimes(1); + expect(onColumnResizeEnd).toHaveBeenCalledWith([ + { + key: 'foo', + width: 620 + }, { + key: 'bar', + width: 190 + }, { + key: 'baz', + width: 190 + } + ]); + expect(tree.queryByRole('slider')).toBeNull(); }); }); @@ -935,8 +1024,9 @@ describe('TableViewSizing', function () { describe('keyboard', () => { it('arrow keys the resizer works - desktop', async () => { jest.spyOn(window.screen, 'width', 'get').mockImplementation(() => 1024); + let onColumnResizeEnd = jest.fn(); let tree = render( - + Foo Bar @@ -999,6 +1089,30 @@ describe('TableViewSizing', function () { fireEvent.keyUp(document.activeElement, {key: 'ArrowLeft'}); + 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'); + } + + fireEvent.keyDown(document.activeElement, {key: 'ArrowUp'}); + fireEvent.keyUp(document.activeElement, {key: 'ArrowUp'}); + fireEvent.keyDown(document.activeElement, {key: 'ArrowUp'}); + fireEvent.keyUp(document.activeElement, {key: 'ArrowUp'}); + + + for (let row of rows) { + expect(row.childNodes[0].style.width).toBe('620px'); + expect(row.childNodes[1].style.width).toBe('190px'); + expect(row.childNodes[2].style.width).toBe('190px'); + } + + fireEvent.keyDown(document.activeElement, {key: 'ArrowDown'}); + fireEvent.keyUp(document.activeElement, {key: 'ArrowDown'}); + fireEvent.keyDown(document.activeElement, {key: 'ArrowDown'}); + fireEvent.keyUp(document.activeElement, {key: 'ArrowDown'}); + + for (let row of rows) { expect(row.childNodes[0].style.width).toBe('600px'); expect(row.childNodes[1].style.width).toBe('200px'); @@ -1007,14 +1121,28 @@ describe('TableViewSizing', function () { fireEvent.keyDown(document.activeElement, {key: 'Escape'}); fireEvent.keyUp(document.activeElement, {key: 'Escape'}); + expect(onColumnResizeEnd).toHaveBeenCalledTimes(1); + expect(onColumnResizeEnd).toHaveBeenCalledWith([ + { + key: 'foo', + width: 600 + }, { + key: 'bar', + width: 200 + }, { + key: 'baz', + width: 200 + } + ]); expect(document.activeElement).toBe(resizableHeader); expect(tree.queryByRole('slider')).toBeNull(); }); it('arrow keys the resizer works - mobile', async () => { + let onColumnResizeEnd = jest.fn(); let tree = render( - + Foo Bar @@ -1085,6 +1213,19 @@ describe('TableViewSizing', function () { fireEvent.keyDown(document.activeElement, {key: 'Escape'}); fireEvent.keyUp(document.activeElement, {key: 'Escape'}); + expect(onColumnResizeEnd).toHaveBeenCalledTimes(1); + expect(onColumnResizeEnd).toHaveBeenCalledWith([ + { + key: 'foo', + width: 600 + }, { + key: 'bar', + width: 200 + }, { + key: 'baz', + width: 200 + } + ]); expect(document.activeElement).toBe(resizableHeader); @@ -1092,8 +1233,9 @@ describe('TableViewSizing', function () { }); it('can exit resize via Enter', async () => { jest.spyOn(window.screen, 'width', 'get').mockImplementation(() => 1024); + let onColumnResizeEnd = jest.fn(); let tree = render( - + Foo Bar @@ -1133,6 +1275,8 @@ describe('TableViewSizing', function () { fireEvent.keyDown(document.activeElement, {key: 'Enter'}); fireEvent.keyUp(document.activeElement, {key: 'Enter'}); + expect(onColumnResizeEnd).toHaveBeenCalledTimes(1); + expect(onColumnResizeEnd).toHaveBeenCalledWith([]); expect(document.activeElement).toBe(resizableHeader); @@ -1140,8 +1284,9 @@ describe('TableViewSizing', function () { }); it('can exit resize via Tab', async () => { jest.spyOn(window.screen, 'width', 'get').mockImplementation(() => 1024); + let onColumnResizeEnd = jest.fn(); let tree = render( - + Foo Bar @@ -1180,6 +1325,8 @@ describe('TableViewSizing', function () { expect(document.activeElement).toBe(resizer); userEvent.tab(); + expect(onColumnResizeEnd).toHaveBeenCalledTimes(1); + expect(onColumnResizeEnd).toHaveBeenCalledWith([]); expect(document.activeElement).toBe(resizableHeader); @@ -1187,8 +1334,9 @@ describe('TableViewSizing', function () { }); it('can exit resize via shift Tab', async () => { jest.spyOn(window.screen, 'width', 'get').mockImplementation(() => 1024); + let onColumnResizeEnd = jest.fn(); let tree = render( - + Foo Bar @@ -1227,6 +1375,8 @@ describe('TableViewSizing', function () { expect(document.activeElement).toBe(resizer); userEvent.tab({shift: true}); + expect(onColumnResizeEnd).toHaveBeenCalledTimes(1); + expect(onColumnResizeEnd).toHaveBeenCalledWith([]); expect(document.activeElement).toBe(resizableHeader); diff --git a/packages/@react-stately/layout/src/TableLayout.ts b/packages/@react-stately/layout/src/TableLayout.ts index 6fa161ef12a..f33e4065d06 100644 --- a/packages/@react-stately/layout/src/TableLayout.ts +++ b/packages/@react-stately/layout/src/TableLayout.ts @@ -103,6 +103,9 @@ export class TableLayout extends ListLayout { height = Math.max(height, layoutNode.layoutInfo.rect.height); columns.push(layoutNode); } + for (let [i, layout] of columns.entries()) { + layout.layoutInfo.zIndex = columns.length - i + 1; + } this.setChildHeights(columns, height); diff --git a/packages/@react-stately/table/src/useTableColumnResizeState.ts b/packages/@react-stately/table/src/useTableColumnResizeState.ts index 01e68b29eac..c8fd7c9b5ef 100644 --- a/packages/@react-stately/table/src/useTableColumnResizeState.ts +++ b/packages/@react-stately/table/src/useTableColumnResizeState.ts @@ -141,9 +141,9 @@ export function useTableColumnResizeState(props: TableColumnResizeStateProps, // eslint-disable-next-line @typescript-eslint/no-unused-vars function onColumnResizeEnd(column: GridNode) { + props.onColumnResizeEnd && isResizing.current && props.onColumnResizeEnd(affectedColumnWidthsRef.current); setCurrentlyResizingColumn(null); isResizing.current = false; - props.onColumnResizeEnd && props.onColumnResizeEnd(affectedColumnWidthsRef.current); affectedColumnWidthsRef.current = []; let widths = new Map(columnWidthsRef.current);