Skip to content

Commit

Permalink
feat: Table roles helper and role="grid" for editable table (#1365)
Browse files Browse the repository at this point in the history
  • Loading branch information
pan-kot authored Jul 27, 2023
1 parent da6bf84 commit 4b2c67b
Show file tree
Hide file tree
Showing 12 changed files with 159 additions and 28 deletions.
1 change: 1 addition & 0 deletions pages/table/inline-editor.permutations.page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export default function InlineEditorPermutations() {
wrapLines={false}
columnId="id"
stickyState={stickyState}
tableRole="grid"
{...permutation}
/>
</tr>
Expand Down
27 changes: 24 additions & 3 deletions src/table/__tests__/a11y.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import liveRegionStyles from '../../../lib/components/internal/components/live-r
interface Item {
id: number;
name: string;
value: string;
}

const defaultColumns: TableProps.ColumnDefinition<Item>[] = [
Expand All @@ -18,9 +19,9 @@ const defaultColumns: TableProps.ColumnDefinition<Item>[] = [
];

const defaultItems: Item[] = [
{ id: 1, name: 'Apples' },
{ id: 2, name: 'Oranges' },
{ id: 3, name: 'Bananas' },
{ id: 1, name: 'Apples', value: 'apples' },
{ id: 2, name: 'Oranges', value: 'oranges' },
{ id: 3, name: 'Bananas', value: 'bananas' },
];

function renderTableWrapper(props?: Partial<TableProps>) {
Expand All @@ -34,6 +35,26 @@ afterAll(() => {
jest.restoreAllMocks();
});

describe('roles', () => {
test('table has role="table" when no columns are editable', () => {
const wrapper = renderTableWrapper({ columnDefinitions: defaultColumns });
expect(wrapper.find('[role="table"]')).not.toBeNull();
expect(wrapper.find('[role="grid"]')).toBeNull();
});

test('table has role="grid" when at least one defined column is editable', () => {
const wrapper = renderTableWrapper({
columnDefinitions: [
...defaultColumns,
{ header: 'value', cell: item => item.value, editConfig: { editingCell: () => null } },
],
visibleColumns: ['id', 'name'],
});
expect(wrapper.find('[role="table"]')).toBeNull();
expect(wrapper.find('[role="grid"]')).not.toBeNull();
});
});

describe('labels', () => {
test('not to have aria-label if omitted', () => {
const wrapper = renderTableWrapper();
Expand Down
4 changes: 4 additions & 0 deletions src/table/__tests__/body-cell.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { renderHook } from '../../__tests__/render-hook';
import { useStickyColumns } from '../../../lib/components/table/sticky-columns';
import styles from '../../../lib/components/table/body-cell/styles.selectors.js';

const tableRole = 'table';

const testItem = {
test: 'testData',
};
Expand Down Expand Up @@ -59,6 +61,7 @@ const TestComponent = ({ isEditing = false, successfulEdit = false }) => {
stickyState={result.current}
successfulEdit={successfulEdit}
columnId="id"
tableRole={tableRole}
/>
</tr>
</tbody>
Expand Down Expand Up @@ -91,6 +94,7 @@ const TestComponent2 = ({ column }: any) => {
wrapLines={false}
stickyState={result.current}
columnId="id"
tableRole={tableRole}
/>
</tr>
</tbody>
Expand Down
5 changes: 5 additions & 0 deletions src/table/__tests__/header-cell.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import resizerStyles from '../../../lib/components/table/resizer/styles.css.js';
import { useStickyColumns } from '../../../lib/components/table/sticky-columns';
import TestI18nProvider from '../../../lib/components/i18n/testing';

const tableRole = 'table';

const testItem = {
test: 'test',
};
Expand Down Expand Up @@ -58,6 +60,7 @@ it('renders a fake focus outline on the sort control', () => {
stickyState={result.current}
columnId="id"
cellRef={() => {}}
tableRole={tableRole}
/>
</TableWrapper>
);
Expand All @@ -82,6 +85,7 @@ it('renders a fake focus outline on the resize control', () => {
stickyState={result.current}
columnId="id"
cellRef={() => {}}
tableRole={tableRole}
/>
</TableWrapper>
);
Expand All @@ -108,6 +112,7 @@ describe('i18n', () => {
columnId="id"
isEditable={true}
cellRef={() => {}}
tableRole={tableRole}
/>
</TableWrapper>
</TestI18nProvider>
Expand Down
16 changes: 7 additions & 9 deletions src/table/body-cell/td-element.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import clsx from 'clsx';
import React from 'react';
import styles from './styles.css.js';
import { getStickyClassNames } from '../utils';
import { StickyColumnsModel, useStickyCellStyles } from '../sticky-columns/use-sticky-columns.js';
import { StickyColumnsModel, useStickyCellStyles } from '../sticky-columns';
import { TableRole, getTableCellRoleProps } from '../table-role/table-role-helper.js';

export interface TableTdElementProps {
className?: string;
Expand All @@ -31,6 +32,7 @@ export interface TableTdElementProps {
columnId: PropertyKey;
stickyState: StickyColumnsModel;
isVisualRefresh?: boolean;
tableRole: TableRole;
}

export const TableTdElement = React.forwardRef<HTMLTableCellElement, TableTdElementProps>(
Expand All @@ -57,17 +59,13 @@ export const TableTdElement = React.forwardRef<HTMLTableCellElement, TableTdElem
hasFooter,
columnId,
stickyState,
tableRole,
},
ref
) => {
let Element: 'th' | 'td' = 'td';
if (isRowHeader) {
Element = 'th';
nativeAttributes = {
...nativeAttributes,
scope: 'row',
};
}
const Element = isRowHeader ? 'th' : 'td';

nativeAttributes = { ...nativeAttributes, ...getTableCellRoleProps({ tableRole, isRowHeader }) };

const stickyStyles = useStickyCellStyles({
stickyColumns: stickyState,
Expand Down
7 changes: 4 additions & 3 deletions src/table/header-cell/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import React from 'react';
import InternalIcon from '../../icon/internal';
import { KeyCode } from '../../internal/keycode';
import { TableProps } from '../interfaces';
import { getAriaSort, getSortingIconName, getSortingStatus, isSorted } from './utils';
import { getSortingIconName, getSortingStatus, isSorted } from './utils';
import styles from './styles.css.js';
import { Resizer } from '../resizer';
import { useUniqueId } from '../../internal/hooks/use-unique-id';
Expand All @@ -14,6 +14,7 @@ import { getStickyClassNames } from '../utils';
import { useInternalI18n } from '../../i18n/context';
import { StickyColumnsModel, useStickyCellStyles } from '../sticky-columns';
import { useMergeRefs } from '../../internal/hooks/use-merge-refs';
import { TableRole, getTableColHeaderRoleProps } from '../table-role';

interface TableHeaderCellProps<ItemType> {
className?: string;
Expand All @@ -38,6 +39,7 @@ interface TableHeaderCellProps<ItemType> {
cellRef: React.RefCallback<HTMLElement>;
focusedComponent?: InteractiveComponent | null;
onFocusedComponentChange?: (element: InteractiveComponent | null) => void;
tableRole: TableRole;
}

export function TableHeaderCell<ItemType>({
Expand Down Expand Up @@ -108,10 +110,9 @@ export function TableHeaderCell<ItemType>({
},
stickyStyles.className
)}
aria-sort={sortingStatus && getAriaSort(sortingStatus)}
style={{ ...style, ...stickyStyles.style }}
scope="col"
ref={mergedRef}
{...getTableColHeaderRoleProps({ sortingStatus })}
>
<div
className={clsx(styles['header-cell-content'], {
Expand Down
6 changes: 0 additions & 6 deletions src/table/header-cell/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,6 @@ const stateToIcon = {
ascending: 'caret-up-filled',
descending: 'caret-down-filled',
} as const;
const stateToAriaSort = {
sortable: 'none',
ascending: 'ascending',
descending: 'descending',
} as const;

export const getSortingStatus = (
sortable: boolean,
Expand All @@ -33,7 +28,6 @@ export const getSortingStatus = (
};

export const getSortingIconName = (sortingState: SortingStatus) => stateToIcon[sortingState];
export const getAriaSort = (sortingState: SortingStatus) => stateToAriaSort[sortingState];
export const isSorted = <T>(column: TableProps.ColumnDefinition<T>, sortingColumn: TableProps.SortingColumn<T>) =>
column === sortingColumn ||
(column.sortingField !== undefined && column.sortingField === sortingColumn.sortingField) ||
Expand Down
16 changes: 10 additions & 6 deletions src/table/internal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import { StickyScrollbar } from './sticky-scrollbar';
import { checkColumnWidths } from './column-widths-utils';
import { useMobile } from '../internal/hooks/use-mobile';
import { useContainerQuery } from '@cloudscape-design/component-toolkit';
import { getTableRoleProps, getTableRowRoleProps } from './table-role';

const SELECTION_COLUMN_WIDTH = 54;
const selectionColumnId = Symbol('selection-column-id');
Expand Down Expand Up @@ -176,6 +177,9 @@ const InternalTable = React.forwardRef(
stickyColumnsLast: stickyColumns?.last || 0,
});

const hasEditableCells = !!columnDefinitions.find(col => col.editConfig);
const tableRole = hasEditableCells ? 'grid' : 'table';

const theadProps: TheadProps = {
containerWidth,
selectionType,
Expand All @@ -202,6 +206,7 @@ const InternalTable = React.forwardRef(
stripedRows,
stickyState,
selectionColumnId,
tableRole,
};

const wrapperRef = useMergeRefs(wrapperMeasureRef, wrapperRefObject, stickyState.refs.wrapper);
Expand Down Expand Up @@ -269,6 +274,7 @@ const InternalTable = React.forwardRef(
onScroll={handleScroll}
tableHasHeader={hasHeader}
contentDensity={contentDensity}
tableRole={tableRole}
/>
)}
</>
Expand Down Expand Up @@ -315,11 +321,7 @@ const InternalTable = React.forwardRef(
resizableColumns && styles['table-layout-fixed'],
contentDensity === 'compact' && getVisualContextClassname('compact-table')
)}
// Browsers have weird mechanism to guess whether it's a data table or a layout table.
// If we state explicitly, they get it always correctly even with low number of rows.
role="table"
aria-label={ariaLabels?.tableLabel}
aria-rowcount={totalItemsCount ? totalItemsCount + 1 : -1}
{...getTableRoleProps({ tableRole, totalItemsCount, ariaLabel: ariaLabels?.tableLabel })}
>
<Thead
ref={theadRef}
Expand Down Expand Up @@ -374,7 +376,7 @@ const InternalTable = React.forwardRef(
{...focusMarkers.item}
onClick={onRowClickHandler && onRowClickHandler.bind(null, rowIndex, item)}
onContextMenu={onRowContextMenuHandler && onRowContextMenuHandler.bind(null, rowIndex, item)}
aria-rowindex={firstIndex ? firstIndex + rowIndex + 1 : undefined}
{...getTableRowRoleProps({ tableRole, firstIndex, rowIndex })}
>
{selectionType !== undefined && (
<TableTdElement
Expand All @@ -392,6 +394,7 @@ const InternalTable = React.forwardRef(
hasFooter={hasFooter}
stickyState={stickyState}
columnId={selectionColumnId}
tableRole={tableRole}
>
<SelectionControl
onFocusDown={moveFocusDown}
Expand Down Expand Up @@ -454,6 +457,7 @@ const InternalTable = React.forwardRef(
columnId={column.id ?? colIndex}
stickyState={stickyState}
isVisualRefresh={isVisualRefresh}
tableRole={tableRole}
/>
);
})}
Expand Down
5 changes: 4 additions & 1 deletion src/table/sticky-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import Thead, { InteractiveComponent, TheadProps } from './thead';
import { useStickyHeader } from './use-sticky-header';
import styles from './styles.css.js';
import { getVisualContextClassname } from '../internal/components/visual-context';
import { TableRole, getTableRoleProps } from './table-role';

export interface StickyHeaderRef {
scrollToTop(): void;
Expand All @@ -25,6 +26,7 @@ interface StickyHeaderProps {
onScroll?: React.UIEventHandler<HTMLDivElement>;
contentDensity?: 'comfortable' | 'compact';
tableHasHeader?: boolean;
tableRole: TableRole;
}

export default forwardRef(StickyHeader);
Expand All @@ -40,6 +42,7 @@ function StickyHeader(
tableRef,
tableHasHeader,
contentDensity,
tableRole,
}: StickyHeaderProps,
ref: React.Ref<StickyHeaderRef>
) {
Expand Down Expand Up @@ -81,8 +84,8 @@ function StickyHeader(
styles['table-layout-fixed'],
contentDensity === 'compact' && getVisualContextClassname('compact-table')
)}
role="table"
ref={secondaryTableRef}
{...getTableRoleProps({ tableRole })}
>
<Thead
ref={secondaryTheadRef}
Expand Down
10 changes: 10 additions & 0 deletions src/table/table-role/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

export {
TableRole,
getTableCellRoleProps,
getTableColHeaderRoleProps,
getTableRoleProps,
getTableRowRoleProps,
} from './table-role-helper';
Loading

0 comments on commit 4b2c67b

Please sign in to comment.