diff --git a/packages/@adobe/spectrum-css-temp/components/table/index.css b/packages/@adobe/spectrum-css-temp/components/table/index.css index c2801095dd5..635cd6796a7 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; @@ -108,7 +117,7 @@ svg.spectrum-Table-sortedIcon { } &:active, - &:focus { + &.focus-ring { outline: none; &::after { inline-size: 2px; @@ -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..1c20d9b89b7 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 { @@ -290,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/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/useTableColumnHeader.ts b/packages/@react-aria/table/src/useTableColumnHeader.ts index 63b451c888d..b2300df6765 100644 --- a/packages/@react-aria/table/src/useTableColumnHeader.ts +++ b/packages/@react-aria/table/src/useTableColumnHeader.ts @@ -27,7 +27,9 @@ export interface AriaTableColumnHeaderProps { /** 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 } export interface TableColumnHeaderAria { @@ -43,18 +45,16 @@ export interface TableColumnHeaderAria { */ export function useTableColumnHeader(props: AriaTableColumnHeaderProps, state: TableState, ref: RefObject): TableColumnHeaderAria { 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 }); @@ -62,6 +62,10 @@ export function useTableColumnHeader(props: AriaTableColumnHeaderProps, state // Needed to pick up the focusable context, enabling things like Tooltips for example let {focusableProps} = useFocusable({}, ref); + if (props.hasMenu) { + pressProps = {}; + } + let ariaSort: DOMAttributes['aria-sort'] = null; let isSortedColumn = state.sortDescriptor?.column === node.key; let sortDirection = state.sortDescriptor?.direction; @@ -84,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 a65005af032..5adfb7c51c2 100644 --- a/packages/@react-aria/table/src/useTableColumnResize.ts +++ b/packages/@react-aria/table/src/useTableColumnResize.ts @@ -10,31 +10,37 @@ * 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, useId} from '@react-aria/utils'; +import {getColumnHeaderId} from './utils'; import {GridNode} from '@react-types/grid'; -import {mergeProps} from '@react-aria/utils'; -import {RefObject, useRef} from 'react'; +// @ts-ignore +import intlMessages from '../intl/*.json'; import {TableColumnResizeState, TableState} from '@react-stately/table'; -import {useKeyboard, useMove} from '@react-aria/interactions'; -import {useLocale} from '@react-aria/i18n'; +import {useKeyboard, useMove, usePress} from '@react-aria/interactions'; +import {useLocale, useLocalizedStringFormatter} from '@react-aria/i18n'; export interface TableColumnResizeAria { + inputProps: DOMAttributes, resizerProps: DOMAttributes } export interface AriaTableColumnResizeProps { column: GridNode, - showResizer: boolean, - label: string + label: string, + triggerRef: RefObject } -export function useTableColumnResize(props: AriaTableColumnResizeProps, state: TableState & TableColumnResizeState, ref: RefObject): TableColumnResizeAria { - let {column: item, showResizer} = props; - const stateRef = useRef(null); +export function useTableColumnResize(props: AriaTableColumnResizeProps, state: TableState, columnState: TableColumnResizeState, 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 - const cursor = useRef(null); - stateRef.current = state; + const cursor = useRef(null); + stateRef.current = columnState; + const stringFormatter = useLocalizedStringFormatter(intlMessages); + let id = useId(); let {direction} = useLocale(); let {keyboardProps} = useKeyboard({ @@ -42,17 +48,14 @@ export function useTableColumnResize(props: AriaTableColumnResizeProps, st 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 columnResizeWidthRef = useRef(0); 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; }, @@ -76,45 +79,89 @@ export function useTableColumnResize(props: AriaTableColumnResizeProps, st } } }, - onMoveEnd({pointerType}) { - if (pointerType !== 'keyboard') { - stateRef.current.onColumnResizeEnd(item); - } + onMoveEnd() { columnResizeWidthRef.current = 0; 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 = { - role: 'separator', 'aria-label': props.label, - 'aria-orientation': 'vertical', - 'aria-labelledby': item.key, - 'aria-valuenow': stateRef.current.getColumnWidth(item.key), - 'aria-valuemin': stateRef.current.getColumnMinWidth(item.key), - 'aria-valuemax': stateRef.current.getColumnMaxWidth(item.key) + 'aria-orientation': 'horizontal' as 'horizontal', + 'aria-labelledby': `${id} ${getColumnHeaderId(state, item.key)}`, + 'aria-valuetext': stringFormatter.format('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); + let nextValue = parseFloat(e.target.value); + + if (nextValue > currentWidth) { + nextValue = currentWidth + 10; + } else { + nextValue = currentWidth - 10; + } + stateRef.current.onColumnResize(item, nextValue); }; + let {pressProps} = usePress({ + onPressStart: (e) => { + if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey || e.pointerType === 'keyboard') { + return; + } + if (e.pointerType === 'virtual' && columnState.currentlyResizingColumn != null) { + stateRef.current.onColumnResizeEnd(item); + focusSafely(triggerRef.current); + return; + } + focusInput(); + }, + onPress: (e) => { + if (e.pointerType === 'touch') { + focusInput(); + } else if (e.pointerType !== 'virtual') { + focusSafely(triggerRef.current); + } + } + }); + 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, + 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 + stateRef.current.onColumnResizeStart(item); + state.setKeyboardNavigationDisabled(true); }, - keyboardProps, - ariaProps - ) - } + onBlur: () => { + stateRef.current.onColumnResizeEnd(item); + state.setKeyboardNavigationDisabled(false); + }, + onChange + }, + ariaProps + ) }; } diff --git a/packages/@react-spectrum/table/package.json b/packages/@react-spectrum/table/package.json index 4cbca53711d..c0b905ca54a 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.6.0", "@react-aria/focus": "^3.7.0", "@react-aria/grid": "^3.4.0", "@react-aria/i18n": "^3.5.0", diff --git a/packages/@react-spectrum/table/src/Resizer.tsx b/packages/@react-spectrum/table/src/Resizer.tsx index 69003c0cc7b..fe7e33ffe53 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'; @@ -8,19 +9,21 @@ import styles from '@adobe/spectrum-css-temp/components/table/vars.css'; import {useLocale, useLocalizedStringFormatter} 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, - showResizer: boolean + showResizer: boolean, + triggerRef: RefObject } -function Resizer(props: ResizerProps, ref: RefObject) { +function Resizer(props: ResizerProps, ref: RefObject) { let {column, showResizer} = props; let {state, columnState} = useTableContext(); let stringFormatter = useLocalizedStringFormatter(intlMessages); let {direction} = useLocale(); - let {resizerProps} = useTableColumnResize({...props, label: stringFormatter.format('columnResizer')}, {...state, ...columnState}, ref); + let {inputProps, resizerProps} = useTableColumnResize({...props, label: stringFormatter.format('columnResizer')}, state, columnState, ref); let style = { cursor: undefined, @@ -35,12 +38,22 @@ function Resizer(props: ResizerProps, ref: RefObject) { } else { style.cursor = 'col-resize'; } + return ( -
+ +
+ + + +
+
); } diff --git a/packages/@react-spectrum/table/src/TableView.tsx b/packages/@react-spectrum/table/src/TableView.tsx index 87b0608bef0..3a2483c0d5f 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 {DOMRef} from '@react-types/shared'; -import {FocusRing, focusSafely, useFocusRing} from '@react-aria/focus'; +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'; // @ts-ignore import intlMessages from '../intl/*.json'; @@ -31,9 +31,9 @@ import stylesOverrides from './table.css'; import {TableColumnResizeState, TableState, useTableColumnResizeState, useTableState} from '@react-stately/table'; import {TableLayout} from '@react-stately/layout'; import {Tooltip, TooltipTrigger} from '@react-spectrum/tooltip'; -import {useHover} from '@react-aria/interactions'; +import {useButton} from '@react-aria/button'; +import {useHover, usePress} from '@react-aria/interactions'; import {useLocale, useLocalizedStringFormatter} from '@react-aria/i18n'; -import {usePress} from '@react-aria/interactions'; import {useProvider, useProviderProps} from '@react-spectrum/provider'; import { useTable, @@ -80,7 +80,8 @@ const SELECTION_CELL_DEFAULT_WIDTH = { interface TableContextValue { state: TableState, layout: TableLayout, - columnState: TableColumnResizeState + columnState: TableColumnResizeState, + headerRowHovered: boolean } const TableContext = React.createContext>(null); @@ -151,6 +152,7 @@ function TableView(props: SpectrumTableProps, ref: DOMRef, unknown>; @@ -193,6 +195,7 @@ function TableView(props: SpectrumTableProps, ref: DOMRef @@ -303,7 +306,7 @@ function TableView(props: SpectrumTableProps, ref: DOMRef + ; - 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 {state, columnState} = useTableContext(); + let ref = useRef(null); + let triggerRef = useRef(null); + let resizingRef = useRef(null); + let {state, columnState, headerRowHovered} = useTableContext(); let stringFormatter = useLocalizedStringFormatter(intlMessages); - let [isHovered, setIsHovered] = useState(false); + let {columnHeaderProps} = useTableColumnHeader({ + node: column, + isVirtualized: true, + hasMenu: 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) { @@ -551,33 +578,71 @@ function ResizableTableColumnHeader(props) { ]; return options; }, [allowsSorting]); - // if we're resizing another column, then hover shouldn't show the resizer of a different column - let showResizer = (isHovered && !columnState.currentlyResizingColumn) || columnState.currentlyResizingColumn === column.key; useEffect(() => { if (columnState.currentlyResizingColumn === column.key) { - focusSafely(ref.current); + // focusSafely won't actually focus because the focus moves from the menuitem to the body during the after transition wait + // without the immediate timeout, Android Chrome doesn't move focus to the resizer + setTimeout(() => { + resizingRef.current.focus(); + }, 0); } }, [columnState.currentlyResizingColumn, column.key]); + let showResizer = headerRowHovered || columnState.currentlyResizingColumn != null; + return ( - <> - - - - - - {(item) => ( - - {item.label} - - )} - - - + +
1, + 'react-spectrum-Table-cell--alignEnd': columnProps.align === 'end' + } + ) + ) + }> + + + {columnProps.hideHeader ? + {column.rendered} : + column.rendered + } + {columnProps.allowsSorting && + + } + + + {(item) => ( + + {item.label} + + )} + + + +
+
); } @@ -707,13 +772,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/test/TableSizing.test.js b/packages/@react-spectrum/table/test/TableSizing.test.js index d8053b33fcc..e0e8342bd78 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'; @@ -648,7 +650,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 +663,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); @@ -692,8 +694,9 @@ describe('TableViewSizing', function () { fireEvent.pointerLeave(resizer, {pointerType: 'mouse', pointerId: 1}); fireEvent.pointerLeave(resizableHeader, {pointerType: 'mouse', pointerId: 1}); + act(() => {jest.runAllTimers();}); - expect(tree.queryByRole('separator')).toBeNull(); + expect(tree.queryByRole('slider')).toBeNull(); }); it('dragging the resizer works - mobile', () => { @@ -716,7 +719,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 +732,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); @@ -760,8 +763,9 @@ describe('TableViewSizing', function () { fireEvent.pointerLeave(resizer, {pointerType: 'mouse', pointerId: 1}); fireEvent.pointerLeave(resizableHeader, {pointerType: 'mouse', pointerId: 1}); + act(() => {jest.runAllTimers();}); - expect(tree.queryByRole('separator')).toBeNull(); + expect(tree.queryByRole('slider')).toBeNull(); }); }); @@ -769,6 +773,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,11 +792,10 @@ 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('separator')).toBeNull(); + expect(tree.queryByRole('slider')).toBeNull(); let rows = tree.getAllByRole('row'); @@ -801,20 +805,19 @@ 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}); + 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('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,10 +847,11 @@ 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', () => { + setInteractionModality('pointer'); let tree = render( @@ -869,7 +873,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'); @@ -879,7 +883,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}); @@ -891,7 +896,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 +928,7 @@ describe('TableViewSizing', function () { act(() => resizer.blur()); act(() => {jest.runAllTimers();}); - expect(tree.queryByRole('separator')).toBeNull(); + expect(tree.queryByRole('slider')).toBeNull(); }); }); @@ -951,9 +956,10 @@ 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('separator')).toBeNull(); + expect(tree.queryByRole('slider')).toBeNull(); let rows = tree.getAllByRole('row'); @@ -971,7 +977,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 +1010,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( @@ -1028,9 +1034,10 @@ 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('separator')).toBeNull(); + expect(tree.queryByRole('slider')).toBeNull(); let rows = tree.getAllByRole('row'); @@ -1048,7 +1055,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 +1088,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); @@ -1106,9 +1113,10 @@ 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('separator')).toBeNull(); + expect(tree.queryByRole('slider')).toBeNull(); fireEvent.keyDown(document.activeElement, {key: 'Enter'}); @@ -1119,7 +1127,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 +1136,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); @@ -1153,9 +1161,10 @@ 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('separator')).toBeNull(); + expect(tree.queryByRole('slider')).toBeNull(); fireEvent.keyDown(document.activeElement, {key: 'Enter'}); @@ -1166,7 +1175,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 +1183,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); @@ -1199,9 +1208,10 @@ 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('separator')).toBeNull(); + expect(tree.queryByRole('slider')).toBeNull(); fireEvent.keyDown(document.activeElement, {key: 'Enter'}); @@ -1212,7 +1222,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 +1230,7 @@ describe('TableViewSizing', function () { expect(document.activeElement).toBe(resizableHeader); - expect(tree.queryByRole('separator')).toBeNull(); + expect(tree.queryByRole('slider')).toBeNull(); }); }); }); 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; }