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

chore: Table grid navigation (no integration) #1389

Merged
merged 64 commits into from
Aug 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
3b34c90
WIP: table grid navigation index.ts and custom page
pan-kot Jul 28, 2023
d2cd17f
WIP: grid navigation API design
pan-kot Jul 28, 2023
565c056
Make grid wrapper focusable by default
pan-kot Jul 28, 2023
13fbd7d
Update test page to feature more focusable elements
pan-kot Jul 28, 2023
473628e
Add delete and duplicate actions to test page
pan-kot Jul 28, 2023
9924fd6
add update action to the test page
pan-kot Jul 28, 2023
95da66b
Use table role helper in test page
pan-kot Jul 28, 2023
4ede126
Create focus traps
pan-kot Jul 28, 2023
97ed608
cancel tab traps for now
pan-kot Jul 28, 2023
b74a3c6
Added tabIndex=-1 to grid cells
pan-kot Jul 28, 2023
20ccfec
Support col-index for grid tables
pan-kot Jul 28, 2023
fe1d5f6
consistent row-index and col-index for grids
pan-kot Jul 28, 2023
d1c9297
remove table wrapper focus for grids
pan-kot Jul 28, 2023
d7a68c7
refactor names and interfaces
pan-kot Jul 28, 2023
dd60a22
remove cell locks
pan-kot Jul 28, 2023
b1b4d74
remove rows and column args
pan-kot Jul 28, 2023
f5745ca
cell focus outline for test page
pan-kot Jul 28, 2023
0adb050
cell keyboard navigation
pan-kot Jul 28, 2023
9977cea
refactor utils structure
pan-kot Jul 28, 2023
9f70d21
getContainer -> getTable
pan-kot Jul 28, 2023
2680e6a
observe removed nodes
pan-kot Jul 28, 2023
53afb20
consider element index
pan-kot Jul 28, 2023
74c8ca9
in-page navigation
pan-kot Jul 28, 2023
b7fd4ac
fix rows mutation
pan-kot Jul 28, 2023
110243d
mute tests
pan-kot Jul 28, 2023
348c2c4
Revert "fix rows mutation"
pan-kot Jul 28, 2023
22420c8
custom page sorting
pan-kot Jul 31, 2023
fbaa764
fix mutation bug
pan-kot Jul 31, 2023
ca2a0c3
non-interactive cell focus outline
pan-kot Jul 31, 2023
014e6a1
Make table first/last cells always focusable
pan-kot Jul 31, 2023
414bc14
widget cells support
pan-kot Jul 31, 2023
f77af8d
Fix table exit
pan-kot Jul 31, 2023
d438d0b
remove unneeded api
pan-kot Jul 31, 2023
0fe721a
Fix focusable index direction
pan-kot Jul 31, 2023
1597cba
retrieve tests
pan-kot Jul 31, 2023
c7ef55e
refactor role helper
pan-kot Jul 31, 2023
2bd6a7d
refactor use grid navigation
pan-kot Jul 31, 2023
3a90e99
fix tests
pan-kot Jul 31, 2023
9d7b9ca
basic tests for table row helper
pan-kot Jul 31, 2023
2bfbbcf
grid-nav unit tests
pan-kot Jul 31, 2023
7b3f192
add integration tests
pan-kot Jul 31, 2023
e228e08
Merge branch 'main' into table-grid-navigation
pan-kot Jul 31, 2023
21a88e2
Focus multi-element cells as cells
pan-kot Aug 4, 2023
86dfed7
Add notes
pan-kot Aug 4, 2023
75b5179
single tab-stop table
pan-kot Aug 4, 2023
342c3de
fix focusin/focusout
pan-kot Aug 14, 2023
eabbb85
Merge branch 'main' of github.com:cloudscape-design/components into t…
pan-kot Aug 14, 2023
a517e5f
default focus outlines
pan-kot Aug 14, 2023
4fed53f
refactor code
pan-kot Aug 14, 2023
863f82b
refactor code
pan-kot Aug 14, 2023
0db3d1e
Fix observer focus
pan-kot Aug 14, 2023
9382171
fix unit tests
pan-kot Aug 14, 2023
eb50b9e
update integ tests
pan-kot Aug 14, 2023
4970e7a
fix element index
pan-kot Aug 14, 2023
d0fbdac
fix unit tests
pan-kot Aug 14, 2023
d364d85
fix focusing behavior
pan-kot Aug 14, 2023
6a2d934
fix unit test
pan-kot Aug 14, 2023
100318c
support corner cases
pan-kot Aug 14, 2023
0d25f43
increase test cov
pan-kot Aug 15, 2023
48c6644
increase test cov
pan-kot Aug 15, 2023
39e2045
revert jest config
pan-kot Aug 15, 2023
e2ec983
Merge branch 'main' of github.com:cloudscape-design/components into t…
pan-kot Aug 15, 2023
47bd747
fix test cov computation
pan-kot Aug 16, 2023
7aa0555
add grid nav docs
pan-kot Aug 17, 2023
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
7 changes: 6 additions & 1 deletion pages/app/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,21 @@ import Header from './components/header';
import StrictModeWrapper from './components/strict-mode-wrapper';
import AppContext, { AppContextProvider, parseQuery } from './app-context';

