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

feat: Accessible resize handles with keyboard activation (toggle button) #1559

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 7 additions & 9 deletions src/table/__integ__/resizable-columns.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,9 @@ class TablePage extends BasePageObject {
async assertColumnWidth(columnIndex: number, expected: number) {
await this.browser.waitUntil(async () => (await this.getColumnWidth(columnIndex)) === expected, {
timeout: 1000,
timeoutMsg: `Column at index "${columnIndex}" should have width "${expected}"`,
timeoutMsg: `Column at index "${columnIndex}" should have width "${expected}". Observed width is "${await this.getColumnWidth(
columnIndex
)}".`,
});
}
}
Expand Down Expand Up @@ -268,16 +270,12 @@ test(
await page.keys(['Tab']);
// wait for the resizer to attach handler

await page.keys(['ArrowRight']);
await page.assertColumnWidth(1, originalWidth + 10);
await expect(page.readTableWidth(1)).resolves.toEqual(originalWidth + 10);

await page.keys(['ArrowRight']);
await page.keys(['Enter', 'ArrowRight', 'ArrowRight', 'Enter']);
await page.assertColumnWidth(1, originalWidth + 20);
await expect(page.readTableWidth(1)).resolves.toEqual(originalWidth + 20);

await page.keys(['ArrowLeft']);
await page.assertColumnWidth(1, originalWidth + 10);
await expect(page.readTableWidth(1)).resolves.toEqual(originalWidth + 10);
await page.keys(['Enter', 'ArrowRight', 'Escape']);
await page.assertColumnWidth(1, originalWidth + 20);
await expect(page.readTableWidth(1)).resolves.toEqual(originalWidth + 20);
})
);
66 changes: 56 additions & 10 deletions src/table/__tests__/resizable-columns.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
import * as React from 'react';
import times from 'lodash/times';
import { render, screen, waitFor } from '@testing-library/react';
import { render, screen } from '@testing-library/react';
import createWrapper, { TableWrapper } from '../../../lib/components/test-utils/dom';
import Table, { TableProps } from '../../../lib/components/table';
import resizerStyles from '../../../lib/components/table/resizer/styles.css.js';
Expand Down Expand Up @@ -310,25 +310,71 @@
HTMLElement.prototype.getBoundingClientRect = originalBoundingClientRect;
});

test('resizes columns with keyboard to the lect', async () => {
test('ignores arrow keys before entering the dragging mode', () => {
const onChange = jest.fn();
const { wrapper } = renderTable(<Table {...defaultProps} onColumnWidthsChange={event => onChange(event.detail)} />);
const columnResizerWrapper = wrapper.findColumnResizer(1)!;

columnResizerWrapper.focus();
columnResizerWrapper.keydown(KeyCode.right);
columnResizerWrapper.click();

expect(onChange).toHaveBeenCalledTimes(0);
});

test('activates and commits resize with resizer click', () => {
const onChange = jest.fn();
const { wrapper } = renderTable(<Table {...defaultProps} onColumnWidthsChange={event => onChange(event.detail)} />);
const columnResizerWrapper = wrapper.findColumnResizer(1)!;

columnResizerWrapper.focus();
columnResizerWrapper.click();
columnResizerWrapper.keydown(KeyCode.left);
expect(columnResizerWrapper.getElement()).toHaveTextContent('140');

Check failure on line 333 in src/table/__tests__/resizable-columns.test.tsx

View workflow job for this annotation

GitHub Actions / build / build

resize with keyboard › activates and commits resize with resizer click

expect(element).toHaveTextContent() Expected element to have text content: 140 Received: at Object.<anonymous> (src/table/__tests__/resizable-columns.test.tsx:333:47)

Check failure on line 333 in src/table/__tests__/resizable-columns.test.tsx

View workflow job for this annotation

GitHub Actions / dry-run / Components unit tests

resize with keyboard › activates and commits resize with resizer click

expect(element).toHaveTextContent() Expected element to have text content: 140 Received: at Object.<anonymous> (src/table/__tests__/resizable-columns.test.tsx:333:47)

columnResizerWrapper.click();

expect(onChange).toHaveBeenCalledTimes(1);
expect(onChange).toHaveBeenCalledWith({ widths: [140, 300] });
});

test('discards resize with escape', () => {
const onChange = jest.fn();
const { wrapper } = renderTable(<Table {...defaultProps} onColumnWidthsChange={event => onChange(event.detail)} />);
const columnResizerWrapper = wrapper.findColumnResizer(1)!;

await waitFor(() => {
expect(onChange).toHaveBeenCalledTimes(1);
expect(onChange).toHaveBeenCalledWith({ widths: [150 - 10, 300] });
});
columnResizerWrapper.focus();
columnResizerWrapper.click();

columnResizerWrapper.keydown(KeyCode.right);
expect(columnResizerWrapper.getElement()).toHaveTextContent('160');

Check failure on line 350 in src/table/__tests__/resizable-columns.test.tsx

View workflow job for this annotation

GitHub Actions / build / build

resize with keyboard › discards resize with escape

expect(element).toHaveTextContent() Expected element to have text content: 160 Received: at Object.<anonymous> (src/table/__tests__/resizable-columns.test.tsx:350:47)

Check failure on line 350 in src/table/__tests__/resizable-columns.test.tsx

View workflow job for this annotation

GitHub Actions / dry-run / Components unit tests

resize with keyboard › discards resize with escape

expect(element).toHaveTextContent() Expected element to have text content: 160 Received: at Object.<anonymous> (src/table/__tests__/resizable-columns.test.tsx:350:47)

columnResizerWrapper.keydown(KeyCode.escape);
expect(columnResizerWrapper.getElement()).toHaveTextContent('150');

columnResizerWrapper.click();

expect(onChange).toHaveBeenCalledTimes(0);
});

test('discards resize on blur', () => {
const onChange = jest.fn();
const { wrapper } = renderTable(<Table {...defaultProps} onColumnWidthsChange={event => onChange(event.detail)} />);
const columnResizerWrapper = wrapper.findColumnResizer(1)!;

columnResizerWrapper.focus();
columnResizerWrapper.click();

columnResizerWrapper.keydown(KeyCode.right);
expect(columnResizerWrapper.getElement()).toHaveTextContent('160');

Check failure on line 369 in src/table/__tests__/resizable-columns.test.tsx

View workflow job for this annotation

GitHub Actions / build / build

resize with keyboard › discards resize on blur

expect(element).toHaveTextContent() Expected element to have text content: 160 Received: at Object.<anonymous> (src/table/__tests__/resizable-columns.test.tsx:369:47)

Check failure on line 369 in src/table/__tests__/resizable-columns.test.tsx

View workflow job for this annotation

GitHub Actions / dry-run / Components unit tests

resize with keyboard › discards resize on blur

expect(element).toHaveTextContent() Expected element to have text content: 160 Received: at Object.<anonymous> (src/table/__tests__/resizable-columns.test.tsx:369:47)

wrapper.findColumnResizer(2)!.focus();
expect(columnResizerWrapper.getElement()).toHaveTextContent('150');

columnResizerWrapper.focus();
columnResizerWrapper.click();

await waitFor(() => {
expect(onChange).toHaveBeenCalledTimes(2);
expect(onChange).toHaveBeenCalledWith({ widths: [150 + 10, 300] });
});
expect(onChange).toHaveBeenCalledTimes(0);
});
});

