Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Table/DataGrid: keyboard resizing improvements #28493

Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ import {
import { Button } from '@fluentui/react-button';
import { storiesOf } from '@storybook/react';
import { Steps, StoryWright } from 'storywright';
import { KeyboardResizingCurrentColumnDataAttribute } from '../../../../packages/react-components/react-table/src/hooks/useTableColumnSizing';

const items = [
{
Expand Down Expand Up @@ -641,9 +640,7 @@ const KeyboardColumnResizingStyle: React.FC<SharedVrTestArgs> = ({ noNativeEleme
<TableHeader>
<TableRow>
{columns.map(column => (
<TableHeaderCell key={column.columnKey} {...{ [`${KeyboardResizingCurrentColumnDataAttribute}`]: '' }}>
{column.label}
</TableHeaderCell>
<TableHeaderCell key={column.columnKey}>{column.label}</TableHeaderCell>
))}
</TableRow>
</TableHeader>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "Table/DataGrid: Improve keyboard column resizing experience",
"packageName": "@fluentui/react-table",
"email": "jirivyhnalek@microsoft.com",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { tokens } from '@fluentui/react-theme';
import type { SlotClassNames } from '@fluentui/react-utilities';
import { createCustomFocusIndicatorStyle } from '@fluentui/react-tabster';
import type { TableHeaderCellSlots, TableHeaderCellState } from './TableHeaderCell.types';
import { KeyboardResizingCurrentColumnDataAttribute } from '../../hooks/useTableColumnSizing';

export const tableHeaderCellClassName = 'fui-TableHeaderCell';
export const tableHeaderCellClassNames: SlotClassNames<TableHeaderCellSlots> = {
Expand Down Expand Up @@ -43,10 +42,6 @@ const useStyles = makeStyles({
{ selector: 'focus-within' },
),
position: 'relative',
[`[${KeyboardResizingCurrentColumnDataAttribute}]`]: {
...shorthands.borderRadius(tokens.borderRadiusMedium),
...shorthands.outline(tokens.strokeWidthThick, 'solid', tokens.colorStrokeFocus2),
},
},

rootInteractive: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ const useStyles = makeStyles({
transitionDuration: '.2s',
zIndex: 1,

':focus': {
opacity: 1,
...shorthands.outline('2px', 'solid', tokens.colorStrokeFocus2),
...shorthands.borderRadius(tokens.borderRadiusMedium),
},

':hover': {
opacity: 1,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as React from 'react';
import { ArrowLeft, ArrowRight, Enter, Escape, Shift, Space } from '@fluentui/keyboard-keys';
import { useEventCallback } from '@fluentui/react-utilities';
import { ColumnResizeState, EnableKeyboardModeOnChangeCallback, TableColumnId } from './types';
import { useFluent_unstable as useFluent } from '@fluentui/react-shared-contexts';
import { useFocusFinders, useTabsterAttributes } from '@fluentui/react-tabster';

const STEP = 20;
const PRECISION_MODIFIER = Shift;
Expand All @@ -11,16 +11,16 @@ const PRECISION_FACTOR = 1 / 4;
export function useKeyboardResizing(columnResizeState: ColumnResizeState) {
const [columnId, setColumnId] = React.useState<TableColumnId>();
const onChangeRef = React.useRef<EnableKeyboardModeOnChangeCallback>();
const addListenerTimeout = React.useRef<number>();
const { findPrevFocusable } = useFocusFinders();

const columnResizeStateRef = React.useRef<ColumnResizeState>(columnResizeState);
React.useEffect(() => {
columnResizeStateRef.current = columnResizeState;
}, [columnResizeState]);

const { targetDocument } = useFluent();
const refMap = React.useMemo(() => new Map(), []);

const keyboardHandler = useEventCallback((event: KeyboardEvent) => {
const keyboardHandler = useEventCallback((event: React.KeyboardEvent) => {
if (!columnId) {
return;
}
Expand Down Expand Up @@ -54,57 +54,85 @@ export function useKeyboardResizing(columnResizeState: ColumnResizeState) {
case Enter:
case Escape:
stopEvent();
disableInteractiveMode();
// Just blur here, the onBlur handler will take care of the rest (disableInteractiveMode).
refMap.get(columnId)?.current?.blur();
break;
}
});

// On component unmout, cancel any timer for adding a listener (if it exists) and remove the listener
React.useEffect(
() => () => {
targetDocument?.defaultView?.clearTimeout(addListenerTimeout.current);
targetDocument?.defaultView?.removeEventListener('keydown', keyboardHandler);
},
[keyboardHandler, targetDocument?.defaultView],
);

const enableInteractiveMode = React.useCallback(
(colId: TableColumnId) => {
setColumnId(colId);
onChangeRef.current?.(colId, true);
// Create the listener in the next tick, because the event that triggered this is still propagating
// when Enter was pressed and would be caught in the keyboardHandler, disabling the keyboard mode immediately.
// No idea why this is happening, but this is a working workaround.
// Tracked here: https://github.com/microsoft/fluentui/issues/27177
addListenerTimeout.current = targetDocument?.defaultView?.setTimeout(() => {
targetDocument?.defaultView?.addEventListener('keydown', keyboardHandler);
}, 0);
// This needs to happen asynchronously, because we cannot focus an element with without tabIndex.
// Only after we have called "setColumnId" the tabIndex will be set to 0 for this handle.
// Meaning we have to wait until the DOM updates with the new tabIndex and then we can focus.
setTimeout(() => {
refMap.get(colId)?.current?.focus();
}, 50);
},
[keyboardHandler, targetDocument?.defaultView],
[refMap],
);

const disableInteractiveMode = React.useCallback(() => {
// Notify the onChange listener that we are disabling interactive mode.
if (columnId) {
onChangeRef.current?.(columnId, false);
}
// Find the previous focusable element (table header button) and focus it.
const el = refMap.get(columnId)?.current;
if (el) {
findPrevFocusable(el)?.focus();
}

setColumnId(undefined);
targetDocument?.defaultView?.removeEventListener('keydown', keyboardHandler);
}, [columnId, keyboardHandler, targetDocument?.defaultView]);
}, [columnId, findPrevFocusable, refMap]);

const toggleInteractiveMode = (colId: TableColumnId, onChange?: EnableKeyboardModeOnChangeCallback) => {
onChangeRef.current = onChange;
if (!columnId) {
enableInteractiveMode(colId);
} else if (colId && columnId !== colId) {
enableInteractiveMode(colId);
setColumnId(colId);
onChange?.(columnId, true);
} else {
disableInteractiveMode();
}
};

const getKeyboardResizingRef = React.useCallback(
(colId: TableColumnId) => {
const ref = refMap.get(colId) || React.createRef<HTMLDivElement>();
refMap.set(colId, ref);
return ref;
},
[refMap],
);

// This makes sure the left and right arrow keys are ignored in tabster,
// so that they can be used for resizing.
const tabsterAttrs = useTabsterAttributes({
focusable: {
ignoreKeydown: {
ArrowLeft: true,
ArrowRight: true,
},
},
});

return {
toggleInteractiveMode,
columnId,
getKeyboardResizingProps: (colId: TableColumnId, currentWidth: number) => ({
onKeyDown: keyboardHandler,
onBlur: disableInteractiveMode,
ref: getKeyboardResizingRef(colId),
role: 'separator',
'aria-label': 'Resize column',
'aria-valuetext': `${currentWidth} pixels`,
'aria-hidden': colId === columnId ? false : true,
tabIndex: colId === columnId ? 0 : undefined,
...tabsterAttrs,
}),
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,12 @@ import {
TableFeaturesState,
UseTableColumnSizingParams,
} from './types';

import { useMeasureElement } from './useMeasureElement';
import { useTableColumnResizeMouseHandler } from './useTableColumnResizeMouseHandler';
import { useTableColumnResizeState } from './useTableColumnResizeState';
import { useKeyboardResizing } from './useKeyboardResizing';

export const KeyboardResizingCurrentColumnDataAttribute = 'data-keyboard-resizing';

export const defaultColumnSizingState: TableColumnSizingState = {
getColumnWidths: () => [],
getOnMouseDown: () => () => null,
Expand Down Expand Up @@ -55,7 +54,7 @@ function useTableColumnSizingState<TItem>(
// Creates the mouse handler and attaches the state to it
const mouseHandler = useTableColumnResizeMouseHandler(columnResizeState);
// Creates the keyboard handler for resizing columns
const { toggleInteractiveMode, columnId: keyboardResizingColumnId } = useKeyboardResizing(columnResizeState);
const { toggleInteractiveMode, getKeyboardResizingProps } = useKeyboardResizing(columnResizeState);

const enableKeyboardMode = React.useCallback(
(columnId: TableColumnId, onChange?: EnableKeyboardModeOnChangeCallback) =>
Expand Down Expand Up @@ -83,13 +82,13 @@ function useTableColumnSizingState<TItem>(
<TableResizeHandle
onMouseDown={mouseHandler.getOnMouseDown(columnId)}
onTouchStart={mouseHandler.getOnMouseDown(columnId)}
{...getKeyboardResizingProps(columnId, col?.width || 0)}
/>
);
return col
? {
style: getColumnStyles(col),
aside,
...(keyboardResizingColumnId === columnId ? { [KeyboardResizingCurrentColumnDataAttribute]: '' } : {}),
}
: {};
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@ import {
MenuPopover,
MenuTrigger,
MenuItem,
useFocusFinders,
TableColumnId,
} from '@fluentui/react-components';

type FileCell = {
Expand Down Expand Up @@ -178,18 +176,6 @@ const columnSizingOptions = {

export const KeyboardColumnResizing = () => {
const refMap = React.useRef<Record<string, HTMLElement | null>>({});
const { findFirstFocusable } = useFocusFinders();

// This will focus on the correct table header cell when the keyboard mode is turned off
const onKeyboardModeChange = React.useCallback(
(columnId: TableColumnId, isKeyboardMode: boolean) => {
const element = refMap.current[columnId];
if (!isKeyboardMode && element) {
findFirstFocusable(element)?.focus();
}
},
[findFirstFocusable],
);

return (
<DataGrid
Expand All @@ -213,9 +199,7 @@ export const KeyboardColumnResizing = () => {
</MenuTrigger>
<MenuPopover>
<MenuList>
<MenuItem
onClick={dataGrid.columnSizing_unstable.enableKeyboardMode(columnId, onKeyboardModeChange)}
>
<MenuItem onClick={dataGrid.columnSizing_unstable.enableKeyboardMode(columnId)}>
Keyboard Column Resizing
</MenuItem>
</MenuList>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@ import {
MenuPopover,
MenuTrigger,
useArrowNavigationGroup,
TableColumnId,
useFocusFinders,
} from '@fluentui/react-components';
import {
DocumentPdfRegular,
Expand Down Expand Up @@ -149,21 +147,6 @@ export const KeyboardColumnResizing = () => {
axis: 'grid',
});

const { findFirstFocusable } = useFocusFinders();

const refMap = React.useRef<Record<string, HTMLElement | null>>({});

// This will focus on the correct table header cell when the keyboard mode is turned off
const onKeyboardModeChange = React.useCallback(
(columnId: TableColumnId, isKeyboardMode: boolean) => {
const element = refMap.current[columnId];
if (!isKeyboardMode && element) {
findFirstFocusable(element)?.focus();
}
},
[findFirstFocusable],
);

return (
<Table sortable aria-label="Table with sort" ref={tableRef} {...keyboardNavAttr}>
<TableHeader>
Expand All @@ -173,15 +156,14 @@ export const KeyboardColumnResizing = () => {
<MenuTrigger>
<TableHeaderCell
key={column.columnId}
ref={el => (refMap.current[column.columnId] = el)}
{...columnSizing_unstable.getTableHeaderCellProps(column.columnId)}
>
{column.renderHeaderCell()}
</TableHeaderCell>
</MenuTrigger>
<MenuPopover>
<MenuList>
<MenuItem onClick={columnSizing_unstable.enableKeyboardMode(column.columnId, onKeyboardModeChange)}>
<MenuItem onClick={columnSizing_unstable.enableKeyboardMode(column.columnId)}>
Keyboard Column Resizing
</MenuItem>
</MenuList>
Expand Down