function isAppLayoutPage(pageId?: string) {
const appLayoutPages = ['app-layout', 'content-layout', 'grid-navigation-custom'];
return pageId !== undefined && appLayoutPages.some(match => pageId.includes(match));
}

function App() {
const {
mode,
pageId,
urlParams: { density, motionDisabled },
} = useContext(AppContext);

const isAppLayout = pageId !== undefined && (pageId.includes('app-layout') || pageId.includes('content-layout'));
// AppLayout already contains <main>
// Also, AppLayout pages should resemble the ConsoleNav 2.0 styles
const isAppLayout = isAppLayoutPage(pageId);
const ContentTag = isAppLayout ? 'div' : 'main';
const isMacOS = navigator.userAgent.toLowerCase().indexOf('macintosh') > -1;

Expand Down
273 changes: 273 additions & 0 deletions pages/table-fragments/grid-navigation-custom.page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React, { useContext, useMemo, useRef, useState } from 'react';
import SpaceBetween from '~components/space-between';
import {
AppLayout,
Button,
ColumnLayout,
Container,
ContentLayout,
FormField,
Header,
HelpPanel,
Icon,
Input,
Link,
SegmentedControl,
} from '~components';
import styles from './styles.scss';
import { id as generateId, generateItems, Instance } from '../table/generate-data';
import AppContext, { AppContextType } from '../app/app-context';
import {
getTableCellRoleProps,
getTableColHeaderRoleProps,
getTableHeaderRowRoleProps,
getTableRoleProps,
getTableRowRoleProps,
getTableWrapperRoleProps,
useGridNavigation,
} from '~components/table/table-role';
import { orderBy } from 'lodash';
import appLayoutLabels from '../app-layout/utils/labels';

type PageContext = React.Context<
AppContextType<{
pageSize: number;
}>
>;

const createColumnDefinitions = ({
onDelete,
onDuplicate,
onUpdate,
}: {
onDelete: (id: string) => void;
onDuplicate: (id: string) => void;
onUpdate: (id: string) => void;
}) => [
{
key: 'id',
label: 'ID',
render: (item: Instance) => item.id,
},
{
key: 'actions',
label: 'Actions',
render: (item: Instance) => (
<div style={{ display: 'flex', gap: '8px', flexWrap: 'nowrap' }}>
<Button variant="inline-icon" iconName="remove" ariaLabel="Delete item" onClick={() => onDelete(item.id)} />
<Button variant="inline-icon" iconName="copy" ariaLabel="Duplicate item" onClick={() => onDuplicate(item.id)} />
<Button variant="inline-icon" iconName="refresh" ariaLabel="Update item" onClick={() => onUpdate(item.id)} />
</div>
),
},
{
key: 'state',
label: 'State',
render: (item: Instance) => item.state,
},
{
key: 'imageId',
label: 'Image ID',
render: (item: Instance) => <Link>{item.imageId}</Link>,
},
{
key: 'state-toggle',
label: 'State toggle',
render: (item: Instance) => (
<SegmentedControl
selectedId={item.state === 'RUNNING' || item.state === 'STOPPING' ? 'On' : 'Off'}
onChange={event => alert(`Changed item state to "${event.detail.selectedId}"`)}
label="Instance state"
options={[
{ text: 'On', id: 'On' },
{ text: 'Off', id: 'Off' },
]}
/>
),
isWidget: true,
},
{ key: 'dnsName', label: 'DNS name', render: (item: Instance) => item.dnsName ?? '?' },
{ key: 'dnsName2', label: 'DNS name 2', render: (item: Instance) => (item.dnsName ?? '?') + ':2' },
{ key: 'dnsName3', label: 'DNS name 3', render: (item: Instance) => (item.dnsName ?? '?') + ':3' },
{ key: 'type', label: 'Type', render: (item: Instance) => item.type },
];

export default function Page() {
const [toolsOpen, setToolsOpen] = useState(false);
const { urlParams, setUrlParams } = useContext(AppContext as PageContext);
const pageSize = urlParams.pageSize ?? 10;

const [items, setItems] = useState(generateItems(25));
const columnDefinitions = useMemo(
() =>
createColumnDefinitions({
onDelete: (id: string) => setItems(prev => prev.filter(item => item.id !== id)),
onDuplicate: (id: string) =>
setItems(prev => prev.flatMap(item => (item.id !== id ? [item] : [item, { ...item, id: generateId() }]))),
onUpdate: (id: string) =>
setItems(prev => prev.map(item => (item.id !== id ? item : { ...item, id: generateId() }))),
}),
[]
);

const [sortingKey, setSortingKey] = useState<null | string>(null);
const [sortingDirection, setSortingDirection] = useState<1 | -1>(1);

const tableRef = useRef<HTMLTableElement>(null);

const tableRole = 'grid';
useGridNavigation({ tableRole, pageSize, getTable: () => tableRef.current });

const sortedItems = useMemo(() => {
if (!sortingKey) {
return items;
}
return orderBy(items, [sortingKey], [sortingDirection === -1 ? 'desc' : 'asc']);
}, [items, sortingKey, sortingDirection]);

return (
<AppLayout
ariaLabels={appLayoutLabels}
contentType="table"
navigationHide={true}
toolsOpen={toolsOpen}
onToolsChange={event => setToolsOpen(event.detail.open)}
tools={<GridNavigationHelpPanel />}
content={
<ContentLayout header={<Header variant="h1">Grid navigation with a custom table grid</Header>}>
<Container
disableContentPaddings={true}
header={
<SpaceBetween size="m">
<ColumnLayout columns={3}>
<FormField label="Page size">
<Input
type="number"
value={pageSize.toString()}
onChange={event => setUrlParams({ pageSize: parseInt(event.detail.value) })}
/>
</FormField>
</ColumnLayout>

<Link onFollow={() => setToolsOpen(true)} data-testid="link-before">
How to use grid navigation?
</Link>
</SpaceBetween>
}
footer={
<Link onFollow={() => setToolsOpen(true)} data-testid="link-after">
How to use grid navigation?
</Link>
}
>
<div className={styles['custom-table']} {...getTableWrapperRoleProps({ tableRole, isScrollable: false })}>
<table
ref={tableRef}
className={styles['custom-table-table']}
{...getTableRoleProps({ tableRole, totalItemsCount: items.length })}
>
<thead>
<tr {...getTableHeaderRowRoleProps({ tableRole })}>
{columnDefinitions.map((column, colIndex) => (
<th
key={column.key}
className={styles['custom-table-cell']}
{...getTableColHeaderRoleProps({ tableRole, colIndex })}
>
<div style={{ display: 'flex', gap: '8px', flexWrap: 'nowrap' }}>
<button
className={styles['custom-table-sorting-header']}
onClick={() => {
if (sortingKey !== column.key) {
setSortingKey(column.key);
setSortingDirection(-1);
} else {
setSortingDirection(prev => (prev === 1 ? -1 : 1));
}
}}
>
{column.label}
</button>
{sortingKey === column.key && sortingDirection === -1 && <Icon name="angle-down" />}
{sortingKey === column.key && sortingDirection === 1 && <Icon name="angle-up" />}
</div>
</th>
))}
</tr>
</thead>
<tbody>
{sortedItems.map((item, rowIndex) => (
<tr key={item.id} {...getTableRowRoleProps({ tableRole, rowIndex, firstIndex: 0 })}>
{columnDefinitions.map((column, colIndex) => (
<td
key={column.key}
className={styles['custom-table-cell']}
{...getTableCellRoleProps({ tableRole, colIndex })}
data-widget-cell={column.isWidget}
>
{column.render(item)}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
</Container>
</ContentLayout>
}
/>
);
}

function GridNavigationHelpPanel() {
return (
<HelpPanel header={<Header variant="h2">Grid navigation</Header>}>
<p>
Grid tables offer better efficient navigation for keyboard users. The navigation intercepts keyboard commands to
focus table cells and focusable cell content using arrow keys and other key combinations. Here is the full list
of commands to move item focus:
</p>
<ul>
<li>
<b>Arrow Up</b> (one item up)
</li>
<li>
<b>Arrow Down</b> (one item down)
</li>
<li>
<b>Arrow Left</b> (one item to the left)
</li>
<li>
<b>Arrow Right</b> (one item to the right)
</li>
<li>
<b>Page Up</b> (one page up)
</li>
<li>
<b>Page Down</b> (one page down)
</li>
<li>
<b>Home</b> (to the first item in the row)
</li>
<li>
<b>End</b> (to the last item in the row)
</li>
<li>
<b>Control+Home</b> (to the first item in the grid)
</li>
<li>
<b>Control+End</b> (to the last item in the grid)
</li>
<li>
<b>Enter</b> (to move focus inside widget cell)
</li>
<li>
<b>Escape</b> (to move widget focus back to cell)
</li>
</ul>
</HelpPanel>
);
}
9 changes: 9 additions & 0 deletions pages/table-fragments/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,13 @@
}
}
}

&-sorting-header {
padding: 0;
margin: 0;
background: none;
border: none;
font-weight: bold;
margin-left: 2px;
}
}
2 changes: 1 addition & 1 deletion pages/table/generate-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export interface Instance {
dnsName?: string;
}

function id() {
export function id() {
const id = Math.ceil(pseudoRandom() * Math.pow(16, 8)).toString(16);
return padStart(id, 8, '0');
}
Expand Down
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 @@ -66,6 +66,7 @@ export default function InlineEditorPermutations() {
onEditEnd={() => {}}
wrapLines={false}
columnId="id"
colIndex={0}
stickyState={stickyState}
tableRole="grid"
{...permutation}
Expand Down
2 changes: 2 additions & 0 deletions src/table/__tests__/body-cell.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ const TestComponent = ({ isEditing = false, successfulEdit = false }) => {
stickyState={result.current}
successfulEdit={successfulEdit}
columnId="id"
colIndex={0}
tableRole={tableRole}
/>
</tr>
Expand Down Expand Up @@ -94,6 +95,7 @@ const TestComponent2 = ({ column }: any) => {
wrapLines={false}
stickyState={result.current}
columnId="id"
colIndex={0}
tableRole={tableRole}
/>
</tr>
Expand Down
6 changes: 4 additions & 2 deletions src/table/body-cell/td-element.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import React from 'react';
import styles from './styles.css.js';
import { getStickyClassNames } from '../utils';
import { StickyColumnsModel, useStickyCellStyles } from '../sticky-columns';
import { TableRole, getTableCellRoleProps } from '../table-role/table-role-helper.js';
import { TableRole, getTableCellRoleProps } from '../table-role';

export interface TableTdElementProps {
className?: string;
Expand All @@ -30,6 +30,7 @@ export interface TableTdElementProps {
hasSelection?: boolean;
hasFooter?: boolean;
columnId: PropertyKey;
colIndex: number;
stickyState: StickyColumnsModel;
isVisualRefresh?: boolean;
tableRole: TableRole;
Expand Down Expand Up @@ -58,14 +59,15 @@ export const TableTdElement = React.forwardRef<HTMLTableCellElement, TableTdElem
hasSelection,
hasFooter,
columnId,
colIndex,
stickyState,
tableRole,
},
ref
) => {
const Element = isRowHeader ? 'th' : 'td';

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

const stickyStyles = useStickyCellStyles({
stickyColumns: stickyState,
Expand Down
3 changes: 2 additions & 1 deletion src/table/header-cell/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export function TableHeaderCell<ItemType>({
columnId,
stickyState,
cellRef,
tableRole,
}: TableHeaderCellProps<ItemType>) {
const i18n = useInternalI18n('table');
const sortable = !!column.sortingComparator || !!column.sortingField;
Expand Down Expand Up @@ -112,7 +113,7 @@ export function TableHeaderCell<ItemType>({
)}
style={{ ...style, ...stickyStyles.style }}
ref={mergedRef}
{...getTableColHeaderRoleProps({ sortingStatus })}
{...getTableColHeaderRoleProps({ tableRole, sortingStatus, colIndex })}
>
<div
className={clsx(styles['header-cell-content'], {
Expand Down
Loading
Loading