Expand All @@ -352,7 +398,7 @@
const getResizeHandle = (columnIndex: number) =>
wrapper.findColumnHeaders()[columnIndex].findByClassName(resizerStyles.resizer)!.getElement();

expect(getResizeHandle(0)).toHaveAccessibleName('Id');

Check failure on line 401 in src/table/__tests__/resizable-columns.test.tsx

View workflow job for this annotation

GitHub Actions / build / build

column header content › resize handles have expected accessible names

expect(element).toHaveAccessibleName() Expected element to have accessible name: Id Received: Resize handle for Id, 0 at Object.<anonymous> (src/table/__tests__/resizable-columns.test.tsx:401:32)

Check failure on line 401 in src/table/__tests__/resizable-columns.test.tsx

View workflow job for this annotation

GitHub Actions / dry-run / Components unit tests

column header content › resize handles have expected accessible names

expect(element).toHaveAccessibleName() Expected element to have accessible name: Id Received: Resize handle for Id, 0 at Object.<anonymous> (src/table/__tests__/resizable-columns.test.tsx:401:32)
expect(getResizeHandle(1)).toHaveAccessibleName('Description');
});
});
20 changes: 14 additions & 6 deletions src/table/header-cell/index.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import clsx from 'clsx';
import React from 'react';
import React, { useRef } from 'react';
import InternalIcon from '../../icon/internal';
import { KeyCode } from '../../internal/keycode';
import { TableProps } from '../interfaces';
import { getSortingIconName, getSortingStatus, isSorted } from './utils';
import styles from './styles.css.js';
import { Resizer } from '../resizer';
import { useUniqueId } from '../../internal/hooks/use-unique-id';
import { useInternalI18n } from '../../i18n/context';
import { StickyColumnsModel } from '../sticky-columns';
import { TableRole } from '../table-role';
import { TableThElement } from './th-element';
import { useMergeRefs } from '../../internal/hooks/use-merge-refs';

interface TableHeaderCellProps<ItemType> {
className?: string;
Expand Down Expand Up @@ -82,13 +82,17 @@ export function TableHeaderCell<ItemType>({
}
};

const headerId = useUniqueId('table-header-');
const headerCellRef = useRef<HTMLElement>(null);
const mergedCellRef = useMergeRefs(headerCellRef, cellRef);
const getHeaderCell = () => headerCellRef.current;
const headerContentRef = useRef<HTMLDivElement>(null);
const getHeaderContent = () => headerContentRef.current;

return (
<TableThElement
className={className}
style={style}
cellRef={cellRef}
cellRef={mergedCellRef}
sortingStatus={sortingStatus}
sortingDisabled={sortingDisabled}
hidden={hidden}
Expand Down Expand Up @@ -121,7 +125,10 @@ export function TableHeaderCell<ItemType>({
}
: {})}
>
<div className={clsx(styles['header-cell-text'], wrapLines && styles['header-cell-text-wrap'])} id={headerId}>
<div
ref={headerContentRef}
className={clsx(styles['header-cell-text'], wrapLines && styles['header-cell-text-wrap'])}
>
{column.header}
{isEditable ? (
<span
Expand All @@ -146,7 +153,8 @@ export function TableHeaderCell<ItemType>({
showFocusRing={focusedComponent === `resize-control-${String(columnId)}`}
onDragMove={newWidth => updateColumn(columnId, newWidth)}
onFinish={onResizeFinish}
ariaLabelledby={headerId}
getHeaderCell={getHeaderCell}
getHeaderContent={getHeaderContent}
minWidth={typeof column.minWidth === 'string' ? parseInt(column.minWidth) : column.minWidth}
/>
)}
Expand Down
2 changes: 1 addition & 1 deletion src/table/header-cell/th-element.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ interface TableThElementProps {
resizableColumns?: boolean;
columnId: PropertyKey;
stickyState: StickyColumnsModel;
cellRef?: React.RefCallback<HTMLElement>;
cellRef?: React.Ref<HTMLElement>;
tableRole: TableRole;
children: React.ReactNode;
}
Expand Down
Loading
Loading