From 02c2487e26e53b30b0aaf1cb66ff112af8e9ef95 Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Thu, 16 Feb 2023 11:03:59 -0800 Subject: [PATCH 01/23] Updating column resize to support mode where resizer is always visible split out from https://github.com/adobe/react-spectrum/pull/4061, see that PR for more details and alternative approaches --- packages/@react-aria/grid/src/useGrid.ts | 30 +++++++++++- .../table/src/useTableColumnResize.ts | 48 ++++++++++++------- .../@react-spectrum/table/src/TableView.tsx | 3 +- .../table/test/TableSizing.test.tsx | 15 ++---- 4 files changed, 65 insertions(+), 31 deletions(-) diff --git a/packages/@react-aria/grid/src/useGrid.ts b/packages/@react-aria/grid/src/useGrid.ts index badf65ddea2..a2f477af464 100644 --- a/packages/@react-aria/grid/src/useGrid.ts +++ b/packages/@react-aria/grid/src/useGrid.ts @@ -16,7 +16,7 @@ import {GridCollection} from '@react-types/grid'; import {GridKeyboardDelegate} from './GridKeyboardDelegate'; import {gridMap} from './utils'; import {GridState} from '@react-stately/grid'; -import {Key, RefObject, useMemo} from 'react'; +import {Key, RefObject, useCallback, useMemo} from 'react'; import {useCollator, useLocale} from '@react-aria/i18n'; import {useGridSelectionAnnouncement} from './useGridSelectionAnnouncement'; import {useHighlightSelectionDescription} from './useHighlightSelectionDescription'; @@ -72,6 +72,7 @@ export function useGrid(props: GridProps, state: GridState(props: GridProps, state: GridState { + if (manager.isFocused) { + // If a focus event bubbled through a portal, reset focus state. + if (!e.currentTarget.contains(e.target)) { + manager.setFocused(false); + } + + return; + } + + // Focus events can bubble through portals. Ignore these events. + if (!e.currentTarget.contains(e.target)) { + return; + } + + manager.setFocused(true); + }, [manager]); + + // Continue to track collection focused state even if keyboard navigation is disabled + let navDisabledHandlers = useMemo(() => ({ + onBlur: collectionProps.onBlur, + onFocus + }), [onFocus, collectionProps.onBlur]); + let gridProps: DOMAttributes = mergeProps( domProps, { @@ -114,7 +140,7 @@ export function useGrid(props: GridProps, state: GridState(props: AriaTableColumnResizeProps, st let id = useId(); let isResizing = useRef(false); let lastSize = useRef(null); + let editModeEnabled = state.tableState.isKeyboardNavigationDisabled; let {direction} = useLocale(); let {keyboardProps} = useKeyboard({ onKeyDown: (e) => { - if (triggerRef?.current && (e.key === 'Escape' || e.key === 'Enter' || e.key === ' ' || e.key === 'Tab')) { - e.preventDefault(); - // switch focus back to the column header on anything that ends edit mode - focusSafely(triggerRef.current); + if (editModeEnabled) { + if (triggerRef?.current && (e.key === 'Escape' || e.key === 'Enter' || e.key === ' ' || e.key === 'Tab')) { + e.preventDefault(); + // switch focus back to the column header on anything that ends edit mode + focusSafely(triggerRef.current); + } + } else { + // Continue propagation on ArrowRight/Left so event bubbles to useSelectableCollection + if ((e.key === 'ArrowRight' || e.key === 'ArrowLeft')) { + e.continuePropagation(); + } + + if (e.key === 'Enter') { + state.tableState.setKeyboardNavigationDisabled(true); + } } } }); let startResize = useCallback((item) => { if (!isResizing.current) { - lastSize.current = state.updateResizedColumns(item.key, state.getColumnWidth(item.key)); - state.startResize(item.key); + lastSize.current = state.onColumnResize(item.key, state.getColumnWidth(item.key)); + state.onColumnResizeStart(item.key); onResizeStart?.(lastSize.current); } isResizing.current = true; }, [isResizing, onResizeStart, state]); let resize = useCallback((item, newWidth) => { - let sizes = state.updateResizedColumns(item.key, newWidth); + let sizes = state.onColumnResize(item.key, newWidth); onResize?.(sizes); lastSize.current = sizes; }, [onResize, state]); let endResize = useCallback((item) => { if (lastSize.current == null) { - lastSize.current = state.updateResizedColumns(item.key, state.getColumnWidth(item.key)); + lastSize.current = state.onColumnResize(item.key, state.getColumnWidth(item.key)); } + + state.onColumnResizeEnd(); if (isResizing.current) { - state.endResize(); onResizeEnd?.(lastSize.current); } isResizing.current = false; @@ -134,6 +147,13 @@ export function useTableColumnResize(props: AriaTableColumnResizeProps, st } }); + let onKeyDown = useCallback((e) => { + if (editModeEnabled) { + moveProps.onKeyDown(e); + } + }, [editModeEnabled, moveProps]); + + let min = Math.floor(state.getColumnMinWidth(item.key)); let max = Math.floor(state.getColumnMaxWidth(item.key)); if (max === Infinity) { @@ -186,7 +206,7 @@ export function useTableColumnResize(props: AriaTableColumnResizeProps, st onPress: (e) => { if (e.pointerType === 'touch') { focusInput(); - } else if (e.pointerType !== 'virtual') { + } else if (e.pointerType !== 'virtual' && e.pointerType !== 'keyboard') { if (triggerRef?.current) { focusSafely(triggerRef.current); } @@ -197,18 +217,12 @@ export function useTableColumnResize(props: AriaTableColumnResizeProps, st return { resizerProps: mergeProps( keyboardProps, - moveProps, + {...moveProps, onKeyDown}, pressProps ), inputProps: mergeProps( { id, - onFocus: () => { - // useMove calls onMoveStart for every keypress, but we want resize start to only be called when we start resize mode - // call instead during focus and blur - startResize(item); - state.tableState.setKeyboardNavigationDisabled(true); - }, onBlur: () => { endResize(item); state.tableState.setKeyboardNavigationDisabled(false); diff --git a/packages/@react-spectrum/table/src/TableView.tsx b/packages/@react-spectrum/table/src/TableView.tsx index 91c91d0f9f3..41c283d3019 100644 --- a/packages/@react-spectrum/table/src/TableView.tsx +++ b/packages/@react-spectrum/table/src/TableView.tsx @@ -732,8 +732,9 @@ function ResizableTableColumnHeader(props) { state.sort(column.key, 'descending'); break; case 'resize': - layout.startResize(column.key); + layout.onColumnResizeStart(column.key); setIsInResizeMode(true); + state.setKeyboardNavigationDisabled(true); break; } }; diff --git a/packages/@react-spectrum/table/test/TableSizing.test.tsx b/packages/@react-spectrum/table/test/TableSizing.test.tsx index 4e930cdff0a..81d7285b236 100644 --- a/packages/@react-spectrum/table/test/TableSizing.test.tsx +++ b/packages/@react-spectrum/table/test/TableSizing.test.tsx @@ -1258,9 +1258,7 @@ describe('TableViewSizing', function () { fireEvent.keyDown(document.activeElement, {key: 'Enter'}); fireEvent.keyUp(document.activeElement, {key: 'Enter'}); - expect(onResizeEnd).toHaveBeenCalledTimes(1); - expect(onResizeEnd).toHaveBeenCalledWith(new Map([['foo', 600], ['bar', '1fr'], ['baz', '1fr']])); - + expect(onResizeEnd).toHaveBeenCalledTimes(0); expect(document.activeElement).toBe(resizableHeader); expect(tree.queryByRole('slider')).toBeNull(); @@ -1307,11 +1305,7 @@ describe('TableViewSizing', function () { expect(document.activeElement).toBe(resizer); userEvent.tab(); - expect(onResizeEnd).toHaveBeenCalledTimes(1); - // TODO: should call with null or the currently calculated widths? - // might be hard to call with current values - expect(onResizeEnd).toHaveBeenCalledWith(new Map([['foo', 600], ['bar', '1fr'], ['baz', '1fr']])); - + expect(onResizeEnd).toHaveBeenCalledTimes(0); expect(document.activeElement).toBe(resizableHeader); expect(tree.queryByRole('slider')).toBeNull(); @@ -1359,9 +1353,7 @@ describe('TableViewSizing', function () { expect(document.activeElement).toBe(resizer); userEvent.tab({shift: true}); - expect(onResizeEnd).toHaveBeenCalledTimes(1); - expect(onResizeEnd).toHaveBeenCalledWith(new Map([['foo', 600], ['bar', '1fr'], ['baz', '1fr']])); - + expect(onResizeEnd).toHaveBeenCalledTimes(0); expect(document.activeElement).toBe(resizableHeader); expect(tree.queryByRole('slider')).toBeNull(); @@ -1656,6 +1648,7 @@ function resizeCol(tree, col, delta) { fireEvent.pointerDown(resizer, {pointerType: 'mouse', pointerId: 1, pageX: 0, pageY: 30}); act(() => {jest.runAllTimers();}); fireEvent.pointerMove(resizer, {pointerType: 'mouse', pointerId: 1, pageX: delta, pageY: 25}); + act(() => {jest.runAllTimers();}); fireEvent.pointerUp(resizer, {pointerType: 'mouse', pointerId: 1}); act(() => {jest.runAllTimers();}); } From a00fa21593caf09b81a4ae6be67ad5823e78190f Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Thu, 16 Feb 2023 11:09:52 -0800 Subject: [PATCH 02/23] update to match latest changes to api --- packages/@react-aria/table/src/useTableColumnResize.ts | 10 +++++----- packages/@react-spectrum/table/src/TableView.tsx | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/@react-aria/table/src/useTableColumnResize.ts b/packages/@react-aria/table/src/useTableColumnResize.ts index 7ae6b5f2141..aae52ed8362 100644 --- a/packages/@react-aria/table/src/useTableColumnResize.ts +++ b/packages/@react-aria/table/src/useTableColumnResize.ts @@ -89,25 +89,25 @@ export function useTableColumnResize(props: AriaTableColumnResizeProps, st let startResize = useCallback((item) => { if (!isResizing.current) { - lastSize.current = state.onColumnResize(item.key, state.getColumnWidth(item.key)); - state.onColumnResizeStart(item.key); + lastSize.current = state.updateResizedColumns(item.key, state.getColumnWidth(item.key)); + state.startResize(item.key); onResizeStart?.(lastSize.current); } isResizing.current = true; }, [isResizing, onResizeStart, state]); let resize = useCallback((item, newWidth) => { - let sizes = state.onColumnResize(item.key, newWidth); + let sizes = state.updateResizedColumns(item.key, newWidth); onResize?.(sizes); lastSize.current = sizes; }, [onResize, state]); let endResize = useCallback((item) => { if (lastSize.current == null) { - lastSize.current = state.onColumnResize(item.key, state.getColumnWidth(item.key)); + lastSize.current = state.updateResizedColumns(item.key, state.getColumnWidth(item.key)); } - state.onColumnResizeEnd(); + state.endResize(); if (isResizing.current) { onResizeEnd?.(lastSize.current); } diff --git a/packages/@react-spectrum/table/src/TableView.tsx b/packages/@react-spectrum/table/src/TableView.tsx index 41c283d3019..e840e8e4d21 100644 --- a/packages/@react-spectrum/table/src/TableView.tsx +++ b/packages/@react-spectrum/table/src/TableView.tsx @@ -732,7 +732,7 @@ function ResizableTableColumnHeader(props) { state.sort(column.key, 'descending'); break; case 'resize': - layout.onColumnResizeStart(column.key); + layout.startResize(column.key); setIsInResizeMode(true); state.setKeyboardNavigationDisabled(true); break; From c270a5ae694abb6772db0393b68faf5ce05977af Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Thu, 16 Feb 2023 11:36:54 -0800 Subject: [PATCH 03/23] mimic docs example --- .../table/stories/example-docs.tsx | 287 ++++++++++++++++++ .../table/stories/useTable.stories.tsx | 136 ++++++++- 2 files changed, 421 insertions(+), 2 deletions(-) create mode 100644 packages/@react-aria/table/stories/example-docs.tsx diff --git a/packages/@react-aria/table/stories/example-docs.tsx b/packages/@react-aria/table/stories/example-docs.tsx new file mode 100644 index 00000000000..2d3d9cb5551 --- /dev/null +++ b/packages/@react-aria/table/stories/example-docs.tsx @@ -0,0 +1,287 @@ +/* + * Copyright 2021 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 {mergeProps} from '@react-aria/utils'; +import React, {RefObject} from 'react'; +import {useButton} from 'react-aria'; +import {useFocusRing} from '@react-aria/focus'; +import {useRef} from 'react'; +import {useTable, useTableCell, useTableColumnHeader, useTableColumnResize, useTableHeaderRow, useTableRow, useTableRowGroup} from '@react-aria/table'; +import {useTableColumnResizeState, useTableState} from '@react-stately/table'; +import {VisuallyHidden} from '@react-aria/visually-hidden'; + +export function Table(props) { + let { + onResizeStart, + onResize, + onResizeEnd + } = props; + + let state = useTableState(props); + let ref = useRef(); + let {collection} = state; + let {gridProps} = useTable( + { + ...props, + // The table itself is scrollable rather than just the body + scrollRef: ref + }, + state, + ref + ); + + let layoutState = useTableColumnResizeState({ + // Matches the width of the table itself + tableWidth: 300 + }, state); + let {widths} = layoutState; + + return ( + + + {collection.headerRows.map(headerRow => ( + + {[...headerRow.childNodes].map(column => ( + + ))} + + ))} + + + {[...collection.body.childNodes].map(row => ( + + {[...row.childNodes].map(cell => ( + + ))} + + ))} + +
+ ); +} + +function ResizableTableRowGroup({type: Element, style, children}) { + let {rowGroupProps} = useTableRowGroup(); + return ( + + {children} + + ); +} + +function ResizableTableHeaderRow({item, state, children}) { + let ref = useRef(); + let {rowProps} = useTableHeaderRow({node: item}, state, ref); + + return ( + + {children} + + ); +} + +function ResizableTableColumnHeader({column, state, layoutState, onResizeStart, onResize, onResizeEnd}) { + let {widths} = layoutState; + let ref = useRef(null); + let resizerRef = useRef(null); + let {columnHeaderProps} = useTableColumnHeader({node: column}, state, ref); + let {isFocusVisible, focusProps} = useFocusRing(); + let allowsResizing = column.props.allowsResizing; + + return ( + 1 ? 'center' : 'left', + padding: '5px 10px', + outline: 'none', + boxShadow: isFocusVisible ? 'inset 0 0 0 2px orange' : 'none', + cursor: 'default', + width: widths.get(column.key), + display: 'block', + flex: '0 0 auto', + boxSizing: 'border-box' + }} + ref={ref}> +
+ + {allowsResizing && + + } +
+ + ); +} + +function Button(props) { + let ref = props.buttonRef; + let {focusProps, isFocusVisible} = useFocusRing(); + let outline = isFocusVisible + ? '2px solid orange' + : 'none'; + let {buttonProps} = useButton(props, ref); + return ; +} + +const Resizer = React.forwardRef((props: any, ref: RefObject) => { + let {column, layoutState, onResizeStart, onResize, onResizeEnd, triggerRef} = props; + let {resizerProps, inputProps} = useTableColumnResize({ + column, + label: 'Resizer', + onResizeStart, + onResize, + onResizeEnd, + triggerRef + }, layoutState, ref); + let {focusProps, isFocusVisible} = useFocusRing(); + + return ( +
+ + + +
+ ); +}); + +function ResizableTableRow({item, children, state}) { + let ref = useRef(); + let isSelected = state.selectionManager.isSelected(item.key); + let {rowProps, isPressed} = useTableRow({ + node: item + }, state, ref); + let {isFocusVisible, focusProps} = useFocusRing(); + + return ( + + {children} + + ); +} + +function ResizableTableCell({cell, state, widths}) { + let ref = useRef(); + let {gridCellProps} = useTableCell({node: cell}, state, ref); + let {isFocusVisible, focusProps} = useFocusRing(); + let column = cell.column; + + return ( + + {cell.rendered} + + ); +} diff --git a/packages/@react-aria/table/stories/useTable.stories.tsx b/packages/@react-aria/table/stories/useTable.stories.tsx index b46beb2f8a2..dc8a5731404 100644 --- a/packages/@react-aria/table/stories/useTable.stories.tsx +++ b/packages/@react-aria/table/stories/useTable.stories.tsx @@ -14,10 +14,12 @@ import {action} from '@storybook/addon-actions'; import {Table as BackwardCompatTable} from './example-backwards-compat'; import {Cell, Column, Row, TableBody, TableHeader} from '@react-stately/table'; import {ColumnSize, SpectrumTableProps} from '@react-types/table'; +import {Table as DocsTable} from './example-docs'; import {Meta, Story} from '@storybook/react'; import React, {Key, useCallback, useMemo, useState} from 'react'; import {Table as ResizingTable} from './example-resizing'; import {Table} from './example'; +import {useAsyncList} from 'react-stately'; const meta: Meta> = { title: 'useTable' @@ -26,7 +28,7 @@ const meta: Meta> = { export default meta; let columns = [ - {name: 'Name', uid: 'name'}, + {name: 'Naglwakenglkawnegklnakwlen glkawen glkawn gkaw neglkme', uid: 'name'}, {name: 'Type', uid: 'type'}, {name: 'Level', uid: 'level'} ]; @@ -53,7 +55,7 @@ const Template: Story> = (args) => ( {column => ( - + {column.name} )} @@ -244,3 +246,133 @@ export const TableWithSomeResizingFRsControlled = { column width state. `}} }; + +function ControlledDocsTable(props: {columns: Array<{name: string, uid: string, width?: ColumnSize | null}>, rows, onResize}) { + let {columns, ...otherProps} = props; + let [widths, _setWidths] = useState(() => new Map(columns.filter(col => col.width).map((col) => [col.uid as Key, col.width]))); + let setWidths = useCallback((newWidths: Map) => { + let controlledKeys = new Set(columns.filter(col => col.width).map((col) => col.uid as Key)); + let newVals = new Map(Array.from(newWidths).filter(([key]) => controlledKeys.has(key))); + _setWidths(newVals); + }, [columns]); + + // Needed to get past column caching so new sizes actually are rendered + // eslint-disable-next-line react-hooks/exhaustive-deps + let cols = useMemo(() => columns.map(col => ({...col})), [widths, columns]); + return ( + + + {column => ( + + {column.name} + + )} + + + {item => ( + + {columnKey => {item[columnKey]}} + + )} + + + ); +} + +function AsyncSortTable() { + let list = useAsyncList({ + async load({signal}) { + let res = await fetch('https://swapi.py4e.com/api/people/?search', { + signal + }); + let json = await res.json(); + return { + items: json.results + }; + }, + async sort({items, sortDescriptor}) { + return { + items: items.sort((a, b) => { + let first = a[sortDescriptor.column]; + let second = b[sortDescriptor.column]; + let cmp = (parseInt(first, 10) || first) < (parseInt(second, 10) || second) + ? -1 + : 1; + if (sortDescriptor.direction === 'descending') { + cmp *= -1; + } + return cmp; + }) + }; + } + }); + + return ( + + + Name + Height + Mass + Birth Year + + + {(item: any) => ( + + {(columnKey) => {item[columnKey]}} + + )} + + + ); +} + +export const DocExample = { + args: {}, + render: (args) => ( + + + {column => ( + + {column.name} + + )} + + + {item => ( + + {columnKey => {item[columnKey]}} + + )} + + + ) +}; + +export const DocExampleControlled = { + args: {columns: columnsFR}, + render: (args) => ( + + ) +}; + +export const DocExampleWithSorting = { + args: {}, + render: () => ( + + ) +}; From f6a3d076a36723815194046f73a7481a5ec2944e Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Thu, 16 Feb 2023 11:39:17 -0800 Subject: [PATCH 04/23] forgot to clean up some things --- packages/@react-aria/table/stories/useTable.stories.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/@react-aria/table/stories/useTable.stories.tsx b/packages/@react-aria/table/stories/useTable.stories.tsx index dc8a5731404..f00e1de3f5f 100644 --- a/packages/@react-aria/table/stories/useTable.stories.tsx +++ b/packages/@react-aria/table/stories/useTable.stories.tsx @@ -28,7 +28,7 @@ const meta: Meta> = { export default meta; let columns = [ - {name: 'Naglwakenglkawnegklnakwlen glkawen glkawn gkaw neglkme', uid: 'name'}, + {name: 'Name', uid: 'name'}, {name: 'Type', uid: 'type'}, {name: 'Level', uid: 'level'} ]; @@ -55,7 +55,7 @@ const Template: Story> = (args) => (
{column => ( - + {column.name} )} From cc62d52c459b95261739b75f0073a963ba915fc1 Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Thu, 16 Feb 2023 17:11:48 -0800 Subject: [PATCH 05/23] pulling in code changes from docs PR get rid of inline styles and fix case where there isnt a separate trigger for starting column resize --- .../table/src/useTableColumnResize.ts | 11 +- .../table/stories/docs-example.css | 94 ++++++++++++++++ .../table/stories/example-docs.tsx | 101 ++++-------------- 3 files changed, 121 insertions(+), 85 deletions(-) create mode 100644 packages/@react-aria/table/stories/docs-example.css diff --git a/packages/@react-aria/table/src/useTableColumnResize.ts b/packages/@react-aria/table/src/useTableColumnResize.ts index aae52ed8362..35f7197b316 100644 --- a/packages/@react-aria/table/src/useTableColumnResize.ts +++ b/packages/@react-aria/table/src/useTableColumnResize.ts @@ -69,10 +69,15 @@ export function useTableColumnResize(props: AriaTableColumnResizeProps, st let {keyboardProps} = useKeyboard({ onKeyDown: (e) => { if (editModeEnabled) { - if (triggerRef?.current && (e.key === 'Escape' || e.key === 'Enter' || e.key === ' ' || e.key === 'Tab')) { + if (e.key === 'Escape' || e.key === 'Enter' || e.key === ' ' || e.key === 'Tab') { e.preventDefault(); - // switch focus back to the column header on anything that ends edit mode - focusSafely(triggerRef.current); + if (triggerRef?.current) { + // switch focus back to the column header on anything that ends edit mode + focusSafely(triggerRef.current); + } else { + endResize(item); + state.tableState.setKeyboardNavigationDisabled(false); + } } } else { // Continue propagation on ArrowRight/Left so event bubbles to useSelectableCollection diff --git a/packages/@react-aria/table/stories/docs-example.css b/packages/@react-aria/table/stories/docs-example.css new file mode 100644 index 00000000000..230378d39cc --- /dev/null +++ b/packages/@react-aria/table/stories/docs-example.css @@ -0,0 +1,94 @@ +.aria-table { + border-collapse: collapse; + width: 300px; + height: 200px; + display: block; + position: relative; + overflow: auto; + + .aria-table-rowGroup { + display: block; + } + + .aria-table-rowGroupHeader { + border-bottom: 2px solid var(--spectrum-global-color-gray-800); + position: sticky; + top: 0; + background: var(--spectrum-gray-100); + width: fit-content; + } + + .aria-table-rowGroupBody { + max-height: 200px; + } + + .aria-table-row { + display: flex; + } + + .aria-table-headerCell { + padding: 5px 10px; + outline: none; + cursor: default; + display: block; + flex: 0 0 auto; + box-sizing: border-box; + text-align: left; + + .aria-table-headerTitle { + width: 100%; + text-align: left; + border: none; + background: transparent; + flex: 1 1 auto; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + margin-inline-start: -6px; + outline: none; + + &.focus { + outline: 2px solid orange; + } + } + } + + .aria-table-resizer { + cursor: col-resize; + width: 6px; + height: auto; + border: 2px; + border-style: none solid; + border-color: grey; + touch-action: none; + flex: 0 0 auto; + box-sizing: border-box; + margin-left: 4px; + + &.focus { + border-color: orange; + } + } + + .aria-table-row { + display: flex; + width: fit-content; + } + + .aria-table-cell { + padding: 5px 10px; + outline: none; + cursor: default; + display: block; + flex: 0 0 auto; + box-sizing: border-box; + box-shadow: none; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + + &.focus { + box-shadow: inset 0 0 0 2px orange; + } + } +} diff --git a/packages/@react-aria/table/stories/example-docs.tsx b/packages/@react-aria/table/stories/example-docs.tsx index 2d3d9cb5551..85631dab4cb 100644 --- a/packages/@react-aria/table/stories/example-docs.tsx +++ b/packages/@react-aria/table/stories/example-docs.tsx @@ -10,6 +10,8 @@ * governing permissions and limitations under the License. */ +import ariaStyles from './docs-example.css'; +import {classNames} from '@react-spectrum/utils'; import {mergeProps} from '@react-aria/utils'; import React, {RefObject} from 'react'; import {useButton} from 'react-aria'; @@ -49,24 +51,10 @@ export function Table(props) {
+ className={classNames(ariaStyles, 'aria-table')}> + className={classNames(ariaStyles, 'aria-table-rowGroupHeader')}> {collection.headerRows.map(headerRow => ( {[...headerRow.childNodes].map(column => ( @@ -83,9 +71,7 @@ export function Table(props) { ))} {[...collection.body.childNodes].map(row => ( @@ -103,15 +89,12 @@ export function Table(props) { ); } -function ResizableTableRowGroup({type: Element, style, children}) { +function ResizableTableRowGroup({type: Element, children, className}) { let {rowGroupProps} = useTableRowGroup(); return ( + className={classNames(ariaStyles, 'aria-table-rowGroup', className)}> {children} ); @@ -125,7 +108,7 @@ function ResizableTableHeaderRow({item, state, children}) { + className={classNames(ariaStyles, 'aria-table-row')}> {children} ); @@ -136,42 +119,23 @@ function ResizableTableColumnHeader({column, state, layoutState, onResizeStart, let ref = useRef(null); let resizerRef = useRef(null); let {columnHeaderProps} = useTableColumnHeader({node: column}, state, ref); - let {isFocusVisible, focusProps} = useFocusRing(); let allowsResizing = column.props.allowsResizing; return ( @@ -181,40 +145,25 @@ function ResizableTableColumnHeader({column, state, layoutState, onResizeStart, function Button(props) { let ref = props.buttonRef; let {focusProps, isFocusVisible} = useFocusRing(); - let outline = isFocusVisible - ? '2px solid orange' - : 'none'; let {buttonProps} = useButton(props, ref); - return ; + return ; } const Resizer = React.forwardRef((props: any, ref: RefObject) => { - let {column, layoutState, onResizeStart, onResize, onResizeEnd, triggerRef} = props; + let {column, layoutState, onResizeStart, onResize, onResizeEnd} = props; let {resizerProps, inputProps} = useTableColumnResize({ column, label: 'Resizer', onResizeStart, onResize, - onResizeEnd, - triggerRef + onResizeEnd }, layoutState, ref); let {focusProps, isFocusVisible} = useFocusRing(); return (
@@ -267,19 +215,8 @@ function ResizableTableCell({cell, state, widths}) { return (
From caefaf7487836ef0a0b51bd5fd78da6c0a1e5d5d Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Thu, 16 Feb 2023 17:54:14 -0800 Subject: [PATCH 06/23] remove sorting story and cleanup --- .../table/stories/example-docs.tsx | 14 ++--- .../table/stories/useTable.stories.tsx | 58 ------------------- 2 files changed, 7 insertions(+), 65 deletions(-) diff --git a/packages/@react-aria/table/stories/example-docs.tsx b/packages/@react-aria/table/stories/example-docs.tsx index 85631dab4cb..33d2566f5fe 100644 --- a/packages/@react-aria/table/stories/example-docs.tsx +++ b/packages/@react-aria/table/stories/example-docs.tsx @@ -13,7 +13,7 @@ import ariaStyles from './docs-example.css'; import {classNames} from '@react-spectrum/utils'; import {mergeProps} from '@react-aria/utils'; -import React, {RefObject} from 'react'; +import React from 'react'; import {useButton} from 'react-aria'; import {useFocusRing} from '@react-aria/focus'; import {useRef} from 'react'; @@ -117,7 +117,6 @@ function ResizableTableHeaderRow({item, state, children}) { function ResizableTableColumnHeader({column, state, layoutState, onResizeStart, onResize, onResizeEnd}) { let {widths} = layoutState; let ref = useRef(null); - let resizerRef = useRef(null); let {columnHeaderProps} = useTableColumnHeader({node: column}, state, ref); let allowsResizing = column.props.allowsResizing; @@ -126,7 +125,7 @@ function ResizableTableColumnHeader({column, state, layoutState, onResizeStart, {...mergeProps(columnHeaderProps)} className={classNames(ariaStyles, 'aria-table-headerCell')} style={{ - width: widths.get(column.key), + width: widths.get(column.key) }} ref={ref}>
@@ -135,7 +134,7 @@ function ResizableTableColumnHeader({column, state, layoutState, onResizeStart, {column.rendered} {allowsResizing && - + }
@@ -149,8 +148,9 @@ function Button(props) { return ; } -const Resizer = React.forwardRef((props: any, ref: RefObject) => { +function Resizer(props) { let {column, layoutState, onResizeStart, onResize, onResizeEnd} = props; + let ref = useRef(); let {resizerProps, inputProps} = useTableColumnResize({ column, label: 'Resizer', @@ -172,7 +172,7 @@ const Resizer = React.forwardRef((props: any, ref: RefObject) ); -}); +} function ResizableTableRow({item, children, state}) { let ref = useRef(); @@ -197,7 +197,7 @@ function ResizableTableRow({item, children, state}) { : 'none', color: isSelected ? 'white' : null, outline: 'none', - boxShadow: isFocusVisible ? 'inset 0 0 0 2px orange' : 'none', + boxShadow: isFocusVisible ? 'inset 0 0 0 2px orange' : 'none' }} {...mergeProps(rowProps, focusProps)} ref={ref}> diff --git a/packages/@react-aria/table/stories/useTable.stories.tsx b/packages/@react-aria/table/stories/useTable.stories.tsx index f00e1de3f5f..f692b4a219c 100644 --- a/packages/@react-aria/table/stories/useTable.stories.tsx +++ b/packages/@react-aria/table/stories/useTable.stories.tsx @@ -19,7 +19,6 @@ import {Meta, Story} from '@storybook/react'; import React, {Key, useCallback, useMemo, useState} from 'react'; import {Table as ResizingTable} from './example-resizing'; import {Table} from './example'; -import {useAsyncList} from 'react-stately'; const meta: Meta> = { title: 'useTable' @@ -283,56 +282,6 @@ function ControlledDocsTable(props: {columns: Array<{name: string, uid: string, ); } -function AsyncSortTable() { - let list = useAsyncList({ - async load({signal}) { - let res = await fetch('https://swapi.py4e.com/api/people/?search', { - signal - }); - let json = await res.json(); - return { - items: json.results - }; - }, - async sort({items, sortDescriptor}) { - return { - items: items.sort((a, b) => { - let first = a[sortDescriptor.column]; - let second = b[sortDescriptor.column]; - let cmp = (parseInt(first, 10) || first) < (parseInt(second, 10) || second) - ? -1 - : 1; - if (sortDescriptor.direction === 'descending') { - cmp *= -1; - } - return cmp; - }) - }; - } - }); - - return ( - - - Name - Height - Mass - Birth Year - - - {(item: any) => ( - - {(columnKey) => {item[columnKey]}} - - )} - - - ); -} - export const DocExample = { args: {}, render: (args) => ( @@ -369,10 +318,3 @@ export const DocExampleControlled = { ) }; - -export const DocExampleWithSorting = { - args: {}, - render: () => ( - - ) -}; From 01f2d31a9e60a37be0eb19eb83fa37b5a0e587ce Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Fri, 17 Feb 2023 09:47:39 -0800 Subject: [PATCH 07/23] starting resize on press for indicator this unfortunately causes a difference in behavior between starting a drag on menu (no resizestart until move) and starting a drag via mouse/touch (resizestart immediately on press) --- .../@react-aria/table/src/useTableColumnResize.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/@react-aria/table/src/useTableColumnResize.ts b/packages/@react-aria/table/src/useTableColumnResize.ts index 35f7197b316..f6d5ab16ddf 100644 --- a/packages/@react-aria/table/src/useTableColumnResize.ts +++ b/packages/@react-aria/table/src/useTableColumnResize.ts @@ -86,6 +86,7 @@ export function useTableColumnResize(props: AriaTableColumnResizeProps, st } if (e.key === 'Enter') { + startResize(item); state.tableState.setKeyboardNavigationDisabled(true); } } @@ -199,6 +200,7 @@ export function useTableColumnResize(props: AriaTableColumnResizeProps, st if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey || e.pointerType === 'keyboard') { return; } + if (e.pointerType === 'virtual' && state.resizingColumn != null) { endResize(item); if (triggerRef?.current) { @@ -206,14 +208,25 @@ export function useTableColumnResize(props: AriaTableColumnResizeProps, st } return; } + focusInput(); + if ((e.pointerType === 'mouse' || e.pointerType === 'virtual') && state.resizingColumn == null) { + startResize(item); + } }, onPress: (e) => { if (e.pointerType === 'touch') { focusInput(); - } else if (e.pointerType !== 'virtual' && e.pointerType !== 'keyboard') { + if (state.resizingColumn == null) { + startResize(item); + } + } + + if (e.pointerType === 'mouse') { if (triggerRef?.current) { focusSafely(triggerRef.current); + } else { + endResize(item); } } } From 5daa07a0ecd7f6a15b8634adf02392b5edb85cc6 Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Fri, 17 Feb 2023 11:38:37 -0800 Subject: [PATCH 08/23] using triggerRef existance to determine if behavior is resize on focus one test is still breaking, debugging --- .../table/src/useTableColumnResize.ts | 40 ++++++++++--------- .../@react-spectrum/table/src/Resizer.tsx | 1 + .../table/test/TableSizing.test.tsx | 12 ++++-- 3 files changed, 30 insertions(+), 23 deletions(-) diff --git a/packages/@react-aria/table/src/useTableColumnResize.ts b/packages/@react-aria/table/src/useTableColumnResize.ts index f6d5ab16ddf..fe8c46ed09a 100644 --- a/packages/@react-aria/table/src/useTableColumnResize.ts +++ b/packages/@react-aria/table/src/useTableColumnResize.ts @@ -36,7 +36,8 @@ export interface AriaTableColumnResizeProps { label: string, /** * Ref to the trigger if resizing was started from a column header menu. If it's provided, - * focus will be returned there when resizing is done. + * focus will be returned there when resizing is done. If it isn't provided, it is assumed that the resizer is + * visible at all time and keyboard resizing is started via pressing Enter on the resizer and not on focus. * */ triggerRef?: RefObject, /** If resizing is disabled. */ @@ -79,7 +80,7 @@ export function useTableColumnResize(props: AriaTableColumnResizeProps, st state.tableState.setKeyboardNavigationDisabled(false); } } - } else { + } else if (!triggerRef?.current) { // Continue propagation on ArrowRight/Left so event bubbles to useSelectableCollection if ((e.key === 'ArrowRight' || e.key === 'ArrowLeft')) { e.continuePropagation(); @@ -113,8 +114,8 @@ export function useTableColumnResize(props: AriaTableColumnResizeProps, st lastSize.current = state.updateResizedColumns(item.key, state.getColumnWidth(item.key)); } - state.endResize(); if (isResizing.current) { + state.endResize(); onResizeEnd?.(lastSize.current); } isResizing.current = false; @@ -147,7 +148,7 @@ export function useTableColumnResize(props: AriaTableColumnResizeProps, st onMoveEnd(e) { let {pointerType} = e; columnResizeWidthRef.current = 0; - if (pointerType === 'mouse') { + if (pointerType === 'mouse' || (pointerType === 'touch' && !triggerRef?.current)) { endResize(item); } } @@ -200,7 +201,6 @@ export function useTableColumnResize(props: AriaTableColumnResizeProps, st if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey || e.pointerType === 'keyboard') { return; } - if (e.pointerType === 'virtual' && state.resizingColumn != null) { endResize(item); if (triggerRef?.current) { @@ -209,25 +209,19 @@ export function useTableColumnResize(props: AriaTableColumnResizeProps, st return; } + // Sometimes onPress won't trigger for quick taps on mobile so we want to focus the input so blurring away + // can cancel resize mode for us. Also handles interaction where we are resizing focusInput(); - if ((e.pointerType === 'mouse' || e.pointerType === 'virtual') && state.resizingColumn == null) { + + // If resizer is always visible, mobile screenreader user can access the visually hidden resizer directly and thus we don't need + // to handle a virtual click to start the resizer. + if (e.pointerType !== 'virtual') { startResize(item); } }, onPress: (e) => { - if (e.pointerType === 'touch') { - focusInput(); - if (state.resizingColumn == null) { - startResize(item); - } - } - - if (e.pointerType === 'mouse') { - if (triggerRef?.current) { - focusSafely(triggerRef.current); - } else { - endResize(item); - } + if (((e.pointerType === 'touch' && !triggerRef?.current) || e.pointerType === 'mouse') && state.resizingColumn != null) { + endResize(item); } } }); @@ -241,6 +235,14 @@ export function useTableColumnResize(props: AriaTableColumnResizeProps, st inputProps: mergeProps( { id, + onFocus: () => { + if (triggerRef?.current) { + // useMove calls onMoveStart for every keypress, but we want resize start to only be called when we start resize mode + // call instead during focus and blur + startResize(item); + state.tableState.setKeyboardNavigationDisabled(true); + } + }, onBlur: () => { endResize(item); state.tableState.setKeyboardNavigationDisabled(false); diff --git a/packages/@react-spectrum/table/src/Resizer.tsx b/packages/@react-spectrum/table/src/Resizer.tsx index d35f04bdb09..f89565837de 100644 --- a/packages/@react-spectrum/table/src/Resizer.tsx +++ b/packages/@react-spectrum/table/src/Resizer.tsx @@ -65,6 +65,7 @@ function Resizer(props: ResizerProps, ref: RefObject) { mergeProps(props, { label: stringFormatter.format('columnResizer'), isDisabled: isEmpty, + shouldResizeOnFocus: true, onResizeStart: () => { if (getInteractionModality() === 'pointer') { if (layout.getColumnMinWidth(column.key) >= layout.getColumnWidth(column.key)) { diff --git a/packages/@react-spectrum/table/test/TableSizing.test.tsx b/packages/@react-spectrum/table/test/TableSizing.test.tsx index 81d7285b236..295b4cea92c 100644 --- a/packages/@react-spectrum/table/test/TableSizing.test.tsx +++ b/packages/@react-spectrum/table/test/TableSizing.test.tsx @@ -1258,7 +1258,8 @@ describe('TableViewSizing', function () { fireEvent.keyDown(document.activeElement, {key: 'Enter'}); fireEvent.keyUp(document.activeElement, {key: 'Enter'}); - expect(onResizeEnd).toHaveBeenCalledTimes(0); + expect(onResizeEnd).toHaveBeenCalledTimes(1); + expect(onResizeEnd).toHaveBeenCalledWith(new Map([['foo', 600], ['bar', '1fr'], ['baz', '1fr']])); expect(document.activeElement).toBe(resizableHeader); expect(tree.queryByRole('slider')).toBeNull(); @@ -1305,7 +1306,10 @@ describe('TableViewSizing', function () { expect(document.activeElement).toBe(resizer); userEvent.tab(); - expect(onResizeEnd).toHaveBeenCalledTimes(0); + expect(onResizeEnd).toHaveBeenCalledTimes(1); + // TODO: should call with null or the currently calculated widths? + // might be hard to call with current values + expect(onResizeEnd).toHaveBeenCalledWith(new Map([['foo', 600], ['bar', '1fr'], ['baz', '1fr']])); expect(document.activeElement).toBe(resizableHeader); expect(tree.queryByRole('slider')).toBeNull(); @@ -1349,11 +1353,11 @@ describe('TableViewSizing', function () { act(() => {jest.runAllTimers();}); let resizer = tree.getByRole('slider'); - expect(document.activeElement).toBe(resizer); userEvent.tab({shift: true}); - expect(onResizeEnd).toHaveBeenCalledTimes(0); + expect(onResizeEnd).toHaveBeenCalledTimes(1); + expect(onResizeEnd).toHaveBeenCalledWith(new Map([['foo', 600], ['bar', '1fr'], ['baz', '1fr']])); expect(document.activeElement).toBe(resizableHeader); expect(tree.queryByRole('slider')).toBeNull(); From 9834b8be6d689ba4a7e7cbea52557cefd304628f Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Fri, 17 Feb 2023 12:05:33 -0800 Subject: [PATCH 09/23] fixing test test would blur on rerender causing a column width update even though resizing wasnt happening. Changed conditonal so calling endResize only causes value updates if we are resizing --- packages/@react-aria/table/src/useTableColumnResize.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/@react-aria/table/src/useTableColumnResize.ts b/packages/@react-aria/table/src/useTableColumnResize.ts index fe8c46ed09a..65dc10c29da 100644 --- a/packages/@react-aria/table/src/useTableColumnResize.ts +++ b/packages/@react-aria/table/src/useTableColumnResize.ts @@ -110,11 +110,11 @@ export function useTableColumnResize(props: AriaTableColumnResizeProps, st }, [onResize, state]); let endResize = useCallback((item) => { - if (lastSize.current == null) { - lastSize.current = state.updateResizedColumns(item.key, state.getColumnWidth(item.key)); - } - if (isResizing.current) { + if (lastSize.current == null) { + lastSize.current = state.updateResizedColumns(item.key, state.getColumnWidth(item.key)); + } + state.endResize(); onResizeEnd?.(lastSize.current); } @@ -210,7 +210,7 @@ export function useTableColumnResize(props: AriaTableColumnResizeProps, st } // Sometimes onPress won't trigger for quick taps on mobile so we want to focus the input so blurring away - // can cancel resize mode for us. Also handles interaction where we are resizing + // can cancel resize mode for us. focusInput(); // If resizer is always visible, mobile screenreader user can access the visually hidden resizer directly and thus we don't need From d84ac1df45b5548cc53c077408cbe4989647d2b3 Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Fri, 17 Feb 2023 13:45:02 -0800 Subject: [PATCH 10/23] make resizer single line for focus --- .../@react-aria/table/stories/docs-example.css | 16 +++++++++++----- .../@react-aria/table/stories/example-docs.tsx | 2 +- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/packages/@react-aria/table/stories/docs-example.css b/packages/@react-aria/table/stories/docs-example.css index 230378d39cc..ee3584ce7ec 100644 --- a/packages/@react-aria/table/stories/docs-example.css +++ b/packages/@react-aria/table/stories/docs-example.css @@ -54,19 +54,25 @@ } .aria-table-resizer { - cursor: col-resize; width: 6px; + background-color: grey; + cursor: col-resize; height: auto; - border: 2px; - border-style: none solid; - border-color: grey; touch-action: none; flex: 0 0 auto; box-sizing: border-box; - margin-left: 4px; + border: 2px; + border-style: none solid; + border-color: transparent; + background-clip: content-box; &.focus { + background-color: orange; + } + + &.resizing { border-color: orange; + background: transparent; } } diff --git a/packages/@react-aria/table/stories/example-docs.tsx b/packages/@react-aria/table/stories/example-docs.tsx index 33d2566f5fe..b182dbb2b93 100644 --- a/packages/@react-aria/table/stories/example-docs.tsx +++ b/packages/@react-aria/table/stories/example-docs.tsx @@ -163,7 +163,7 @@ function Resizer(props) { return (
Date: Fri, 17 Feb 2023 13:47:23 -0800 Subject: [PATCH 11/23] nit reorganizing --- .../table/stories/useTable.stories.tsx | 72 +++++++++---------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/packages/@react-aria/table/stories/useTable.stories.tsx b/packages/@react-aria/table/stories/useTable.stories.tsx index f692b4a219c..b99f08d3e6c 100644 --- a/packages/@react-aria/table/stories/useTable.stories.tsx +++ b/packages/@react-aria/table/stories/useTable.stories.tsx @@ -246,42 +246,6 @@ export const TableWithSomeResizingFRsControlled = { `}} }; -function ControlledDocsTable(props: {columns: Array<{name: string, uid: string, width?: ColumnSize | null}>, rows, onResize}) { - let {columns, ...otherProps} = props; - let [widths, _setWidths] = useState(() => new Map(columns.filter(col => col.width).map((col) => [col.uid as Key, col.width]))); - let setWidths = useCallback((newWidths: Map) => { - let controlledKeys = new Set(columns.filter(col => col.width).map((col) => col.uid as Key)); - let newVals = new Map(Array.from(newWidths).filter(([key]) => controlledKeys.has(key))); - _setWidths(newVals); - }, [columns]); - - // Needed to get past column caching so new sizes actually are rendered - // eslint-disable-next-line react-hooks/exhaustive-deps - let cols = useMemo(() => columns.map(col => ({...col})), [widths, columns]); - return ( - - - {column => ( - - {column.name} - - )} - - - {item => ( - - {columnKey => {item[columnKey]}} - - )} - - - ); -} - export const DocExample = { args: {}, render: (args) => ( @@ -318,3 +282,39 @@ export const DocExampleControlled = { ) }; + +function ControlledDocsTable(props: {columns: Array<{name: string, uid: string, width?: ColumnSize | null}>, rows, onResize}) { + let {columns, ...otherProps} = props; + let [widths, _setWidths] = useState(() => new Map(columns.filter(col => col.width).map((col) => [col.uid as Key, col.width]))); + let setWidths = useCallback((newWidths: Map) => { + let controlledKeys = new Set(columns.filter(col => col.width).map((col) => col.uid as Key)); + let newVals = new Map(Array.from(newWidths).filter(([key]) => controlledKeys.has(key))); + _setWidths(newVals); + }, [columns]); + + // Needed to get past column caching so new sizes actually are rendered + // eslint-disable-next-line react-hooks/exhaustive-deps + let cols = useMemo(() => columns.map(col => ({...col})), [widths, columns]); + return ( + + + {column => ( + + {column.name} + + )} + + + {item => ( + + {columnKey => {item[columnKey]}} + + )} + + + ); +} From 5a6741c12a527cc354f0a2d32bd093d31f9c7408 Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Fri, 17 Feb 2023 14:05:29 -0800 Subject: [PATCH 12/23] mimic docs example remove selection from example to mirror docs --- packages/@react-aria/table/stories/docs-example.css | 2 +- packages/@react-aria/table/stories/useTable.stories.tsx | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/@react-aria/table/stories/docs-example.css b/packages/@react-aria/table/stories/docs-example.css index ee3584ce7ec..64ff83c9b7d 100644 --- a/packages/@react-aria/table/stories/docs-example.css +++ b/packages/@react-aria/table/stories/docs-example.css @@ -72,7 +72,7 @@ &.resizing { border-color: orange; - background: transparent; + background-color: transparent; } } diff --git a/packages/@react-aria/table/stories/useTable.stories.tsx b/packages/@react-aria/table/stories/useTable.stories.tsx index b99f08d3e6c..6ba4b3b01c0 100644 --- a/packages/@react-aria/table/stories/useTable.stories.tsx +++ b/packages/@react-aria/table/stories/useTable.stories.tsx @@ -250,10 +250,7 @@ export const DocExample = { args: {}, render: (args) => ( Date: Fri, 17 Feb 2023 15:07:48 -0800 Subject: [PATCH 13/23] adding description for keyboard users for Enter to start resizing this is for the aria table example where resizing is entered manually via Enter while focused on the resizer --- packages/@react-aria/table/intl/en-US.json | 3 ++- .../table/src/useTableColumnResize.ts | 23 +++++++++++-------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/packages/@react-aria/table/intl/en-US.json b/packages/@react-aria/table/intl/en-US.json index ff040bb66ee..78a7a208161 100644 --- a/packages/@react-aria/table/intl/en-US.json +++ b/packages/@react-aria/table/intl/en-US.json @@ -6,5 +6,6 @@ "descending": "descending", "ascendingSort": "sorted by column {columnName} in ascending order", "descendingSort": "sorted by column {columnName} in descending order", - "columnSize": "{value} pixels" + "columnSize": "{value} pixels", + "resizerDescription": "Press Enter to start resizing" } diff --git a/packages/@react-aria/table/src/useTableColumnResize.ts b/packages/@react-aria/table/src/useTableColumnResize.ts index 65dc10c29da..c8c34ef8bb2 100644 --- a/packages/@react-aria/table/src/useTableColumnResize.ts +++ b/packages/@react-aria/table/src/useTableColumnResize.ts @@ -13,13 +13,13 @@ import {ChangeEvent, Key, RefObject, useCallback, useRef} from 'react'; import {DOMAttributes, FocusableElement} from '@react-types/shared'; import {focusSafely} from '@react-aria/focus'; -import {focusWithoutScrolling, mergeProps, useId} from '@react-aria/utils'; +import {focusWithoutScrolling, mergeProps, useDescription, useId} from '@react-aria/utils'; import {getColumnHeaderId} from './utils'; import {GridNode} from '@react-types/grid'; // @ts-ignore import intlMessages from '../intl/*.json'; import {TableColumnResizeState} from '@react-stately/table'; -import {useKeyboard, useMove, usePress} from '@react-aria/interactions'; +import {useInteractionModality, useKeyboard, useMove, usePress} from '@react-aria/interactions'; import {useLocale, useLocalizedStringFormatter} from '@react-aria/i18n'; export interface TableColumnResizeAria { @@ -65,6 +65,7 @@ export function useTableColumnResize(props: AriaTableColumnResizeProps, st let isResizing = useRef(false); let lastSize = useRef(null); let editModeEnabled = state.tableState.isKeyboardNavigationDisabled; + let resizeOnFocus = !!triggerRef?.current; let {direction} = useLocale(); let {keyboardProps} = useKeyboard({ @@ -72,7 +73,7 @@ export function useTableColumnResize(props: AriaTableColumnResizeProps, st if (editModeEnabled) { if (e.key === 'Escape' || e.key === 'Enter' || e.key === ' ' || e.key === 'Tab') { e.preventDefault(); - if (triggerRef?.current) { + if (resizeOnFocus) { // switch focus back to the column header on anything that ends edit mode focusSafely(triggerRef.current); } else { @@ -80,7 +81,7 @@ export function useTableColumnResize(props: AriaTableColumnResizeProps, st state.tableState.setKeyboardNavigationDisabled(false); } } - } else if (!triggerRef?.current) { + } else if (!resizeOnFocus) { // Continue propagation on ArrowRight/Left so event bubbles to useSelectableCollection if ((e.key === 'ArrowRight' || e.key === 'ArrowLeft')) { e.continuePropagation(); @@ -148,7 +149,7 @@ export function useTableColumnResize(props: AriaTableColumnResizeProps, st onMoveEnd(e) { let {pointerType} = e; columnResizeWidthRef.current = 0; - if (pointerType === 'mouse' || (pointerType === 'touch' && !triggerRef?.current)) { + if (pointerType === 'mouse' || (pointerType === 'touch' && !resizeOnFocus)) { endResize(item); } } @@ -167,6 +168,9 @@ export function useTableColumnResize(props: AriaTableColumnResizeProps, st max = Number.MAX_SAFE_INTEGER; } let value = Math.floor(state.getColumnWidth(item.key)); + let modality = useInteractionModality(); + let description = !resizeOnFocus && modality === 'keyboard' && !isResizing.current ? stringFormatter.format('resizerDescription') : undefined; + let descriptionProps = useDescription(description); let ariaProps = { 'aria-label': props.label, 'aria-orientation': 'horizontal' as 'horizontal', @@ -175,7 +179,8 @@ export function useTableColumnResize(props: AriaTableColumnResizeProps, st 'type': 'range', min, max, - value + value, + ...descriptionProps }; const focusInput = useCallback(() => { @@ -203,7 +208,7 @@ export function useTableColumnResize(props: AriaTableColumnResizeProps, st } if (e.pointerType === 'virtual' && state.resizingColumn != null) { endResize(item); - if (triggerRef?.current) { + if (resizeOnFocus) { focusSafely(triggerRef.current); } return; @@ -220,7 +225,7 @@ export function useTableColumnResize(props: AriaTableColumnResizeProps, st } }, onPress: (e) => { - if (((e.pointerType === 'touch' && !triggerRef?.current) || e.pointerType === 'mouse') && state.resizingColumn != null) { + if (((e.pointerType === 'touch' && !resizeOnFocus) || e.pointerType === 'mouse') && state.resizingColumn != null) { endResize(item); } } @@ -236,7 +241,7 @@ export function useTableColumnResize(props: AriaTableColumnResizeProps, st { id, onFocus: () => { - if (triggerRef?.current) { + if (resizeOnFocus) { // useMove calls onMoveStart for every keypress, but we want resize start to only be called when we start resize mode // call instead during focus and blur startResize(item); From 1f4213fae824d6d9e5a62896b519e7be0fe140d1 Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Fri, 17 Feb 2023 15:09:49 -0800 Subject: [PATCH 14/23] fixing issue where tab wasnt exiting the table when focused on the reizer --- packages/@react-aria/table/src/useTableColumnResize.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/@react-aria/table/src/useTableColumnResize.ts b/packages/@react-aria/table/src/useTableColumnResize.ts index c8c34ef8bb2..37f39415364 100644 --- a/packages/@react-aria/table/src/useTableColumnResize.ts +++ b/packages/@react-aria/table/src/useTableColumnResize.ts @@ -82,8 +82,8 @@ export function useTableColumnResize(props: AriaTableColumnResizeProps, st } } } else if (!resizeOnFocus) { - // Continue propagation on ArrowRight/Left so event bubbles to useSelectableCollection - if ((e.key === 'ArrowRight' || e.key === 'ArrowLeft')) { + // Continue propagation on ArrowRight/Left/Tab so event bubbles to useSelectableCollection + if ((e.key === 'ArrowRight' || e.key === 'ArrowLeft' || e.key === 'Tab')) { e.continuePropagation(); } From 2bc566f1a0912cbc5a1b5674169f6e67bfe43207 Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Fri, 17 Feb 2023 15:23:12 -0800 Subject: [PATCH 15/23] adding min width for columns to avoid weirdness with trying to collapse 0 --- packages/@react-aria/table/stories/example-docs.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/@react-aria/table/stories/example-docs.tsx b/packages/@react-aria/table/stories/example-docs.tsx index b182dbb2b93..6e86787c6ea 100644 --- a/packages/@react-aria/table/stories/example-docs.tsx +++ b/packages/@react-aria/table/stories/example-docs.tsx @@ -13,7 +13,7 @@ import ariaStyles from './docs-example.css'; import {classNames} from '@react-spectrum/utils'; import {mergeProps} from '@react-aria/utils'; -import React from 'react'; +import React, {useCallback} from 'react'; import {useButton} from 'react-aria'; import {useFocusRing} from '@react-aria/focus'; import {useRef} from 'react'; @@ -41,9 +41,14 @@ export function Table(props) { ref ); + let getDefaultMinWidth = useCallback((node) => { + return 40; + }, []); + let layoutState = useTableColumnResizeState({ // Matches the width of the table itself - tableWidth: 300 + tableWidth: 300, + getDefaultMinWidth }, state); let {widths} = layoutState; From 54b732732613fed0b0cd775cba4d36020b467efd Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Fri, 17 Feb 2023 15:23:44 -0800 Subject: [PATCH 16/23] fix lint --- packages/@react-aria/table/stories/example-docs.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@react-aria/table/stories/example-docs.tsx b/packages/@react-aria/table/stories/example-docs.tsx index 6e86787c6ea..9874ef9945c 100644 --- a/packages/@react-aria/table/stories/example-docs.tsx +++ b/packages/@react-aria/table/stories/example-docs.tsx @@ -41,7 +41,7 @@ export function Table(props) { ref ); - let getDefaultMinWidth = useCallback((node) => { + let getDefaultMinWidth = useCallback(() => { return 40; }, []); From dc7aaa632a74c064fc466729269222058dae0629 Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Fri, 17 Feb 2023 16:02:30 -0800 Subject: [PATCH 17/23] propagate all keydown events if we arent in resize mode and have always visible resizers forgot that we also have other keyboard combos like cmd + a or escape that should also be handled by useSelectableCollection --- packages/@react-aria/table/src/useTableColumnResize.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/@react-aria/table/src/useTableColumnResize.ts b/packages/@react-aria/table/src/useTableColumnResize.ts index 37f39415364..b2d0d526de1 100644 --- a/packages/@react-aria/table/src/useTableColumnResize.ts +++ b/packages/@react-aria/table/src/useTableColumnResize.ts @@ -82,10 +82,8 @@ export function useTableColumnResize(props: AriaTableColumnResizeProps, st } } } else if (!resizeOnFocus) { - // Continue propagation on ArrowRight/Left/Tab so event bubbles to useSelectableCollection - if ((e.key === 'ArrowRight' || e.key === 'ArrowLeft' || e.key === 'Tab')) { - e.continuePropagation(); - } + // Continue propagation on keydown events so they still bubbles to useSelectableCollection and are handled there + e.continuePropagation(); if (e.key === 'Enter') { startResize(item); From c2a8a7f316a8a2ca4c28a19fcde01588f36eaa0b Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Fri, 17 Feb 2023 16:40:49 -0800 Subject: [PATCH 18/23] removing ref read in render --- packages/@react-aria/table/src/useTableColumnResize.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/@react-aria/table/src/useTableColumnResize.ts b/packages/@react-aria/table/src/useTableColumnResize.ts index b2d0d526de1..d7b4f463b8e 100644 --- a/packages/@react-aria/table/src/useTableColumnResize.ts +++ b/packages/@react-aria/table/src/useTableColumnResize.ts @@ -65,11 +65,11 @@ export function useTableColumnResize(props: AriaTableColumnResizeProps, st let isResizing = useRef(false); let lastSize = useRef(null); let editModeEnabled = state.tableState.isKeyboardNavigationDisabled; - let resizeOnFocus = !!triggerRef?.current; let {direction} = useLocale(); let {keyboardProps} = useKeyboard({ onKeyDown: (e) => { + let resizeOnFocus = !!triggerRef?.current; if (editModeEnabled) { if (e.key === 'Escape' || e.key === 'Enter' || e.key === ' ' || e.key === 'Tab') { e.preventDefault(); @@ -145,6 +145,7 @@ export function useTableColumnResize(props: AriaTableColumnResizeProps, st } }, onMoveEnd(e) { + let resizeOnFocus = !!triggerRef?.current; let {pointerType} = e; columnResizeWidthRef.current = 0; if (pointerType === 'mouse' || (pointerType === 'touch' && !resizeOnFocus)) { @@ -167,7 +168,7 @@ export function useTableColumnResize(props: AriaTableColumnResizeProps, st } let value = Math.floor(state.getColumnWidth(item.key)); let modality = useInteractionModality(); - let description = !resizeOnFocus && modality === 'keyboard' && !isResizing.current ? stringFormatter.format('resizerDescription') : undefined; + let description = !triggerRef && modality === 'keyboard' && !isResizing.current ? stringFormatter.format('resizerDescription') : undefined; let descriptionProps = useDescription(description); let ariaProps = { 'aria-label': props.label, @@ -205,6 +206,7 @@ export function useTableColumnResize(props: AriaTableColumnResizeProps, st return; } if (e.pointerType === 'virtual' && state.resizingColumn != null) { + let resizeOnFocus = !!triggerRef?.current; endResize(item); if (resizeOnFocus) { focusSafely(triggerRef.current); @@ -223,6 +225,7 @@ export function useTableColumnResize(props: AriaTableColumnResizeProps, st } }, onPress: (e) => { + let resizeOnFocus = !!triggerRef?.current; if (((e.pointerType === 'touch' && !resizeOnFocus) || e.pointerType === 'mouse') && state.resizingColumn != null) { endResize(item); } @@ -239,6 +242,7 @@ export function useTableColumnResize(props: AriaTableColumnResizeProps, st { id, onFocus: () => { + let resizeOnFocus = !!triggerRef?.current; if (resizeOnFocus) { // useMove calls onMoveStart for every keypress, but we want resize start to only be called when we start resize mode // call instead during focus and blur From e1b1abff5093eb7d2bf1780373532e1cc73c2fa0 Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Tue, 28 Feb 2023 14:07:08 -0800 Subject: [PATCH 19/23] add aria description to input for virtual modality too if the user enters the table using control+option+arrow keys in voiceover, they will be virtual modality so we want the description for press enter to resize to be audible there --- packages/@react-aria/table/src/useTableColumnResize.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@react-aria/table/src/useTableColumnResize.ts b/packages/@react-aria/table/src/useTableColumnResize.ts index d7b4f463b8e..3f51a7107c6 100644 --- a/packages/@react-aria/table/src/useTableColumnResize.ts +++ b/packages/@react-aria/table/src/useTableColumnResize.ts @@ -168,7 +168,7 @@ export function useTableColumnResize(props: AriaTableColumnResizeProps, st } let value = Math.floor(state.getColumnWidth(item.key)); let modality = useInteractionModality(); - let description = !triggerRef && modality === 'keyboard' && !isResizing.current ? stringFormatter.format('resizerDescription') : undefined; + let description = !triggerRef && (modality === 'keyboard' || modality === 'virtual') && !isResizing.current ? stringFormatter.format('resizerDescription') : undefined; let descriptionProps = useDescription(description); let ariaProps = { 'aria-label': props.label, From cf63a95d449ec69e20147cbcbc3ff99add0025c7 Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Wed, 1 Mar 2023 13:03:55 -0800 Subject: [PATCH 20/23] addressing review comments --- packages/@react-aria/grid/src/useGrid.ts | 6 +++--- packages/@react-aria/table/stories/example-docs.tsx | 2 +- packages/@react-aria/table/stories/resizing.css | 11 +++++++++++ 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/packages/@react-aria/grid/src/useGrid.ts b/packages/@react-aria/grid/src/useGrid.ts index a2f477af464..dbf4b928503 100644 --- a/packages/@react-aria/grid/src/useGrid.ts +++ b/packages/@react-aria/grid/src/useGrid.ts @@ -93,7 +93,7 @@ export function useGrid(props: GridProps, state: GridState(props: GridProps, state: GridState(props: GridProps, state: GridState Date: Wed, 1 Mar 2023 14:23:56 -0800 Subject: [PATCH 21/23] prevent extraneous scrolling when keyboard navigating to the resizer margin applied on the visually hidden input makes scrollIntoView think it needs to scroll it --- packages/@react-aria/table/stories/docs-example.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/@react-aria/table/stories/docs-example.css b/packages/@react-aria/table/stories/docs-example.css index 64ff83c9b7d..ba3311fd139 100644 --- a/packages/@react-aria/table/stories/docs-example.css +++ b/packages/@react-aria/table/stories/docs-example.css @@ -74,6 +74,10 @@ border-color: orange; background-color: transparent; } + + input { + margin: 0px; + } } .aria-table-row { From 60a66c5b44cc8503daa99ecf1bbd6864096cfa52 Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Fri, 3 Mar 2023 11:55:30 -0800 Subject: [PATCH 22/23] address review comments --- packages/@react-aria/table/src/useTableColumnResize.ts | 6 +++++- packages/@react-aria/table/stories/docs-example.css | 4 ---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/@react-aria/table/src/useTableColumnResize.ts b/packages/@react-aria/table/src/useTableColumnResize.ts index 3f51a7107c6..711c62bfe0a 100644 --- a/packages/@react-aria/table/src/useTableColumnResize.ts +++ b/packages/@react-aria/table/src/useTableColumnResize.ts @@ -168,7 +168,7 @@ export function useTableColumnResize(props: AriaTableColumnResizeProps, st } let value = Math.floor(state.getColumnWidth(item.key)); let modality = useInteractionModality(); - let description = !triggerRef && (modality === 'keyboard' || modality === 'virtual') && !isResizing.current ? stringFormatter.format('resizerDescription') : undefined; + let description = triggerRef?.current != null && (modality === 'keyboard' || modality === 'virtual') && !isResizing.current ? stringFormatter.format('resizerDescription') : undefined; let descriptionProps = useDescription(description); let ariaProps = { 'aria-label': props.label, @@ -241,6 +241,10 @@ export function useTableColumnResize(props: AriaTableColumnResizeProps, st inputProps: mergeProps( { id, + // Override browser default margin. Without this, scrollIntoViewport will think we need to scroll the input into view + style: { + margin: '0px' + }, onFocus: () => { let resizeOnFocus = !!triggerRef?.current; if (resizeOnFocus) { diff --git a/packages/@react-aria/table/stories/docs-example.css b/packages/@react-aria/table/stories/docs-example.css index ba3311fd139..64ff83c9b7d 100644 --- a/packages/@react-aria/table/stories/docs-example.css +++ b/packages/@react-aria/table/stories/docs-example.css @@ -74,10 +74,6 @@ border-color: orange; background-color: transparent; } - - input { - margin: 0px; - } } .aria-table-row { From b228c3a162d2f40c0b1ae92bdc12b671d4e6321a Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Fri, 3 Mar 2023 12:04:23 -0800 Subject: [PATCH 23/23] fix logic --- packages/@react-aria/table/src/useTableColumnResize.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@react-aria/table/src/useTableColumnResize.ts b/packages/@react-aria/table/src/useTableColumnResize.ts index 711c62bfe0a..81f77f0e18e 100644 --- a/packages/@react-aria/table/src/useTableColumnResize.ts +++ b/packages/@react-aria/table/src/useTableColumnResize.ts @@ -168,7 +168,7 @@ export function useTableColumnResize(props: AriaTableColumnResizeProps, st } let value = Math.floor(state.getColumnWidth(item.key)); let modality = useInteractionModality(); - let description = triggerRef?.current != null && (modality === 'keyboard' || modality === 'virtual') && !isResizing.current ? stringFormatter.format('resizerDescription') : undefined; + let description = triggerRef?.current == null && (modality === 'keyboard' || modality === 'virtual') && !isResizing.current ? stringFormatter.format('resizerDescription') : undefined; let descriptionProps = useDescription(description); let ariaProps = { 'aria-label': props.label,
1 ? 'center' : 'left', - padding: '5px 10px', - outline: 'none', - boxShadow: isFocusVisible ? 'inset 0 0 0 2px orange' : 'none', - cursor: 'default', width: widths.get(column.key), - display: 'block', - flex: '0 0 auto', - boxSizing: 'border-box' }} ref={ref}>
{allowsResizing && - + }
{cell.rendered}