From 7f5bcafc8c9c55ee6dc3c764cbf25d2ecc19dc1f Mon Sep 17 00:00:00 2001 From: Rob Snow Date: Mon, 11 Jul 2022 15:26:48 -0700 Subject: [PATCH 01/19] Table Column Resize via screen readers --- 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 71e5867126f..a46736f54a1 100644 --- a/packages/@react-aria/table/src/useTableColumnResize.ts +++ b/packages/@react-aria/table/src/useTableColumnResize.ts @@ -85,9 +85,9 @@ export function useTableColumnResize(props: ResizerProps, state: TableStat }); let ariaProps = { - role: 'separator', + role: 'slider', 'aria-label': props.label, - 'aria-orientation': 'vertical', + 'aria-orientation': 'horizontal', 'aria-labelledby': item.key, 'aria-valuenow': stateRef.current.getColumnWidth(item.key), 'aria-valuemin': stateRef.current.getColumnMinWidth(item.key), From 7246140deaee4b87b3fb34f31bd13f83e62ccbd7 Mon Sep 17 00:00:00 2001 From: Rob Snow Date: Mon, 11 Jul 2022 15:41:54 -0700 Subject: [PATCH 02/19] Improve aria announcement --- packages/@react-aria/table/intl/ar-AE.json | 3 +- packages/@react-aria/table/intl/en-US.json | 3 +- .../table/src/useTableColumnResize.ts | 8 ++- .../table/test/TableSizing.test.js | 60 +++++++++---------- 4 files changed, 40 insertions(+), 34 deletions(-) diff --git a/packages/@react-aria/table/intl/ar-AE.json b/packages/@react-aria/table/intl/ar-AE.json index b113a16c271..3b5c0f94681 100644 --- a/packages/@react-aria/table/intl/ar-AE.json +++ b/packages/@react-aria/table/intl/ar-AE.json @@ -5,5 +5,6 @@ "descendingSort": "ترتيب حسب العمود {columnName} بترتيب تنازلي", "select": "تحديد", "selectAll": "تحديد الكل", - "sortable": "عمود قابل للترتيب" + "sortable": "عمود قابل للترتيب", + "resizeTextValue": "{value} pixels" } diff --git a/packages/@react-aria/table/intl/en-US.json b/packages/@react-aria/table/intl/en-US.json index 165348b0d0b..ff040bb66ee 100644 --- a/packages/@react-aria/table/intl/en-US.json +++ b/packages/@react-aria/table/intl/en-US.json @@ -5,5 +5,6 @@ "ascending": "ascending", "descending": "descending", "ascendingSort": "sorted by column {columnName} in ascending order", - "descendingSort": "sorted by column {columnName} in descending order" + "descendingSort": "sorted by column {columnName} in descending order", + "columnSize": "{value} pixels" } diff --git a/packages/@react-aria/table/src/useTableColumnResize.ts b/packages/@react-aria/table/src/useTableColumnResize.ts index a46736f54a1..fb2d09599e3 100644 --- a/packages/@react-aria/table/src/useTableColumnResize.ts +++ b/packages/@react-aria/table/src/useTableColumnResize.ts @@ -14,9 +14,11 @@ import {ColumnResizeState, TableState} from '@react-stately/table'; import {focusSafely} from '@react-aria/focus'; import {GridNode} from '@react-types/grid'; import {HTMLAttributes, RefObject, useRef} from 'react'; +// @ts-ignore +import intlMessages from '../intl/*.json'; import {mergeProps} from '@react-aria/utils'; import {useKeyboard, useMove} from '@react-aria/interactions'; -import {useLocale} from '@react-aria/i18n'; +import {useLocale, useMessageFormatter} from '@react-aria/i18n'; interface ResizerAria { resizerProps: HTMLAttributes @@ -34,6 +36,7 @@ export function useTableColumnResize(props: ResizerProps, state: TableStat // 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 = state; + const formatMessage = useMessageFormatter(intlMessages); let {direction} = useLocale(); let {keyboardProps} = useKeyboard({ @@ -89,7 +92,8 @@ export function useTableColumnResize(props: ResizerProps, state: TableStat 'aria-label': props.label, 'aria-orientation': 'horizontal', 'aria-labelledby': item.key, - 'aria-valuenow': stateRef.current.getColumnWidth(item.key), + 'aria-valuenow': Math.floor(stateRef.current.getColumnWidth(item.key)), + 'aria-valuetext': formatMessage('columnSize', {value: Math.floor(stateRef.current.getColumnWidth(item.key))}), 'aria-valuemin': stateRef.current.getColumnMinWidth(item.key), 'aria-valuemax': stateRef.current.getColumnMaxWidth(item.key) }; diff --git a/packages/@react-spectrum/table/test/TableSizing.test.js b/packages/@react-spectrum/table/test/TableSizing.test.js index d8053b33fcc..8e113a2f28e 100644 --- a/packages/@react-spectrum/table/test/TableSizing.test.js +++ b/packages/@react-spectrum/table/test/TableSizing.test.js @@ -648,7 +648,7 @@ describe('TableViewSizing', function () { // trigger pointer modality fireEvent.pointerMove(tree.container); - expect(tree.queryByRole('separator')).toBeNull(); + expect(tree.queryByRole('slider')).toBeNull(); let rows = tree.getAllByRole('row'); @@ -661,8 +661,8 @@ describe('TableViewSizing', function () { let resizableHeader = tree.getAllByRole('columnheader')[0]; fireEvent.pointerEnter(resizableHeader); - expect(tree.getByRole('separator')).toBeVisible(); - let resizer = tree.getByRole('separator'); + expect(tree.getByRole('slider')).toBeVisible(); + let resizer = tree.getByRole('slider'); fireEvent.pointerEnter(resizer); @@ -693,7 +693,7 @@ describe('TableViewSizing', function () { fireEvent.pointerLeave(resizer, {pointerType: 'mouse', pointerId: 1}); fireEvent.pointerLeave(resizableHeader, {pointerType: 'mouse', pointerId: 1}); - expect(tree.queryByRole('separator')).toBeNull(); + expect(tree.queryByRole('slider')).toBeNull(); }); it('dragging the resizer works - mobile', () => { @@ -716,7 +716,7 @@ describe('TableViewSizing', function () { // trigger pointer modality fireEvent.pointerMove(tree.container); - expect(tree.queryByRole('separator')).toBeNull(); + expect(tree.queryByRole('slider')).toBeNull(); let rows = tree.getAllByRole('row'); @@ -729,8 +729,8 @@ describe('TableViewSizing', function () { let resizableHeader = tree.getAllByRole('columnheader')[0]; fireEvent.pointerEnter(resizableHeader); - expect(tree.getByRole('separator')).toBeVisible(); - let resizer = tree.getByRole('separator'); + expect(tree.getByRole('slider')).toBeVisible(); + let resizer = tree.getByRole('slider'); fireEvent.pointerEnter(resizer); @@ -761,7 +761,7 @@ describe('TableViewSizing', function () { fireEvent.pointerLeave(resizer, {pointerType: 'mouse', pointerId: 1}); fireEvent.pointerLeave(resizableHeader, {pointerType: 'mouse', pointerId: 1}); - expect(tree.queryByRole('separator')).toBeNull(); + expect(tree.queryByRole('slider')).toBeNull(); }); }); @@ -791,7 +791,7 @@ describe('TableViewSizing', function () { fireEvent.pointerUp(document.body, {pointerType: 'touch', pointerId: 1}); act(() => {jest.runAllTimers();}); - expect(tree.queryByRole('separator')).toBeNull(); + expect(tree.queryByRole('slider')).toBeNull(); let rows = tree.getAllByRole('row'); @@ -813,8 +813,8 @@ describe('TableViewSizing', function () { fireEvent.pointerUp(resizeMenuItem, {pointerType: 'touch', pointerId: 1}); act(() => {jest.runAllTimers();}); - expect(tree.getByRole('separator')).toBeVisible(); - let resizer = tree.getByRole('separator'); + expect(tree.getByRole('slider')).toBeVisible(); + let resizer = tree.getByRole('slider'); // actual locations do not matter, the delta matters between events for the calculation of useMove fireEvent.pointerDown(resizer, {pointerType: 'touch', pointerId: 1, pageX: 600, pageY: 30}); @@ -844,7 +844,7 @@ describe('TableViewSizing', function () { act(() => resizer.blur()); act(() => {jest.runAllTimers();}); - expect(tree.queryByRole('separator')).toBeNull(); + expect(tree.queryByRole('slider')).toBeNull(); }); it('dragging the resizer works - mobile', () => { @@ -869,7 +869,7 @@ describe('TableViewSizing', function () { fireEvent.pointerUp(document.body, {pointerType: 'touch', pointerId: 1}); act(() => {jest.runAllTimers();}); - expect(tree.queryByRole('separator')).toBeNull(); + expect(tree.queryByRole('slider')).toBeNull(); let rows = tree.getAllByRole('row'); @@ -891,7 +891,7 @@ describe('TableViewSizing', function () { fireEvent.pointerUp(resizeMenuItem, {pointerType: 'touch', pointerId: 1}); act(() => {jest.runAllTimers();}); - let resizer = tree.getByRole('separator'); + let resizer = tree.getByRole('slider'); expect(resizer).toBeVisible(); expect(document.activeElement).toBe(resizer); @@ -923,7 +923,7 @@ describe('TableViewSizing', function () { act(() => resizer.blur()); act(() => {jest.runAllTimers();}); - expect(tree.queryByRole('separator')).toBeNull(); + expect(tree.queryByRole('slider')).toBeNull(); }); }); @@ -953,7 +953,7 @@ describe('TableViewSizing', function () { let resizableHeader = tree.getAllByRole('columnheader')[0]; expect(document.activeElement).toBe(resizableHeader); - expect(tree.queryByRole('separator')).toBeNull(); + expect(tree.queryByRole('slider')).toBeNull(); let rows = tree.getAllByRole('row'); @@ -971,7 +971,7 @@ describe('TableViewSizing', function () { act(() => {jest.runAllTimers();}); act(() => {jest.runAllTimers();}); - let resizer = tree.getByRole('separator'); + let resizer = tree.getByRole('slider'); expect(document.activeElement).toBe(resizer); @@ -1004,7 +1004,7 @@ describe('TableViewSizing', function () { expect(document.activeElement).toBe(resizableHeader); - expect(tree.queryByRole('separator')).toBeNull(); + expect(tree.queryByRole('slider')).toBeNull(); }); it('arrow keys the resizer works - mobile', async () => { let tree = render( @@ -1030,7 +1030,7 @@ describe('TableViewSizing', function () { let resizableHeader = tree.getAllByRole('columnheader')[0]; expect(document.activeElement).toBe(resizableHeader); - expect(tree.queryByRole('separator')).toBeNull(); + expect(tree.queryByRole('slider')).toBeNull(); let rows = tree.getAllByRole('row'); @@ -1048,7 +1048,7 @@ describe('TableViewSizing', function () { act(() => {jest.runAllTimers();}); act(() => {jest.runAllTimers();}); - let resizer = tree.getByRole('separator'); + let resizer = tree.getByRole('slider'); expect(document.activeElement).toBe(resizer); @@ -1081,7 +1081,7 @@ describe('TableViewSizing', function () { expect(document.activeElement).toBe(resizableHeader); - expect(tree.queryByRole('separator')).toBeNull(); + expect(tree.queryByRole('slider')).toBeNull(); }); it('can exit resize via Enter', async () => { jest.spyOn(window.screen, 'width', 'get').mockImplementation(() => 1024); @@ -1108,7 +1108,7 @@ describe('TableViewSizing', function () { let resizableHeader = tree.getAllByRole('columnheader')[0]; expect(document.activeElement).toBe(resizableHeader); - expect(tree.queryByRole('separator')).toBeNull(); + expect(tree.queryByRole('slider')).toBeNull(); fireEvent.keyDown(document.activeElement, {key: 'Enter'}); @@ -1119,7 +1119,7 @@ describe('TableViewSizing', function () { act(() => {jest.runAllTimers();}); act(() => {jest.runAllTimers();}); - let resizer = tree.getByRole('separator'); + let resizer = tree.getByRole('slider'); expect(document.activeElement).toBe(resizer); @@ -1128,7 +1128,7 @@ describe('TableViewSizing', function () { expect(document.activeElement).toBe(resizableHeader); - expect(tree.queryByRole('separator')).toBeNull(); + expect(tree.queryByRole('slider')).toBeNull(); }); it('can exit resize via Tab', async () => { jest.spyOn(window.screen, 'width', 'get').mockImplementation(() => 1024); @@ -1155,7 +1155,7 @@ describe('TableViewSizing', function () { let resizableHeader = tree.getAllByRole('columnheader')[0]; expect(document.activeElement).toBe(resizableHeader); - expect(tree.queryByRole('separator')).toBeNull(); + expect(tree.queryByRole('slider')).toBeNull(); fireEvent.keyDown(document.activeElement, {key: 'Enter'}); @@ -1166,7 +1166,7 @@ describe('TableViewSizing', function () { act(() => {jest.runAllTimers();}); act(() => {jest.runAllTimers();}); - let resizer = tree.getByRole('separator'); + let resizer = tree.getByRole('slider'); expect(document.activeElement).toBe(resizer); @@ -1174,7 +1174,7 @@ describe('TableViewSizing', function () { expect(document.activeElement).toBe(resizableHeader); - expect(tree.queryByRole('separator')).toBeNull(); + expect(tree.queryByRole('slider')).toBeNull(); }); it('can exit resize via shift Tab', async () => { jest.spyOn(window.screen, 'width', 'get').mockImplementation(() => 1024); @@ -1201,7 +1201,7 @@ describe('TableViewSizing', function () { let resizableHeader = tree.getAllByRole('columnheader')[0]; expect(document.activeElement).toBe(resizableHeader); - expect(tree.queryByRole('separator')).toBeNull(); + expect(tree.queryByRole('slider')).toBeNull(); fireEvent.keyDown(document.activeElement, {key: 'Enter'}); @@ -1212,7 +1212,7 @@ describe('TableViewSizing', function () { act(() => {jest.runAllTimers();}); act(() => {jest.runAllTimers();}); - let resizer = tree.getByRole('separator'); + let resizer = tree.getByRole('slider'); expect(document.activeElement).toBe(resizer); @@ -1220,7 +1220,7 @@ describe('TableViewSizing', function () { expect(document.activeElement).toBe(resizableHeader); - expect(tree.queryByRole('separator')).toBeNull(); + expect(tree.queryByRole('slider')).toBeNull(); }); }); }); From 964bb5ff541fb38d33264ac152b4229009cb1375 Mon Sep 17 00:00:00 2001 From: Rob Snow Date: Tue, 12 Jul 2022 18:02:24 -0700 Subject: [PATCH 03/19] separate column header from trigger button --- .../components/table/index.css | 29 +++-- .../components/table/skin.css | 9 +- .../table/src/useTableColumnHeader.ts | 47 +++++++ packages/@react-spectrum/table/package.json | 1 + .../@react-spectrum/table/src/TableView.tsx | 118 +++++++++++++----- .../table/test/TableSizing.test.js | 26 ++-- 6 files changed, 173 insertions(+), 57 deletions(-) diff --git a/packages/@adobe/spectrum-css-temp/components/table/index.css b/packages/@adobe/spectrum-css-temp/components/table/index.css index c2801095dd5..b5ff032c427 100644 --- a/packages/@adobe/spectrum-css-temp/components/table/index.css +++ b/packages/@adobe/spectrum-css-temp/components/table/index.css @@ -86,15 +86,24 @@ svg.spectrum-Table-sortedIcon { transform: rotateZ(180deg); } } + &.is-resizable { + padding: 0; + .spectrum-Table-headCellContents { + flex: 1 1 auto; + min-width: 0; + } + .spectrum-Table-headCellButton { + box-sizing: border-box; + padding: var(--spectrum-table-header-padding-y) var(--spectrum-table-header-padding-x); + } + } } .spectrum-Table-columnResizer { display: flex; + flex: 0 0 auto; justify-content: flex-end; box-sizing: border-box; - position: absolute; - inset-block-start: 0px; - inset-inline-end: 0px; inline-size: 10px; block-size: 100%; user-select: none; @@ -223,7 +232,8 @@ svg.spectrum-Table-sortedIcon { } .spectrum-Table-cell, -.spectrum-Table-headCell { +.spectrum-Table-headCell, +.spectrum-Table-headCellButton { position: relative; &:focus { @@ -251,7 +261,8 @@ svg.spectrum-Table-sortedIcon { } } -.spectrum-Table-headCell { +.spectrum-Table-headCell, +.spectrum-Table-headCellButton { &:focus-ring, &.is-focused { &::before { @@ -267,14 +278,6 @@ svg.spectrum-Table-sortedIcon { border-inline-end-width: var(--spectrum-table-divider-border-size); } -.spectrum-Table-cell--divider { - &.is-resizable { - &:hover { - border-inline-end-width: 3px; - } - } -} - .spectrum-Table-row { position: relative; cursor: default; diff --git a/packages/@adobe/spectrum-css-temp/components/table/skin.css b/packages/@adobe/spectrum-css-temp/components/table/skin.css index 9b26fa9039a..aa7bef77e67 100644 --- a/packages/@adobe/spectrum-css-temp/components/table/skin.css +++ b/packages/@adobe/spectrum-css-temp/components/table/skin.css @@ -49,12 +49,6 @@ governing permissions and limitations under the License. } } } - - &.is-resizable { - &.is-hovered { - color: var(--spectrum-table-header-text-color-hover); - } - } } /* Helper for shared drop target overlay */ @@ -68,7 +62,8 @@ governing permissions and limitations under the License. } .spectrum-Table-cell, -.spectrum-Table-headCell { +.spectrum-Table-headCell, +.spectrum-Table-headCellButton { &:focus-ring, &.is-focused { &::before { diff --git a/packages/@react-aria/table/src/useTableColumnHeader.ts b/packages/@react-aria/table/src/useTableColumnHeader.ts index 5f99c001f30..c04610d329e 100644 --- a/packages/@react-aria/table/src/useTableColumnHeader.ts +++ b/packages/@react-aria/table/src/useTableColumnHeader.ts @@ -91,3 +91,50 @@ export function useTableColumnHeader(props: ColumnHeaderProps, state: TableSt } }; } + +/** + * Provides the behavior and accessibility implementation for a column header in a table. + * @param props - Props for the column header. + * @param state - State of the table, as returned by `useTableState`. + * @param ref - The ref attached to the column header element. + */ +export function useInteractiveTableColumnHeader(props: ColumnHeaderProps, state: TableState, ref: RefObject): ColumnHeaderAria { + let {node} = props; + let allowsSorting = node.props.allowsSorting; + // the selection cell column header needs to focus the checkbox within it but the other columns should focus the cell so that focus doesn't land on the resizer + let {gridCellProps} = useGridCell({...props, focusMode: node.props.isSelectionCell || node.props.allowsResizing || node.props.allowsSorting ? 'child' : 'cell'}, state, ref); + + + // Needed to pick up the focusable context, enabling things like Tooltips for example + let {focusableProps} = useFocusable({}, ref); + + let ariaSort: HTMLAttributes['aria-sort'] = null; + let isSortedColumn = state.sortDescriptor?.column === node.key; + let sortDirection = state.sortDescriptor?.direction; + // aria-sort not supported in Android Talkback + if (node.props.allowsSorting && !isAndroid()) { + ariaSort = isSortedColumn ? sortDirection : 'none'; + } + + let formatMessage = useMessageFormatter(intlMessages); + let sortDescription; + if (allowsSorting) { + sortDescription = `${formatMessage('sortable')}`; + // Android Talkback doesn't support aria-sort so we add sort order details to the aria-described by here + if (isSortedColumn && sortDirection && isAndroid()) { + sortDescription = `${sortDescription}, ${formatMessage(sortDirection)}`; + } + } + + let descriptionProps = useDescription(sortDescription); + + return { + columnHeaderProps: { + ...mergeProps(gridCellProps, focusableProps, descriptionProps), + role: 'columnheader', + id: getColumnHeaderId(state, node.key), + 'aria-colspan': node.colspan && node.colspan > 1 ? node.colspan : null, + 'aria-sort': ariaSort + } + }; +} diff --git a/packages/@react-spectrum/table/package.json b/packages/@react-spectrum/table/package.json index 42e2e5f2900..fecc9c2a6ff 100644 --- a/packages/@react-spectrum/table/package.json +++ b/packages/@react-spectrum/table/package.json @@ -32,6 +32,7 @@ }, "dependencies": { "@babel/runtime": "^7.6.2", + "@react-aria/button": "^3.5.1", "@react-aria/focus": "^3.6.0", "@react-aria/grid": "^3.3.0", "@react-aria/i18n": "^3.4.0", diff --git a/packages/@react-spectrum/table/src/TableView.tsx b/packages/@react-spectrum/table/src/TableView.tsx index db9b5c7be3d..5f5480479c0 100644 --- a/packages/@react-spectrum/table/src/TableView.tsx +++ b/packages/@react-spectrum/table/src/TableView.tsx @@ -13,9 +13,9 @@ import ArrowDownSmall from '@spectrum-icons/ui/ArrowDownSmall'; import {chain, mergeProps, useLayoutEffect} from '@react-aria/utils'; import {Checkbox} from '@react-spectrum/checkbox'; -import {classNames, useDOMRef, useStyleProps} from '@react-spectrum/utils'; +import {classNames, useDOMRef, useFocusableRef, useStyleProps} from '@react-spectrum/utils'; import {ColumnResizeState, TableState, useTableColumnResizeState, useTableState} from '@react-stately/table'; -import {DOMRef} from '@react-types/shared'; +import {DOMRef, FocusableRef} from '@react-types/shared'; import {FocusRing, focusSafely, useFocusRing} from '@react-aria/focus'; import {GridNode} from '@react-types/grid'; // @ts-ignore @@ -31,11 +31,10 @@ import styles from '@adobe/spectrum-css-temp/components/table/vars.css'; import stylesOverrides from './table.css'; import {TableLayout} from '@react-stately/layout'; import {Tooltip, TooltipTrigger} from '@react-spectrum/tooltip'; +import {useButton} from '@react-aria/button'; import {useHover} from '@react-aria/interactions'; -import {useLocale, useMessageFormatter} from '@react-aria/i18n'; -import {usePress} from '@react-aria/interactions'; -import {useProvider, useProviderProps} from '@react-spectrum/provider'; import { + useInteractiveTableColumnHeader, useTable, useTableCell, useTableColumnHeader, @@ -45,6 +44,9 @@ import { useTableSelectAllCheckbox, useTableSelectionCheckbox } from '@react-aria/table'; +import {useLocale, useMessageFormatter} from '@react-aria/i18n'; +import {usePress} from '@react-aria/interactions'; +import {useProvider, useProviderProps} from '@react-spectrum/provider'; import {VisuallyHidden} from '@react-aria/visually-hidden'; const DEFAULT_HEADER_HEIGHT = { @@ -466,13 +468,10 @@ function TableColumnHeader(props) { let columnProps = column.props as SpectrumColumnProps; - if (columnProps.width && columnProps.allowsResizing) { - throw new Error('Controlled state is not yet supported with column resizing. Please use defaultWidth for uncontrolled column resizing or remove the allowsResizing prop.'); - } - let {hoverProps, isHovered} = useHover(props); const allProps = [columnHeaderProps, hoverProps]; + return (
} - {props.children}
); } +let _TableColumnHeaderButton = (props, ref: FocusableRef) => { + let domRef = useFocusableRef(ref); + let {buttonProps} = useButton({...props, elementType: 'div'}, domRef); + return ( +
+ +
{props.children}
+
+
+ ); +}; +let TableColumnHeaderButton = React.forwardRef(_TableColumnHeaderButton); + function ResizableTableColumnHeader(props) { let {column} = props; - let ref = useRef(); + let ref = useRef(null); + let resizingRef = useRef(null); let {state, columnState} = useTableContext(); let formatMessage = useMessageFormatter(intlMessages); - let [isHovered, setIsHovered] = useState(false); + let {columnHeaderProps} = useInteractiveTableColumnHeader({ + node: column, + isVirtualized: true + }, state, ref); + + let {hoverProps, isHovered} = useHover(props); + + const allProps = [columnHeaderProps, hoverProps]; + + let columnProps = column.props as SpectrumColumnProps; + + if (columnProps.width && columnProps.allowsResizing) { + throw new Error('Controlled state is not yet supported with column resizing. Please use defaultWidth for uncontrolled column resizing or remove the allowsResizing prop.'); + } const onMenuSelect = (key) => { switch (key) { @@ -556,28 +581,61 @@ function ResizableTableColumnHeader(props) { useEffect(() => { if (columnState.currentlyResizingColumn === column.key) { - focusSafely(ref.current); + focusSafely(resizingRef.current); } }, [columnState.currentlyResizingColumn, column.key]); return ( - <> - - - - - - {(item) => ( - - {item.label} - - )} - - - + +
1, + 'react-spectrum-Table-cell--alignEnd': columnProps.align === 'end' + } + ) + ) + }> + + + {columnProps.hideHeader ? + {column.rendered} : + column.rendered + } + {columnProps.allowsSorting && + + } + + + {(item) => ( + + {item.label} + + )} + + + +
+
); } diff --git a/packages/@react-spectrum/table/test/TableSizing.test.js b/packages/@react-spectrum/table/test/TableSizing.test.js index 8e113a2f28e..072474c3243 100644 --- a/packages/@react-spectrum/table/test/TableSizing.test.js +++ b/packages/@react-spectrum/table/test/TableSizing.test.js @@ -801,7 +801,8 @@ describe('TableViewSizing', function () { expect(row.childNodes[2].style.width).toBe('200px'); } - let resizableHeader = tree.getAllByRole('columnheader')[0]; + let header = tree.getAllByRole('columnheader')[0]; + let resizableHeader = within(header).getByRole('button'); fireEvent.pointerDown(resizableHeader, {pointerType: 'touch', pointerId: 1}); fireEvent.pointerUp(resizableHeader, {pointerType: 'touch', pointerId: 1}); @@ -879,7 +880,8 @@ describe('TableViewSizing', function () { expect(row.childNodes[2].style.width).toBe('200px'); } - let resizableHeader = tree.getAllByRole('columnheader')[0]; + let header = tree.getAllByRole('columnheader')[0]; + let resizableHeader = within(header).getByRole('button'); fireEvent.pointerDown(resizableHeader, {pointerType: 'touch', pointerId: 1}); fireEvent.pointerUp(resizableHeader, {pointerType: 'touch', pointerId: 1}); @@ -951,7 +953,8 @@ describe('TableViewSizing', function () { fireEvent.keyDown(document.activeElement, {key: 'ArrowUp'}); fireEvent.keyUp(document.activeElement, {key: 'ArrowUp'}); - let resizableHeader = tree.getAllByRole('columnheader')[0]; + let header = tree.getAllByRole('columnheader')[0]; + let resizableHeader = within(header).getByRole('button'); expect(document.activeElement).toBe(resizableHeader); expect(tree.queryByRole('slider')).toBeNull(); @@ -1001,6 +1004,7 @@ describe('TableViewSizing', function () { fireEvent.keyDown(document.activeElement, {key: 'Escape'}); fireEvent.keyUp(document.activeElement, {key: 'Escape'}); + act(() => {jest.runAllTimers();}); expect(document.activeElement).toBe(resizableHeader); @@ -1028,7 +1032,8 @@ describe('TableViewSizing', function () { fireEvent.keyDown(document.activeElement, {key: 'ArrowUp'}); fireEvent.keyUp(document.activeElement, {key: 'ArrowUp'}); - let resizableHeader = tree.getAllByRole('columnheader')[0]; + let header = tree.getAllByRole('columnheader')[0]; + let resizableHeader = within(header).getByRole('button'); expect(document.activeElement).toBe(resizableHeader); expect(tree.queryByRole('slider')).toBeNull(); @@ -1078,6 +1083,7 @@ describe('TableViewSizing', function () { fireEvent.keyDown(document.activeElement, {key: 'Escape'}); fireEvent.keyUp(document.activeElement, {key: 'Escape'}); + act(() => {jest.runAllTimers();}); expect(document.activeElement).toBe(resizableHeader); @@ -1106,7 +1112,8 @@ describe('TableViewSizing', function () { fireEvent.keyDown(document.activeElement, {key: 'ArrowUp'}); fireEvent.keyUp(document.activeElement, {key: 'ArrowUp'}); - let resizableHeader = tree.getAllByRole('columnheader')[0]; + let header = tree.getAllByRole('columnheader')[0]; + let resizableHeader = within(header).getByRole('button'); expect(document.activeElement).toBe(resizableHeader); expect(tree.queryByRole('slider')).toBeNull(); @@ -1125,6 +1132,7 @@ describe('TableViewSizing', function () { fireEvent.keyDown(document.activeElement, {key: 'Enter'}); fireEvent.keyUp(document.activeElement, {key: 'Enter'}); + act(() => {jest.runAllTimers();}); expect(document.activeElement).toBe(resizableHeader); @@ -1153,7 +1161,8 @@ describe('TableViewSizing', function () { fireEvent.keyDown(document.activeElement, {key: 'ArrowUp'}); fireEvent.keyUp(document.activeElement, {key: 'ArrowUp'}); - let resizableHeader = tree.getAllByRole('columnheader')[0]; + let header = tree.getAllByRole('columnheader')[0]; + let resizableHeader = within(header).getByRole('button'); expect(document.activeElement).toBe(resizableHeader); expect(tree.queryByRole('slider')).toBeNull(); @@ -1171,6 +1180,7 @@ describe('TableViewSizing', function () { expect(document.activeElement).toBe(resizer); userEvent.tab(); + act(() => {jest.runAllTimers();}); expect(document.activeElement).toBe(resizableHeader); @@ -1199,7 +1209,8 @@ describe('TableViewSizing', function () { fireEvent.keyDown(document.activeElement, {key: 'ArrowUp'}); fireEvent.keyUp(document.activeElement, {key: 'ArrowUp'}); - let resizableHeader = tree.getAllByRole('columnheader')[0]; + let header = tree.getAllByRole('columnheader')[0]; + let resizableHeader = within(header).getByRole('button'); expect(document.activeElement).toBe(resizableHeader); expect(tree.queryByRole('slider')).toBeNull(); @@ -1217,6 +1228,7 @@ describe('TableViewSizing', function () { expect(document.activeElement).toBe(resizer); userEvent.tab({shift: true}); + act(() => {jest.runAllTimers();}); expect(document.activeElement).toBe(resizableHeader); From 01b6f9ee30afb671d9e3cd20d7504aab10ecea04 Mon Sep 17 00:00:00 2001 From: Rob Snow Date: Wed, 13 Jul 2022 18:00:25 -0700 Subject: [PATCH 04/19] Get VO working again --- .../components/table/skin.css | 2 +- .../table/src/useTableColumnResize.ts | 18 +++++------ .../@react-spectrum/table/src/Resizer.tsx | 16 ++++++---- .../@react-spectrum/table/src/TableView.tsx | 30 +++++++++++-------- .../table/stories/Table.stories.tsx | 19 ++++++++++++ .../table/test/TableSizing.test.js | 20 +++++-------- 6 files changed, 63 insertions(+), 42 deletions(-) diff --git a/packages/@adobe/spectrum-css-temp/components/table/skin.css b/packages/@adobe/spectrum-css-temp/components/table/skin.css index aa7bef77e67..1c20d9b89b7 100644 --- a/packages/@adobe/spectrum-css-temp/components/table/skin.css +++ b/packages/@adobe/spectrum-css-temp/components/table/skin.css @@ -285,7 +285,7 @@ tbody.spectrum-Table-body { } &:active, - &:focus { + &:focus-ring { &::after { background-color: var(--spectrum-global-color-blue-400); } diff --git a/packages/@react-aria/table/src/useTableColumnResize.ts b/packages/@react-aria/table/src/useTableColumnResize.ts index fb2d09599e3..4ae34620735 100644 --- a/packages/@react-aria/table/src/useTableColumnResize.ts +++ b/packages/@react-aria/table/src/useTableColumnResize.ts @@ -27,11 +27,13 @@ interface ResizerAria { interface ResizerProps { column: GridNode, showResizer: boolean, - label: string + label: string, + triggerRef: RefObject } +// eslint-disable-next-line @typescript-eslint/no-unused-vars export function useTableColumnResize(props: ResizerProps, state: TableState & ColumnResizeState, ref: RefObject): ResizerAria { - let {column: item, showResizer} = props; + let {column: item, showResizer, triggerRef} = 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); @@ -44,17 +46,14 @@ export function useTableColumnResize(props: ResizerProps, state: TableStat 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(ref.current.closest('[role="columnheader"]')); + focusSafely(triggerRef.current); } } }); const columnResizeWidthRef = useRef(null); const {moveProps} = useMove({ - onMoveStart({pointerType}) { - if (pointerType !== 'keyboard') { - stateRef.current.onColumnResizeStart(item); - } + onMoveStart() { columnResizeWidthRef.current = stateRef.current.getColumnWidth(item.key); cursor.current = document.body.style.cursor; }, @@ -78,10 +77,7 @@ export function useTableColumnResize(props: ResizerProps, state: TableStat } } }, - onMoveEnd({pointerType}) { - if (pointerType !== 'keyboard') { - stateRef.current.onColumnResizeEnd(item); - } + onMoveEnd() { columnResizeWidthRef.current = 0; document.body.style.cursor = cursor.current; } diff --git a/packages/@react-spectrum/table/src/Resizer.tsx b/packages/@react-spectrum/table/src/Resizer.tsx index ffb12d4eb1e..1bc94ffec19 100644 --- a/packages/@react-spectrum/table/src/Resizer.tsx +++ b/packages/@react-spectrum/table/src/Resizer.tsx @@ -1,5 +1,6 @@ /* eslint-disable jsx-a11y/role-supports-aria-props */ import {classNames} from '@react-spectrum/utils'; +import {FocusRing} from '@react-aria/focus'; import {GridNode} from '@react-types/grid'; // @ts-ignore import intlMessages from '../intl/*.json'; @@ -11,7 +12,8 @@ import {useTableContext} from './TableView'; interface ResizerProps { column: GridNode, - showResizer: boolean + showResizer: boolean, + triggerRef: RefObject } function Resizer(props: ResizerProps, ref: RefObject) { @@ -36,11 +38,13 @@ function Resizer(props: ResizerProps, ref: RefObject) { style.cursor = 'col-resize'; } return ( -
+ +
+ ); } diff --git a/packages/@react-spectrum/table/src/TableView.tsx b/packages/@react-spectrum/table/src/TableView.tsx index 5f5480479c0..34a15eaef3e 100644 --- a/packages/@react-spectrum/table/src/TableView.tsx +++ b/packages/@react-spectrum/table/src/TableView.tsx @@ -16,7 +16,7 @@ import {Checkbox} from '@react-spectrum/checkbox'; import {classNames, useDOMRef, useFocusableRef, useStyleProps} from '@react-spectrum/utils'; import {ColumnResizeState, TableState, useTableColumnResizeState, useTableState} from '@react-stately/table'; import {DOMRef, FocusableRef} from '@react-types/shared'; -import {FocusRing, focusSafely, useFocusRing} from '@react-aria/focus'; +import {FocusRing, useFocusRing} from '@react-aria/focus'; import {GridNode} from '@react-types/grid'; // @ts-ignore import intlMessages from '../intl/*.json'; @@ -82,7 +82,8 @@ const SELECTION_CELL_DEFAULT_WIDTH = { interface TableContextValue { state: TableState, layout: TableLayout, - columnState: ColumnResizeState + columnState: ColumnResizeState, + headerRowHovered: boolean } const TableContext = React.createContext>(null); @@ -153,6 +154,7 @@ function TableView(props: SpectrumTableProps, ref: DOMRef, unknown>; @@ -195,6 +197,7 @@ function TableView(props: SpectrumTableProps, ref: DOMRef @@ -305,7 +308,7 @@ function TableView(props: SpectrumTableProps, ref: DOMRef + { if (columnState.currentlyResizingColumn === column.key) { - focusSafely(resizingRef.current); + // focusSafely won't actually focus because the focus moves from the menuitem to the body during the after transition wait + resizingRef.current.focus(); } }, [columnState.currentlyResizingColumn, column.key]); @@ -613,7 +617,7 @@ function ResizableTableColumnHeader(props) { ) }> - + {columnProps.hideHeader ? {column.rendered} : column.rendered @@ -633,7 +637,8 @@ function ResizableTableColumnHeader(props) { + showResizer={showResizer} + triggerRef={triggerRef} />
); @@ -765,13 +770,14 @@ function TableRow({item, children, hasActions, ...otherProps}) { ); } -function TableHeaderRow({item, children, style}) { +function TableHeaderRow({item, children, style, ...props}) { let {state} = useTableContext(); let ref = useRef(); let {rowProps} = useTableHeaderRow({node: item, isVirtualized: true}, state, ref); + let {hoverProps} = useHover(props); return ( -
+
{children}
); diff --git a/packages/@react-spectrum/table/stories/Table.stories.tsx b/packages/@react-spectrum/table/stories/Table.stories.tsx index 7ce6b67fe37..ce7624316fb 100644 --- a/packages/@react-spectrum/table/stories/Table.stories.tsx +++ b/packages/@react-spectrum/table/stories/Table.stories.tsx @@ -1361,6 +1361,25 @@ storiesOf('TableView', module) () => ( ) + ) + .add( + 'what', + () => ( + + + Foo + Bar + Baz + + + {item => + ( + {key => {item[key]}} + ) + } + + + ) ); function AsyncLoadingExample(props) { diff --git a/packages/@react-spectrum/table/test/TableSizing.test.js b/packages/@react-spectrum/table/test/TableSizing.test.js index 072474c3243..88ea7e94e11 100644 --- a/packages/@react-spectrum/table/test/TableSizing.test.js +++ b/packages/@react-spectrum/table/test/TableSizing.test.js @@ -10,15 +10,17 @@ * governing permissions and limitations under the License. */ + jest.mock('@react-aria/live-announcer'); import {act, render as renderComponent, within} from '@testing-library/react'; import {ActionButton} from '@react-spectrum/button'; import Add from '@spectrum-icons/workflow/Add'; import {Cell, Column, Row, TableBody, TableHeader, TableView} from '../'; -import {fireEvent, installPointerEvent} from '@react-spectrum/test-utils'; +import {fireEvent, installPointerEvent, triggerTouch} from '@react-spectrum/test-utils'; import {HidingColumns} from '../stories/HidingColumns'; import {Provider} from '@react-spectrum/provider'; import React from 'react'; +import {setInteractionModality} from '@react-aria/interactions'; import {theme} from '@react-spectrum/theme-default'; import userEvent from '@testing-library/user-event'; @@ -769,6 +771,7 @@ describe('TableViewSizing', function () { installPointerEvent(); it('dragging the resizer works - desktop', () => { + setInteractionModality('pointer'); jest.spyOn(window.screen, 'width', 'get').mockImplementation(() => 1024); let tree = render( @@ -787,8 +790,7 @@ describe('TableViewSizing', function () { ); - fireEvent.pointerDown(document.body, {pointerType: 'touch', pointerId: 1}); - fireEvent.pointerUp(document.body, {pointerType: 'touch', pointerId: 1}); + triggerTouch(document.body); act(() => {jest.runAllTimers();}); expect(tree.queryByRole('slider')).toBeNull(); @@ -804,14 +806,12 @@ describe('TableViewSizing', function () { let header = tree.getAllByRole('columnheader')[0]; let resizableHeader = within(header).getByRole('button'); - fireEvent.pointerDown(resizableHeader, {pointerType: 'touch', pointerId: 1}); - fireEvent.pointerUp(resizableHeader, {pointerType: 'touch', pointerId: 1}); + triggerTouch(resizableHeader); act(() => {jest.runAllTimers();}); let resizeMenuItem = tree.getAllByRole('menuitem')[0]; - fireEvent.pointerDown(resizeMenuItem, {pointerType: 'touch', pointerId: 1}); - fireEvent.pointerUp(resizeMenuItem, {pointerType: 'touch', pointerId: 1}); + triggerTouch(resizeMenuItem); act(() => {jest.runAllTimers();}); expect(tree.getByRole('slider')).toBeVisible(); @@ -849,6 +849,7 @@ describe('TableViewSizing', function () { }); it('dragging the resizer works - mobile', () => { + setInteractionModality('pointer'); let tree = render( @@ -1004,7 +1005,6 @@ describe('TableViewSizing', function () { fireEvent.keyDown(document.activeElement, {key: 'Escape'}); fireEvent.keyUp(document.activeElement, {key: 'Escape'}); - act(() => {jest.runAllTimers();}); expect(document.activeElement).toBe(resizableHeader); @@ -1083,7 +1083,6 @@ describe('TableViewSizing', function () { fireEvent.keyDown(document.activeElement, {key: 'Escape'}); fireEvent.keyUp(document.activeElement, {key: 'Escape'}); - act(() => {jest.runAllTimers();}); expect(document.activeElement).toBe(resizableHeader); @@ -1132,7 +1131,6 @@ describe('TableViewSizing', function () { fireEvent.keyDown(document.activeElement, {key: 'Enter'}); fireEvent.keyUp(document.activeElement, {key: 'Enter'}); - act(() => {jest.runAllTimers();}); expect(document.activeElement).toBe(resizableHeader); @@ -1180,7 +1178,6 @@ describe('TableViewSizing', function () { expect(document.activeElement).toBe(resizer); userEvent.tab(); - act(() => {jest.runAllTimers();}); expect(document.activeElement).toBe(resizableHeader); @@ -1228,7 +1225,6 @@ describe('TableViewSizing', function () { expect(document.activeElement).toBe(resizer); userEvent.tab({shift: true}); - act(() => {jest.runAllTimers();}); expect(document.activeElement).toBe(resizableHeader); From 5f4a54dccb734c7c3670d4548064daee04d67d7c Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Fri, 15 Jul 2022 11:47:10 -0700 Subject: [PATCH 05/19] fixing Android Talkback table column resizing changed resizer to input w/ onChange since Android Talkback doesnt fire keyboad events when swiping up/down. aria-valuemax Infinitiy breaks android talkback slider interactions --- .../table/src/useTableColumnResize.ts | 25 ++++++++++++++++--- .../@react-spectrum/table/src/Resizer.tsx | 6 ++++- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/packages/@react-aria/table/src/useTableColumnResize.ts b/packages/@react-aria/table/src/useTableColumnResize.ts index 4ae34620735..cef464033e4 100644 --- a/packages/@react-aria/table/src/useTableColumnResize.ts +++ b/packages/@react-aria/table/src/useTableColumnResize.ts @@ -10,10 +10,10 @@ * governing permissions and limitations under the License. */ +import {ChangeEvent, HTMLAttributes, RefObject, useRef} from 'react'; import {ColumnResizeState, TableState} from '@react-stately/table'; import {focusSafely} from '@react-aria/focus'; import {GridNode} from '@react-types/grid'; -import {HTMLAttributes, RefObject, useRef} from 'react'; // @ts-ignore import intlMessages from '../intl/*.json'; import {mergeProps} from '@react-aria/utils'; @@ -84,14 +84,30 @@ export function useTableColumnResize(props: ResizerProps, state: TableStat }); let ariaProps = { - role: 'slider', 'aria-label': props.label, 'aria-orientation': 'horizontal', 'aria-labelledby': item.key, 'aria-valuenow': Math.floor(stateRef.current.getColumnWidth(item.key)), 'aria-valuetext': formatMessage('columnSize', {value: Math.floor(stateRef.current.getColumnWidth(item.key))}), 'aria-valuemin': stateRef.current.getColumnMinWidth(item.key), - 'aria-valuemax': stateRef.current.getColumnMaxWidth(item.key) + 'aria-valuemax': 10000, + min: stateRef.current.getColumnMinWidth(item.key), + max: 10000, + value: stateRef.current.getColumnWidth(item.key) + }; + + + let onChange = (e: ChangeEvent) => { + let currentWidth = stateRef.current.getColumnWidth(item.key); + let nextValue = parseFloat(e.target.value); + + if (nextValue > currentWidth) { + nextValue = currentWidth + 10; + } else { + nextValue = currentWidth - 10; + + } + stateRef.current.onColumnResize(item, nextValue); }; return { @@ -112,7 +128,8 @@ export function useTableColumnResize(props: ResizerProps, state: TableStat tabIndex: showResizer ? 0 : undefined }, keyboardProps, - ariaProps + ariaProps, + {onChange} ) } }; diff --git a/packages/@react-spectrum/table/src/Resizer.tsx b/packages/@react-spectrum/table/src/Resizer.tsx index 1bc94ffec19..fbb206618bb 100644 --- a/packages/@react-spectrum/table/src/Resizer.tsx +++ b/packages/@react-spectrum/table/src/Resizer.tsx @@ -37,11 +37,15 @@ function Resizer(props: ResizerProps, ref: RefObject) { } else { style.cursor = 'col-resize'; } + return ( -
From 75fe70208ee67bfc40f6f11765c9fbce4339c4b4 Mon Sep 17 00:00:00 2001 From: Rob Snow Date: Fri, 15 Jul 2022 13:48:49 -0700 Subject: [PATCH 06/19] mimic slider thumb --- .../components/table/index.css | 2 +- .../table/src/useTableColumnHeader.ts | 61 ++-------- .../table/src/useTableColumnResize.ts | 104 ++++++++++++------ .../@react-spectrum/table/src/Resizer.tsx | 25 +++-- .../@react-spectrum/table/src/TableView.tsx | 9 +- 5 files changed, 101 insertions(+), 100 deletions(-) diff --git a/packages/@adobe/spectrum-css-temp/components/table/index.css b/packages/@adobe/spectrum-css-temp/components/table/index.css index b5ff032c427..635cd6796a7 100644 --- a/packages/@adobe/spectrum-css-temp/components/table/index.css +++ b/packages/@adobe/spectrum-css-temp/components/table/index.css @@ -117,7 +117,7 @@ svg.spectrum-Table-sortedIcon { } &:active, - &:focus { + &.focus-ring { outline: none; &::after { inline-size: 2px; diff --git a/packages/@react-aria/table/src/useTableColumnHeader.ts b/packages/@react-aria/table/src/useTableColumnHeader.ts index c04610d329e..57faa92b952 100644 --- a/packages/@react-aria/table/src/useTableColumnHeader.ts +++ b/packages/@react-aria/table/src/useTableColumnHeader.ts @@ -26,7 +26,9 @@ interface ColumnHeaderProps { /** An object representing the [column header](https://www.w3.org/TR/wai-aria-1.1/#columnheader). Contains all the relevant information that makes up the column header. */ node: GridNode, /** Whether the [column header](https://www.w3.org/TR/wai-aria-1.1/#columnheader) is contained in a virtual scroller. */ - isVirtualized?: boolean + isVirtualized?: boolean, + /** Whether the column has a menu in the header, this changes interactions with the header. */ + hasMenu?: boolean } interface ColumnHeaderAria { @@ -42,18 +44,16 @@ interface ColumnHeaderAria { */ export function useTableColumnHeader(props: ColumnHeaderProps, state: TableState, ref: RefObject): ColumnHeaderAria { let {node} = props; - let allowsResizing = node.props.allowsResizing; let allowsSorting = node.props.allowsSorting; // the selection cell column header needs to focus the checkbox within it but the other columns should focus the cell so that focus doesn't land on the resizer - let {gridCellProps} = useGridCell({...props, focusMode: node.props.isSelectionCell || node.props.allowsResizing || node.props.allowsSorting ? 'child' : 'cell'}, state, ref); + let {gridCellProps} = useGridCell({...props, focusMode: node.props.isSelectionCell || props.hasMenu || node.props.allowsSorting ? 'child' : 'cell'}, state, ref); let isSelectionCellDisabled = node.props.isSelectionCell && state.selectionManager.selectionMode === 'single'; let {pressProps} = usePress({ - // Disabled for allowsResizing because if resizing is allowed, a menu trigger is added to the column header. - isDisabled: (!(allowsSorting || allowsResizing)) || isSelectionCellDisabled, + isDisabled: !allowsSorting || isSelectionCellDisabled, onPress() { - !allowsResizing && state.sort(node.key); + state.sort(node.key); }, ref }); @@ -61,53 +61,10 @@ export function useTableColumnHeader(props: ColumnHeaderProps, state: TableSt // Needed to pick up the focusable context, enabling things like Tooltips for example let {focusableProps} = useFocusable({}, ref); - let ariaSort: HTMLAttributes['aria-sort'] = null; - let isSortedColumn = state.sortDescriptor?.column === node.key; - let sortDirection = state.sortDescriptor?.direction; - // aria-sort not supported in Android Talkback - if (node.props.allowsSorting && !isAndroid()) { - ariaSort = isSortedColumn ? sortDirection : 'none'; - } - - let formatMessage = useMessageFormatter(intlMessages); - let sortDescription; - if (allowsSorting) { - sortDescription = `${formatMessage('sortable')}`; - // Android Talkback doesn't support aria-sort so we add sort order details to the aria-described by here - if (isSortedColumn && sortDirection && isAndroid()) { - sortDescription = `${sortDescription}, ${formatMessage(sortDirection)}`; - } + if (props.hasMenu) { + pressProps = {}; } - let descriptionProps = useDescription(sortDescription); - - return { - columnHeaderProps: { - ...mergeProps(gridCellProps, pressProps, focusableProps, descriptionProps), - role: 'columnheader', - id: getColumnHeaderId(state, node.key), - 'aria-colspan': node.colspan && node.colspan > 1 ? node.colspan : null, - 'aria-sort': ariaSort - } - }; -} - -/** - * Provides the behavior and accessibility implementation for a column header in a table. - * @param props - Props for the column header. - * @param state - State of the table, as returned by `useTableState`. - * @param ref - The ref attached to the column header element. - */ -export function useInteractiveTableColumnHeader(props: ColumnHeaderProps, state: TableState, ref: RefObject): ColumnHeaderAria { - let {node} = props; - let allowsSorting = node.props.allowsSorting; - // the selection cell column header needs to focus the checkbox within it but the other columns should focus the cell so that focus doesn't land on the resizer - let {gridCellProps} = useGridCell({...props, focusMode: node.props.isSelectionCell || node.props.allowsResizing || node.props.allowsSorting ? 'child' : 'cell'}, state, ref); - - - // Needed to pick up the focusable context, enabling things like Tooltips for example - let {focusableProps} = useFocusable({}, ref); - let ariaSort: HTMLAttributes['aria-sort'] = null; let isSortedColumn = state.sortDescriptor?.column === node.key; let sortDirection = state.sortDescriptor?.direction; @@ -130,7 +87,7 @@ export function useInteractiveTableColumnHeader(props: ColumnHeaderProps, sta return { columnHeaderProps: { - ...mergeProps(gridCellProps, focusableProps, descriptionProps), + ...mergeProps(gridCellProps, pressProps, focusableProps, descriptionProps), role: 'columnheader', id: getColumnHeaderId(state, node.key), 'aria-colspan': node.colspan && node.colspan > 1 ? node.colspan : null, diff --git a/packages/@react-aria/table/src/useTableColumnResize.ts b/packages/@react-aria/table/src/useTableColumnResize.ts index cef464033e4..7f3aa0af9ab 100644 --- a/packages/@react-aria/table/src/useTableColumnResize.ts +++ b/packages/@react-aria/table/src/useTableColumnResize.ts @@ -10,18 +10,19 @@ * governing permissions and limitations under the License. */ -import {ChangeEvent, HTMLAttributes, RefObject, useRef} from 'react'; import {ColumnResizeState, TableState} from '@react-stately/table'; import {focusSafely} from '@react-aria/focus'; +import {focusWithoutScrolling, mergeProps, useGlobalListeners} from '@react-aria/utils'; import {GridNode} from '@react-types/grid'; // @ts-ignore import intlMessages from '../intl/*.json'; -import {mergeProps} from '@react-aria/utils'; +import React, {ChangeEvent, HTMLAttributes, RefObject, useCallback, useRef} from 'react'; import {useKeyboard, useMove} from '@react-aria/interactions'; import {useLocale, useMessageFormatter} from '@react-aria/i18n'; interface ResizerAria { - resizerProps: HTMLAttributes + inputProps: HTMLAttributes, + resizerProps: HTMLAttributes } interface ResizerProps { @@ -32,13 +33,14 @@ interface ResizerProps { } // eslint-disable-next-line @typescript-eslint/no-unused-vars -export function useTableColumnResize(props: ResizerProps, state: TableState & ColumnResizeState, ref: RefObject): ResizerAria { +export function useTableColumnResize(props: ResizerProps, state: TableState & ColumnResizeState, ref: RefObject): ResizerAria { let {column: item, showResizer, triggerRef} = 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 = state; const formatMessage = useMessageFormatter(intlMessages); + let {addGlobalListener, removeGlobalListener} = useGlobalListeners(); let {direction} = useLocale(); let {keyboardProps} = useKeyboard({ @@ -82,20 +84,28 @@ export function useTableColumnResize(props: ResizerProps, state: TableStat document.body.style.cursor = cursor.current; } }); + let min = Math.floor(stateRef.current.getColumnMinWidth(item.key)); + let max = Math.floor(stateRef.current.getColumnMaxWidth(item.key)); + if (max === Infinity) { + max = Number.MAX_SAFE_INTEGER; + } + let value = Math.floor(stateRef.current.getColumnWidth(item.key)); let ariaProps = { 'aria-label': props.label, - 'aria-orientation': 'horizontal', - 'aria-labelledby': item.key, - 'aria-valuenow': Math.floor(stateRef.current.getColumnWidth(item.key)), - 'aria-valuetext': formatMessage('columnSize', {value: Math.floor(stateRef.current.getColumnWidth(item.key))}), - 'aria-valuemin': stateRef.current.getColumnMinWidth(item.key), - 'aria-valuemax': 10000, - min: stateRef.current.getColumnMinWidth(item.key), - max: 10000, - value: stateRef.current.getColumnWidth(item.key) + 'aria-orientation': 'horizontal' as 'horizontal', + 'aria-labelledby': 'none', + 'aria-valuetext': formatMessage('columnSize', {value}), + min, + max, + value }; + const focusInput = useCallback(() => { + if (ref.current) { + focusWithoutScrolling(ref.current); + } + }, [ref]); let onChange = (e: ChangeEvent) => { let currentWidth = stateRef.current.getColumnWidth(item.key); @@ -110,27 +120,55 @@ export function useTableColumnResize(props: ResizerProps, state: TableStat stateRef.current.onColumnResize(item, nextValue); }; + let onDown = (id?: number) => { + focusInput(); + addGlobalListener(window, 'mouseup', onUp, false); + addGlobalListener(window, 'touchend', onUp, false); + addGlobalListener(window, 'pointerup', onUp, false); + }; + + let onUp = (e) => { + focusInput(); + removeGlobalListener(window, 'mouseup', onUp, false); + removeGlobalListener(window, 'touchend', onUp, false); + removeGlobalListener(window, 'pointerup', onUp, false); + }; + return { - resizerProps: { - ...mergeProps( - moveProps, - { - 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 - stateRef.current.onColumnResizeStart(item); - state.setKeyboardNavigationDisabled(true); - }, - onBlur: () => { - stateRef.current.onColumnResizeEnd(item); - state.setKeyboardNavigationDisabled(false); - }, - tabIndex: showResizer ? 0 : undefined + resizerProps: mergeProps( + keyboardProps, + moveProps, + { + onMouseDown: (e: React.MouseEvent) => { + if (e.button !== 0 || e.altKey || e.ctrlKey || e.metaKey) { + return; + } + onDown(); }, - keyboardProps, - ariaProps, - {onChange} - ) - } + onPointerDown: (e: React.PointerEvent) => { + if (e.button !== 0 || e.altKey || e.ctrlKey || e.metaKey) { + return; + } + onDown(e.pointerId); + }, + onTouchStart: (e: React.TouchEvent) => {onDown(e.changedTouches[0].identifier);} + } + ), + inputProps: mergeProps( + { + 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 + stateRef.current.onColumnResizeStart(item); + state.setKeyboardNavigationDisabled(true); + }, + onBlur: () => { + stateRef.current.onColumnResizeEnd(item); + state.setKeyboardNavigationDisabled(false); + }, + onChange + }, + ariaProps + ) }; } diff --git a/packages/@react-spectrum/table/src/Resizer.tsx b/packages/@react-spectrum/table/src/Resizer.tsx index fbb206618bb..54591fbbd95 100644 --- a/packages/@react-spectrum/table/src/Resizer.tsx +++ b/packages/@react-spectrum/table/src/Resizer.tsx @@ -9,6 +9,7 @@ import styles from '@adobe/spectrum-css-temp/components/table/vars.css'; import {useLocale, useMessageFormatter} from '@react-aria/i18n'; import {useTableColumnResize} from '@react-aria/table'; import {useTableContext} from './TableView'; +import {VisuallyHidden} from '@react-aria/visually-hidden'; interface ResizerProps { column: GridNode, @@ -16,13 +17,13 @@ interface ResizerProps { triggerRef: RefObject } -function Resizer(props: ResizerProps, ref: RefObject) { +function Resizer(props: ResizerProps, ref: RefObject) { let {column, showResizer} = props; let {state, columnState} = useTableContext(); let formatMessage = useMessageFormatter(intlMessages); let {direction} = useLocale(); - let {resizerProps} = useTableColumnResize({...props, label: formatMessage('columnResizer')}, {...state, ...columnState}, ref); + let {inputProps, resizerProps} = useTableColumnResize({...props, label: formatMessage('columnResizer')}, {...state, ...columnState}, ref); let style = { cursor: undefined, @@ -39,15 +40,19 @@ function Resizer(props: ResizerProps, ref: RefObject) { } return ( - - +
+ className={classNames(styles, 'spectrum-Table-columnResizer')} + {...resizerProps}> + + + +
); } diff --git a/packages/@react-spectrum/table/src/TableView.tsx b/packages/@react-spectrum/table/src/TableView.tsx index 34a15eaef3e..744cfcd23d0 100644 --- a/packages/@react-spectrum/table/src/TableView.tsx +++ b/packages/@react-spectrum/table/src/TableView.tsx @@ -534,9 +534,10 @@ function ResizableTableColumnHeader(props) { let resizingRef = useRef(null); let {state, columnState, headerRowHovered} = useTableContext(); let formatMessage = useMessageFormatter(intlMessages); - let {columnHeaderProps} = useInteractiveTableColumnHeader({ + let {columnHeaderProps} = useTableColumnHeader({ node: column, - isVirtualized: true + isVirtualized: true, + hasMenu: true }, state, ref); let {hoverProps, isHovered} = useHover(props); @@ -580,8 +581,6 @@ function ResizableTableColumnHeader(props) { return options; }, [allowsSorting]); - let showResizer = headerRowHovered || columnState.currentlyResizingColumn === column.key; - useEffect(() => { if (columnState.currentlyResizingColumn === column.key) { // focusSafely won't actually focus because the focus moves from the menuitem to the body during the after transition wait @@ -589,6 +588,8 @@ function ResizableTableColumnHeader(props) { } }, [columnState.currentlyResizingColumn, column.key]); + let showResizer = headerRowHovered || columnState.currentlyResizingColumn != null; + return (
Date: Fri, 15 Jul 2022 14:09:26 -0700 Subject: [PATCH 07/19] fix tests and pointer interaction --- .../table/src/useTableColumnResize.ts | 31 +++++++++++++------ .../@react-spectrum/table/src/TableView.tsx | 7 ++--- .../table/test/TableSizing.test.js | 2 ++ 3 files changed, 26 insertions(+), 14 deletions(-) diff --git a/packages/@react-aria/table/src/useTableColumnResize.ts b/packages/@react-aria/table/src/useTableColumnResize.ts index 7f3aa0af9ab..930c3117b46 100644 --- a/packages/@react-aria/table/src/useTableColumnResize.ts +++ b/packages/@react-aria/table/src/useTableColumnResize.ts @@ -27,14 +27,13 @@ interface ResizerAria { interface ResizerProps { column: GridNode, - showResizer: boolean, label: string, triggerRef: RefObject } // eslint-disable-next-line @typescript-eslint/no-unused-vars export function useTableColumnResize(props: ResizerProps, state: TableState & ColumnResizeState, ref: RefObject): ResizerAria { - let {column: item, showResizer, triggerRef} = props; + let {column: item, triggerRef} = 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); @@ -120,18 +119,30 @@ export function useTableColumnResize(props: ResizerProps, state: TableStat stateRef.current.onColumnResize(item, nextValue); }; - let onDown = (id?: number) => { + let onDown = () => { focusInput(); - addGlobalListener(window, 'mouseup', onUp, false); + addGlobalListener(window, 'mouseup', onPointerUp, false); addGlobalListener(window, 'touchend', onUp, false); - addGlobalListener(window, 'pointerup', onUp, false); + addGlobalListener(window, 'pointerup', onPointerUp, false); }; - let onUp = (e) => { + let onPointerUp = (e) => { + // don't hide the resizer for touch since it's harder to bring back + if (e.pointerType === 'touch') { + focusInput(); + } else { + focusSafely(triggerRef.current); + } + removeGlobalListener(window, 'mouseup', onPointerUp, false); + removeGlobalListener(window, 'touchend', onUp, false); + removeGlobalListener(window, 'pointerup', onPointerUp, false); + }; + + let onUp = () => { focusInput(); - removeGlobalListener(window, 'mouseup', onUp, false); + removeGlobalListener(window, 'mouseup', onPointerUp, false); removeGlobalListener(window, 'touchend', onUp, false); - removeGlobalListener(window, 'pointerup', onUp, false); + removeGlobalListener(window, 'pointerup', onPointerUp, false); }; return { @@ -149,9 +160,9 @@ export function useTableColumnResize(props: ResizerProps, state: TableStat if (e.button !== 0 || e.altKey || e.ctrlKey || e.metaKey) { return; } - onDown(e.pointerId); + onDown(); }, - onTouchStart: (e: React.TouchEvent) => {onDown(e.changedTouches[0].identifier);} + onTouchStart: () => {onDown();} } ), inputProps: mergeProps( diff --git a/packages/@react-spectrum/table/src/TableView.tsx b/packages/@react-spectrum/table/src/TableView.tsx index 744cfcd23d0..1af100eebb3 100644 --- a/packages/@react-spectrum/table/src/TableView.tsx +++ b/packages/@react-spectrum/table/src/TableView.tsx @@ -33,8 +33,10 @@ import {TableLayout} from '@react-stately/layout'; import {Tooltip, TooltipTrigger} from '@react-spectrum/tooltip'; import {useButton} from '@react-aria/button'; import {useHover} from '@react-aria/interactions'; +import {useLocale, useMessageFormatter} from '@react-aria/i18n'; +import {usePress} from '@react-aria/interactions'; +import {useProvider, useProviderProps} from '@react-spectrum/provider'; import { - useInteractiveTableColumnHeader, useTable, useTableCell, useTableColumnHeader, @@ -44,9 +46,6 @@ import { useTableSelectAllCheckbox, useTableSelectionCheckbox } from '@react-aria/table'; -import {useLocale, useMessageFormatter} from '@react-aria/i18n'; -import {usePress} from '@react-aria/interactions'; -import {useProvider, useProviderProps} from '@react-spectrum/provider'; import {VisuallyHidden} from '@react-aria/visually-hidden'; const DEFAULT_HEADER_HEIGHT = { diff --git a/packages/@react-spectrum/table/test/TableSizing.test.js b/packages/@react-spectrum/table/test/TableSizing.test.js index 88ea7e94e11..e0e8342bd78 100644 --- a/packages/@react-spectrum/table/test/TableSizing.test.js +++ b/packages/@react-spectrum/table/test/TableSizing.test.js @@ -694,6 +694,7 @@ describe('TableViewSizing', function () { fireEvent.pointerLeave(resizer, {pointerType: 'mouse', pointerId: 1}); fireEvent.pointerLeave(resizableHeader, {pointerType: 'mouse', pointerId: 1}); + act(() => {jest.runAllTimers();}); expect(tree.queryByRole('slider')).toBeNull(); }); @@ -762,6 +763,7 @@ describe('TableViewSizing', function () { fireEvent.pointerLeave(resizer, {pointerType: 'mouse', pointerId: 1}); fireEvent.pointerLeave(resizableHeader, {pointerType: 'mouse', pointerId: 1}); + act(() => {jest.runAllTimers();}); expect(tree.queryByRole('slider')).toBeNull(); }); From 2d942c9ee77e6d51141e4be0e316ac77cccdd325 Mon Sep 17 00:00:00 2001 From: Rob Snow Date: Fri, 15 Jul 2022 14:12:52 -0700 Subject: [PATCH 08/19] Can only support max integer width --- packages/@react-stately/table/src/utils.ts | 26 +++++++++++----------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/@react-stately/table/src/utils.ts b/packages/@react-stately/table/src/utils.ts index 9ae33a5f2ba..dfe00f76283 100644 --- a/packages/@react-stately/table/src/utils.ts +++ b/packages/@react-stately/table/src/utils.ts @@ -14,16 +14,16 @@ export function getContentWidth(widths: Map): number { // numbers and percents are considered static. *fr units or a lack of units are considered dynamic. export function isStatic(width: number | string): boolean { return width != null && (!isNaN(width as number) || (String(width)).match(/^(\d+)(?=%$)/) !== null); -} +} function parseFractionalUnit(width: string): number { if (!width) { return 1; - } + } let match = width.match(/^(\d+)(?=fr$)/); // if width is the incorrect format, just deafult it to a 1fr if (!match) { - console.warn(`width: ${width} is not a supported format, width should be a number (ex. 150), percentage (ex. '50%') or fr unit (ex. '2fr')`, + console.warn(`width: ${width} is not a supported format, width should be a number (ex. 150), percentage (ex. '50%') or fr unit (ex. '2fr')`, 'defaulting to \'1fr\''); return 1; } @@ -40,12 +40,12 @@ export function parseStaticWidth(width: number | string, tableWidth: number): nu } return width; } - - + + export function getMaxWidth(maxWidth: number | string, tableWidth: number): number { return maxWidth != null ? parseStaticWidth(maxWidth, tableWidth) - : Infinity; + : Number.MAX_SAFE_INTEGER; } export function getMinWidth(minWidth: number | string, tableWidth: number): number { @@ -59,7 +59,7 @@ function mapDynamicColumns(dynamicColumns: GridNode[], availableSpace: num (sum, column) => sum + parseFractionalUnit(column.props.defaultWidth), 0 ); - + let columns = dynamicColumns.map((column, index) => { const targetWidth = (parseFractionalUnit(column.props.defaultWidth) * availableSpace) / fractions; @@ -71,10 +71,10 @@ function mapDynamicColumns(dynamicColumns: GridNode[], availableSpace: num return { ...column, index, - delta + delta }; }); - + return columns; } @@ -98,14 +98,14 @@ function findDynamicColumnWidths(dynamicColumns: mappedColumn[], available }); return columns; -} - +} + export function getDynamicColumnWidths(dynamicColumns: GridNode[], availableSpace: number, tableWidth: number) { let columns = mapDynamicColumns(dynamicColumns, availableSpace, tableWidth); - + columns.sort((a, b) => b.delta - a.delta); columns = findDynamicColumnWidths(columns, availableSpace, tableWidth); columns.sort((a, b) => a.index - b.index); - + return columns; } From c58e258cf788280da494566fcb70591d41663ebc Mon Sep 17 00:00:00 2001 From: Rob Snow Date: Fri, 15 Jul 2022 14:26:21 -0700 Subject: [PATCH 09/19] improve announcements --- .../table/src/useTableColumnResize.ts | 17 ++++++++++------- packages/@react-spectrum/table/src/Resizer.tsx | 2 +- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/packages/@react-aria/table/src/useTableColumnResize.ts b/packages/@react-aria/table/src/useTableColumnResize.ts index 930c3117b46..982055e141d 100644 --- a/packages/@react-aria/table/src/useTableColumnResize.ts +++ b/packages/@react-aria/table/src/useTableColumnResize.ts @@ -12,13 +12,14 @@ import {ColumnResizeState, TableState} from '@react-stately/table'; import {focusSafely} from '@react-aria/focus'; -import {focusWithoutScrolling, mergeProps, useGlobalListeners} from '@react-aria/utils'; +import {focusWithoutScrolling, mergeProps, useGlobalListeners, useId} from '@react-aria/utils'; import {GridNode} from '@react-types/grid'; // @ts-ignore import intlMessages from '../intl/*.json'; import React, {ChangeEvent, HTMLAttributes, RefObject, useCallback, useRef} from 'react'; import {useKeyboard, useMove} from '@react-aria/interactions'; import {useLocale, useMessageFormatter} from '@react-aria/i18n'; +import {getColumnHeaderId} from './utils'; interface ResizerAria { inputProps: HTMLAttributes, @@ -32,14 +33,15 @@ interface ResizerProps { } // eslint-disable-next-line @typescript-eslint/no-unused-vars -export function useTableColumnResize(props: ResizerProps, state: TableState & ColumnResizeState, ref: RefObject): ResizerAria { +export function useTableColumnResize(props: ResizerProps, state: TableState, columnState: ColumnResizeState, ref: RefObject): ResizerAria { let {column: item, triggerRef} = props; - const stateRef = useRef(null); + 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 = state; + const cursor = useRef(null); + stateRef.current = columnState; const formatMessage = useMessageFormatter(intlMessages); let {addGlobalListener, removeGlobalListener} = useGlobalListeners(); + let id = useId(); let {direction} = useLocale(); let {keyboardProps} = useKeyboard({ @@ -52,7 +54,7 @@ export function useTableColumnResize(props: ResizerProps, state: TableStat } }); - const columnResizeWidthRef = useRef(null); + const columnResizeWidthRef = useRef(0); const {moveProps} = useMove({ onMoveStart() { columnResizeWidthRef.current = stateRef.current.getColumnWidth(item.key); @@ -93,7 +95,7 @@ export function useTableColumnResize(props: ResizerProps, state: TableStat let ariaProps = { 'aria-label': props.label, 'aria-orientation': 'horizontal' as 'horizontal', - 'aria-labelledby': 'none', + 'aria-labelledby': `${id} ${getColumnHeaderId(state, item.key)}`, 'aria-valuetext': formatMessage('columnSize', {value}), min, max, @@ -167,6 +169,7 @@ export function useTableColumnResize(props: ResizerProps, state: TableStat ), 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 diff --git a/packages/@react-spectrum/table/src/Resizer.tsx b/packages/@react-spectrum/table/src/Resizer.tsx index 54591fbbd95..f11c5380a60 100644 --- a/packages/@react-spectrum/table/src/Resizer.tsx +++ b/packages/@react-spectrum/table/src/Resizer.tsx @@ -23,7 +23,7 @@ function Resizer(props: ResizerProps, ref: RefObject) { let formatMessage = useMessageFormatter(intlMessages); let {direction} = useLocale(); - let {inputProps, resizerProps} = useTableColumnResize({...props, label: formatMessage('columnResizer')}, {...state, ...columnState}, ref); + let {inputProps, resizerProps} = useTableColumnResize({...props, label: formatMessage('columnResizer')}, state, columnState, ref); let style = { cursor: undefined, From 218689ea01db47470410f03b3cd6ca656fd09083 Mon Sep 17 00:00:00 2001 From: Rob Snow Date: Fri, 15 Jul 2022 14:36:13 -0700 Subject: [PATCH 10/19] remove extraneous story --- .../table/stories/Table.stories.tsx | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/packages/@react-spectrum/table/stories/Table.stories.tsx b/packages/@react-spectrum/table/stories/Table.stories.tsx index ce7624316fb..7ce6b67fe37 100644 --- a/packages/@react-spectrum/table/stories/Table.stories.tsx +++ b/packages/@react-spectrum/table/stories/Table.stories.tsx @@ -1361,25 +1361,6 @@ storiesOf('TableView', module) () => ( ) - ) - .add( - 'what', - () => ( - - - Foo - Bar - Baz - - - {item => - ( - {key => {item[key]}} - ) - } - - - ) ); function AsyncLoadingExample(props) { From 9d1d5a433397baaca5422ab7d769be1f062cc667 Mon Sep 17 00:00:00 2001 From: Rob Snow Date: Fri, 15 Jul 2022 14:38:02 -0700 Subject: [PATCH 11/19] fix lint --- 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 982055e141d..db34bde92c5 100644 --- a/packages/@react-aria/table/src/useTableColumnResize.ts +++ b/packages/@react-aria/table/src/useTableColumnResize.ts @@ -13,13 +13,13 @@ import {ColumnResizeState, TableState} from '@react-stately/table'; import {focusSafely} from '@react-aria/focus'; import {focusWithoutScrolling, mergeProps, useGlobalListeners, useId} from '@react-aria/utils'; +import {getColumnHeaderId} from './utils'; import {GridNode} from '@react-types/grid'; // @ts-ignore import intlMessages from '../intl/*.json'; import React, {ChangeEvent, HTMLAttributes, RefObject, useCallback, useRef} from 'react'; import {useKeyboard, useMove} from '@react-aria/interactions'; import {useLocale, useMessageFormatter} from '@react-aria/i18n'; -import {getColumnHeaderId} from './utils'; interface ResizerAria { inputProps: HTMLAttributes, From b9b20432277c6258284b2cb1c214324abd9792fb Mon Sep 17 00:00:00 2001 From: Rob Snow Date: Tue, 26 Jul 2022 17:04:27 -0700 Subject: [PATCH 12/19] fix types --- 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 a5460ea8f0c..06772b06f31 100644 --- a/packages/@react-aria/table/src/useTableColumnResize.ts +++ b/packages/@react-aria/table/src/useTableColumnResize.ts @@ -34,7 +34,7 @@ export interface AriaTableColumnResizeProps { } // eslint-disable-next-line @typescript-eslint/no-unused-vars -export function useTableColumnResize(props: ResizerProps, state: TableState, columnState: ColumnResizeState, ref: RefObject): AriaTableColumnResizeProps { +export function useTableColumnResize(props: AriaTableColumnResizeProps, state: TableState, columnState: ColumnResizeState, ref: RefObject): TableColumnResizeAria { let {column: item, triggerRef} = 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 From 48b09cb5621eac33fdab813a9ecd88ca222397cc Mon Sep 17 00:00:00 2001 From: Robert Snow Date: Thu, 4 Aug 2022 10:43:29 -0700 Subject: [PATCH 13/19] remove extra newline --- packages/@react-aria/table/src/useTableColumnResize.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/@react-aria/table/src/useTableColumnResize.ts b/packages/@react-aria/table/src/useTableColumnResize.ts index 9a44dce0035..4769e05cbdc 100644 --- a/packages/@react-aria/table/src/useTableColumnResize.ts +++ b/packages/@react-aria/table/src/useTableColumnResize.ts @@ -117,7 +117,6 @@ export function useTableColumnResize(props: AriaTableColumnResizeProps, st nextValue = currentWidth + 10; } else { nextValue = currentWidth - 10; - } stateRef.current.onColumnResize(item, nextValue); }; From 4825f5055a22ba447600603716544359c86cf011 Mon Sep 17 00:00:00 2001 From: Rob Snow Date: Wed, 10 Aug 2022 15:17:30 -0700 Subject: [PATCH 14/19] review comments --- .../table/src/useTableColumnResize.ts | 28 ++++++++----------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/packages/@react-aria/table/src/useTableColumnResize.ts b/packages/@react-aria/table/src/useTableColumnResize.ts index 4769e05cbdc..29a2c225c8c 100644 --- a/packages/@react-aria/table/src/useTableColumnResize.ts +++ b/packages/@react-aria/table/src/useTableColumnResize.ts @@ -19,7 +19,7 @@ import {GridNode} from '@react-types/grid'; import intlMessages from '../intl/*.json'; import React, {ChangeEvent, RefObject, useCallback, useRef} from 'react'; import {TableColumnResizeState, TableState} from '@react-stately/table'; -import {useKeyboard, useMove} from '@react-aria/interactions'; +import {useKeyboard, useMove, usePress} from '@react-aria/interactions'; import {useLocale, useLocalizedStringFormatter} from '@react-aria/i18n'; export interface TableColumnResizeAria { @@ -33,7 +33,6 @@ export interface AriaTableColumnResizeProps { triggerRef: RefObject } -// eslint-disable-next-line @typescript-eslint/no-unused-vars export function useTableColumnResize(props: AriaTableColumnResizeProps, state: TableState, columnState: TableColumnResizeState, ref: RefObject): TableColumnResizeAria { let {column: item, triggerRef} = props; const stateRef = useRef>(null); @@ -147,25 +146,20 @@ export function useTableColumnResize(props: AriaTableColumnResizeProps, st removeGlobalListener(window, 'pointerup', onPointerUp, false); }; + let {pressProps} = usePress({ + onPressStart: (e) => { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey || e.pointerType === 'keyboard') { + return; + } + onDown(); + } + }); + return { resizerProps: mergeProps( keyboardProps, moveProps, - { - onMouseDown: (e: React.MouseEvent) => { - if (e.button !== 0 || e.altKey || e.ctrlKey || e.metaKey) { - return; - } - onDown(); - }, - onPointerDown: (e: React.PointerEvent) => { - if (e.button !== 0 || e.altKey || e.ctrlKey || e.metaKey) { - return; - } - onDown(); - }, - onTouchStart: () => {onDown();} - } + pressProps ), inputProps: mergeProps( { From fdf1d1e8cfb2a9576b260e1caf8adf79dec56d73 Mon Sep 17 00:00:00 2001 From: Rob Snow Date: Wed, 10 Aug 2022 15:43:27 -0700 Subject: [PATCH 15/19] Fix Chrome Android --- packages/@react-spectrum/table/src/TableView.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/@react-spectrum/table/src/TableView.tsx b/packages/@react-spectrum/table/src/TableView.tsx index 753358d81bf..88fff0377eb 100644 --- a/packages/@react-spectrum/table/src/TableView.tsx +++ b/packages/@react-spectrum/table/src/TableView.tsx @@ -582,7 +582,10 @@ function ResizableTableColumnHeader(props) { useEffect(() => { if (columnState.currentlyResizingColumn === column.key) { // focusSafely won't actually focus because the focus moves from the menuitem to the body during the after transition wait - resizingRef.current.focus(); + // without the immediate timeout, Android Chrome doesn't move focus to the resizer + setTimeout(() => { + resizingRef.current.focus(); + }, 0); } }, [columnState.currentlyResizingColumn, column.key]); From 6623a3c3c113bd394c4e5808a748298d391f3033 Mon Sep 17 00:00:00 2001 From: Rob Snow Date: Wed, 10 Aug 2022 15:44:42 -0700 Subject: [PATCH 16/19] fix lint --- 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 29a2c225c8c..c7ede4be91a 100644 --- a/packages/@react-aria/table/src/useTableColumnResize.ts +++ b/packages/@react-aria/table/src/useTableColumnResize.ts @@ -10,6 +10,7 @@ * governing permissions and limitations under the License. */ +import {ChangeEvent, RefObject, useCallback, useRef} from 'react'; import {DOMAttributes} from '@react-types/shared'; import {focusSafely} from '@react-aria/focus'; import {focusWithoutScrolling, mergeProps, useGlobalListeners, useId} from '@react-aria/utils'; @@ -17,7 +18,6 @@ import {getColumnHeaderId} from './utils'; import {GridNode} from '@react-types/grid'; // @ts-ignore import intlMessages from '../intl/*.json'; -import React, {ChangeEvent, RefObject, useCallback, useRef} from 'react'; import {TableColumnResizeState, TableState} from '@react-stately/table'; import {useKeyboard, useMove, usePress} from '@react-aria/interactions'; import {useLocale, useLocalizedStringFormatter} from '@react-aria/i18n'; From b48aa5b8ad70eb6fdd47361e273f9599d1f4e0a7 Mon Sep 17 00:00:00 2001 From: Rob Snow Date: Wed, 10 Aug 2022 16:46:28 -0700 Subject: [PATCH 17/19] clicking on resizer in AT should confirm size --- packages/@react-aria/table/src/useTableColumnResize.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/@react-aria/table/src/useTableColumnResize.ts b/packages/@react-aria/table/src/useTableColumnResize.ts index c7ede4be91a..9b9172dc747 100644 --- a/packages/@react-aria/table/src/useTableColumnResize.ts +++ b/packages/@react-aria/table/src/useTableColumnResize.ts @@ -151,6 +151,9 @@ export function useTableColumnResize(props: AriaTableColumnResizeProps, st if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey || e.pointerType === 'keyboard') { return; } + if (e.pointerType === 'virtual' && columnState.currentlyResizingColumn != null) { + stateRef.current.onColumnResizeEnd(item); + } onDown(); } }); From ed01e29a1ddd0eb43d73047977663397a3b76573 Mon Sep 17 00:00:00 2001 From: Rob Snow Date: Thu, 11 Aug 2022 15:18:58 -0700 Subject: [PATCH 18/19] Fix Android Talkback returning focus --- .../table/src/useTableColumnResize.ts | 37 +++++-------------- 1 file changed, 10 insertions(+), 27 deletions(-) diff --git a/packages/@react-aria/table/src/useTableColumnResize.ts b/packages/@react-aria/table/src/useTableColumnResize.ts index 9b9172dc747..006dfbadf56 100644 --- a/packages/@react-aria/table/src/useTableColumnResize.ts +++ b/packages/@react-aria/table/src/useTableColumnResize.ts @@ -120,32 +120,6 @@ export function useTableColumnResize(props: AriaTableColumnResizeProps, st stateRef.current.onColumnResize(item, nextValue); }; - let onDown = () => { - focusInput(); - addGlobalListener(window, 'mouseup', onPointerUp, false); - addGlobalListener(window, 'touchend', onUp, false); - addGlobalListener(window, 'pointerup', onPointerUp, false); - }; - - let onPointerUp = (e) => { - // don't hide the resizer for touch since it's harder to bring back - if (e.pointerType === 'touch') { - focusInput(); - } else { - focusSafely(triggerRef.current); - } - removeGlobalListener(window, 'mouseup', onPointerUp, false); - removeGlobalListener(window, 'touchend', onUp, false); - removeGlobalListener(window, 'pointerup', onPointerUp, false); - }; - - let onUp = () => { - focusInput(); - removeGlobalListener(window, 'mouseup', onPointerUp, false); - removeGlobalListener(window, 'touchend', onUp, false); - removeGlobalListener(window, 'pointerup', onPointerUp, false); - }; - let {pressProps} = usePress({ onPressStart: (e) => { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey || e.pointerType === 'keyboard') { @@ -153,8 +127,17 @@ export function useTableColumnResize(props: AriaTableColumnResizeProps, st } if (e.pointerType === 'virtual' && columnState.currentlyResizingColumn != null) { stateRef.current.onColumnResizeEnd(item); + focusSafely(triggerRef.current); + return; + } + focusInput(); + }, + onPress: (e) => { + if (e.pointerType === 'touch') { + focusInput(); + } else { + focusSafely(triggerRef.current); } - onDown(); } }); From fe73dee47ed93da519668e72fb626332edfac572 Mon Sep 17 00:00:00 2001 From: Rob Snow Date: Thu, 11 Aug 2022 16:00:41 -0700 Subject: [PATCH 19/19] Fix restore focus to trigger --- packages/@react-aria/table/src/useTableColumnHeader.ts | 4 +++- packages/@react-aria/table/src/useTableColumnResize.ts | 5 ++--- packages/@react-spectrum/table/src/TableView.tsx | 4 ++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/@react-aria/table/src/useTableColumnHeader.ts b/packages/@react-aria/table/src/useTableColumnHeader.ts index 8e6a5168595..b2300df6765 100644 --- a/packages/@react-aria/table/src/useTableColumnHeader.ts +++ b/packages/@react-aria/table/src/useTableColumnHeader.ts @@ -88,7 +88,9 @@ export function useTableColumnHeader(props: AriaTableColumnHeaderProps, state return { columnHeaderProps: { - ...mergeProps(gridCellProps, pressProps, focusableProps, descriptionProps), + ...mergeProps(gridCellProps, pressProps, focusableProps, descriptionProps, { + onPointerDown: (e) => console.log(e.target.outerHTML) + }), role: 'columnheader', id: getColumnHeaderId(state, node.key), 'aria-colspan': node.colspan && node.colspan > 1 ? node.colspan : null, diff --git a/packages/@react-aria/table/src/useTableColumnResize.ts b/packages/@react-aria/table/src/useTableColumnResize.ts index 006dfbadf56..5adfb7c51c2 100644 --- a/packages/@react-aria/table/src/useTableColumnResize.ts +++ b/packages/@react-aria/table/src/useTableColumnResize.ts @@ -13,7 +13,7 @@ import {ChangeEvent, RefObject, useCallback, useRef} from 'react'; import {DOMAttributes} from '@react-types/shared'; import {focusSafely} from '@react-aria/focus'; -import {focusWithoutScrolling, mergeProps, useGlobalListeners, useId} from '@react-aria/utils'; +import {focusWithoutScrolling, mergeProps, useId} from '@react-aria/utils'; import {getColumnHeaderId} from './utils'; import {GridNode} from '@react-types/grid'; // @ts-ignore @@ -40,7 +40,6 @@ export function useTableColumnResize(props: AriaTableColumnResizeProps, st const cursor = useRef(null); stateRef.current = columnState; const stringFormatter = useLocalizedStringFormatter(intlMessages); - let {addGlobalListener, removeGlobalListener} = useGlobalListeners(); let id = useId(); let {direction} = useLocale(); @@ -135,7 +134,7 @@ export function useTableColumnResize(props: AriaTableColumnResizeProps, st onPress: (e) => { if (e.pointerType === 'touch') { focusInput(); - } else { + } else if (e.pointerType !== 'virtual') { focusSafely(triggerRef.current); } } diff --git a/packages/@react-spectrum/table/src/TableView.tsx b/packages/@react-spectrum/table/src/TableView.tsx index 88fff0377eb..3a2483c0d5f 100644 --- a/packages/@react-spectrum/table/src/TableView.tsx +++ b/packages/@react-spectrum/table/src/TableView.tsx @@ -13,7 +13,7 @@ import ArrowDownSmall from '@spectrum-icons/ui/ArrowDownSmall'; import {chain, mergeProps, useLayoutEffect} from '@react-aria/utils'; import {Checkbox} from '@react-spectrum/checkbox'; -import {classNames, useDOMRef, useFocusableRef, useStyleProps} from '@react-spectrum/utils'; +import {classNames, useDOMRef, useFocusableRef, useStyleProps, useUnwrapDOMRef} from '@react-spectrum/utils'; import {DOMRef, FocusableRef} from '@react-types/shared'; import {FocusRing, useFocusRing} from '@react-aria/focus'; import {GridNode} from '@react-types/grid'; @@ -640,7 +640,7 @@ function ResizableTableColumnHeader(props) { ref={resizingRef} column={column} showResizer={showResizer} - triggerRef={triggerRef} /> + triggerRef={useUnwrapDOMRef(triggerRef)} />
);