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

52 changes: 1 addition & 51 deletions apps/vr-tests-react-components/src/stories/Table.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ import {
import { Button } from '@fluentui/react-button';
george-cz marked this conversation as resolved.
Show resolved Hide resolved
george-cz marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -635,52 +634,6 @@ const Truncate: React.FC<SharedVrTestArgs & { truncate?: boolean }> = ({ noNativ
</Table>
);

const KeyboardColumnResizingStyle: React.FC<SharedVrTestArgs> = ({ noNativeElements }) => {
return (
<Table noNativeElements={noNativeElements}>
<TableHeader>
<TableRow>
{columns.map(column => (
<TableHeaderCell key={column.columnKey} {...{ [`${KeyboardResizingCurrentColumnDataAttribute}`]: '' }}>
{column.label}
</TableHeaderCell>
))}
</TableRow>
</TableHeader>
<TableBody>
{items.map((item, i) => (
<TableRow key={item.file.label} className={`row-${i}`}>
<TableCell>
<TableCellLayout media={item.file.icon}>
{item.file.label}
<TableCellActions>
<Button icon={<EditRegular />} appearance="subtle" />
<Button icon={<MoreHorizontalRegular />} appearance="subtle" />
</TableCellActions>
</TableCellLayout>
</TableCell>
<TableCell>
<TableCellLayout
media={
<Avatar name={item.author.label} badge={{ status: item.author.status as PresenceBadgeStatus }} />
}
>
{item.author.label}
</TableCellLayout>
</TableCell>
<TableCell>
<TableCellLayout>{item.lastUpdated.label}</TableCellLayout>
</TableCell>
<TableCell>
<TableCellLayout media={item.lastUpdate.icon}>{item.lastUpdate.label}</TableCellLayout>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
);
};

([true, false] as const).forEach(noNativeElements => {
const layoutName = noNativeElements ? 'flex' : 'table';
storiesOf(`Table layout ${layoutName} - cell actions`, module)
Expand Down Expand Up @@ -776,10 +729,7 @@ const KeyboardColumnResizingStyle: React.FC<SharedVrTestArgs> = ({ noNativeEleme
includeDarkMode: true,
includeHighContrast: true,
includeRtl: true,
})
.addStory('keyboard column resizing style', () => (
<KeyboardColumnResizingStyle noNativeElements={noNativeElements} />
));
});

storiesOf(`Table ${layoutName} - subtle selection`, module)
.addDecorator(story => (
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": "[email protected]",
"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': {
ling1726 marked this conversation as resolved.
Show resolved Hide resolved
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(), []);
george-cz marked this conversation as resolved.
Show resolved Hide resolved

const keyboardHandler = useEventCallback((event: KeyboardEvent) => {
const keyboardHandler = useEventCallback((event: React.KeyboardEvent) => {
if (!columnId) {
return;
}
Expand All @@ -36,15 +36,15 @@ export function useKeyboardResizing(columnResizeState: ColumnResizeState) {
switch (event.key) {
case ArrowLeft:
stopEvent();
columnResizeStateRef.current.setColumnWidth(event, {
columnResizeStateRef.current.setColumnWidth(event.nativeEvent, {
columnId,
width: width - (precisionModifier ? STEP * PRECISION_FACTOR : STEP),
});
return;

case ArrowRight:
stopEvent();
columnResizeStateRef.current.setColumnWidth(event, {
columnResizeStateRef.current.setColumnWidth(event.nativeEvent, {
columnId,
width: width + (precisionModifier ? STEP * PRECISION_FACTOR : STEP),
});
Expand All @@ -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();
ling1726 marked this conversation as resolved.
Show resolved Hide resolved
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:/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(() => {
george-cz marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -88,7 +88,15 @@ describe('useTableColumnSizing', () => {
const props = renderHookResult.result.current.columnSizing_unstable.getTableHeaderCellProps(1);
expect(props).toMatchInlineSnapshot(`
Object {
"aside": <TableResizeHandle />,
"aside": <TableResizeHandle
aria-hidden={true}
aria-label="Resize column"
aria-valuetext="150 pixels"
data-tabster="{\\"focusable\\":{\\"ignoreKeydown\\":{\\"ArrowLeft\\":true,\\"ArrowRight\\":true}}}"
onBlur={[Function]}
onKeyDown={[Function]}
role="separator"
/>,
"style": Object {
"maxWidth": 150,
"minWidth": 150,
Expand Down
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,
george-cz marked this conversation as resolved.
Show resolved Hide resolved
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
Loading
Loading