Skip to content

Commit

Permalink
refactor: Simpler grid navigation implementation (#1846)
Browse files Browse the repository at this point in the history
  • Loading branch information
pan-kot authored Jan 2, 2024
1 parent 998f48b commit 342874a
Show file tree
Hide file tree
Showing 9 changed files with 317 additions and 337 deletions.
45 changes: 33 additions & 12 deletions pages/table-fragments/grid-navigation-custom.page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React, { useContext, useEffect, useMemo, useRef, useState } from 'react';
import SpaceBetween from '~components/space-between';
import React, { useContext, useMemo, useRef, useState } from 'react';
import {
AppLayout,
Button,
ButtonDropdown,
Checkbox,
ColumnLayout,
Container,
ContentLayout,
Expand All @@ -17,8 +17,10 @@ import {
Link,
RadioGroup,
Select,
SpaceBetween,
StatusIndicator,
} from '~components';
import { useEffectOnUpdate } from '~components/internal/hooks/use-effect-on-update';
import styles from './styles.scss';
import { id as generateId, generateItems, Instance, InstanceState } from '../table/generate-data';
import AppContext, { AppContextType } from '../app/app-context';
Expand All @@ -36,11 +38,17 @@ import { orderBy, range } from 'lodash';
import appLayoutLabels from '../app-layout/utils/labels';
import { stateToStatusIndicator } from '../table/shared-configs';

interface ExtendedWindow extends Window {
refreshItems: () => void;
}
declare const window: ExtendedWindow;

type PageContext = React.Context<
AppContextType<{
pageSize: number;
tableRole: TableRole;
actionsMode: ActionsMode;
autoRefresh: boolean;
}>
>;

Expand All @@ -56,17 +64,19 @@ const actionsModeOptions = [
export default function Page() {
const [toolsOpen, setToolsOpen] = useState(false);
const { urlParams, setUrlParams } = useContext(AppContext as PageContext);
const pageSize = urlParams.pageSize ?? 10;
const tableRole = urlParams.tableRole ?? 'grid';
const actionsMode = urlParams.actionsMode ?? 'dropdown';
const { pageSize = 10, tableRole = 'grid', actionsMode = 'dropdown', autoRefresh = false } = urlParams;

const [items, setItems] = useState(generateItems(25));
const [refreshCounter, setRefreshCounter] = useState(0);
window.refreshItems = () => setRefreshCounter(prev => prev + 1);

useEffect(() => {
setInterval(() => {
setItems(prev => [...prev, ...generateItems(1)]);
}, 10000);
}, []);
useEffectOnUpdate(() => {
setItems(prev => [...prev.slice(1), ...generateItems(1)]);
if (autoRefresh) {
const timeoutId = setTimeout(() => setRefreshCounter(prev => prev + 1), 10000);
return () => clearTimeout(timeoutId);
}
}, [autoRefresh, refreshCounter]);

const columnDefinitions = [
{
Expand Down Expand Up @@ -174,6 +184,17 @@ export default function Page() {
</FormField>
</ColumnLayout>

<SpaceBetween alignItems="center" size="m" direction="horizontal">
<Checkbox
checked={autoRefresh}
onChange={event => setUrlParams({ autoRefresh: event.detail.checked })}
>
Auto-refresh every 10 seconds
</Checkbox>

<Button onClick={() => setRefreshCounter(prev => prev + 1)} iconName="refresh" ariaLabel="Refresh" />
</SpaceBetween>

<Link onFollow={() => setToolsOpen(true)} data-testid="link-before">
How to use grid navigation?
</Link>
Expand Down Expand Up @@ -368,8 +389,8 @@ function DnsEditCell({ item }: { item: Instance }) {
style={{ display: 'flex', gap: '8px', flexWrap: 'nowrap' }}
>
<Input autoFocus={true} value={value} onChange={event => setValue(event.detail.value)} />
<Button iconName="check" onClick={() => setActive(false)} />
<Button iconName="close" onClick={() => setActive(false)} />
<Button iconName="check" ariaLabel="Save" onClick={() => setActive(false)} />
<Button iconName="close" ariaLabel="Cancel" onClick={() => setActive(false)} />
</div>
);
}
Expand Down
73 changes: 57 additions & 16 deletions src/table/table-role/__integ__/grid-navigation.test.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,81 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import { BasePageObject } from '@cloudscape-design/browser-test-tools/page-objects';
import useBrowser from '@cloudscape-design/browser-test-tools/use-browser';
import createWrapper from '../../../../lib/components/test-utils/selectors';
import { GridNavigationPageObject } from './page-object';

test(
'cell action remains focused when row re-renders',
useBrowser({ width: 1800, height: 800 }, async browser => {
const page = new BasePageObject(browser);
await browser.url('#/light/table-fragments/grid-navigation-custom/?actionsMode=inline');
interface Options {
actionsMode?: 'dropdown' | 'inline';
}

const setupTest = (
{ actionsMode = 'dropdown' }: Options,
testFn: (page: GridNavigationPageObject) => Promise<void>
) => {
return useBrowser(async browser => {
const page = new GridNavigationPageObject(browser);
const query = new URLSearchParams({ actionsMode });
await browser.url(`#/light/table-fragments/grid-navigation-custom/?${query.toString()}`);
await page.waitForVisible('table');
await testFn(page);
});
};

test(
'cell action remains focused when row re-renders',
setupTest({ actionsMode: 'inline' }, async page => {
await page.click('button[aria-label="Update item"]');
await expect(page.isFocused('button[aria-label="Update item"]')).resolves.toBe(true);
})
);

test(
'cell focus stays in the same position when row gets removed',
useBrowser({ width: 1800, height: 800 }, async browser => {
const page = new BasePageObject(browser);
await browser.url('#/light/table-fragments/grid-navigation-custom/?actionsMode=inline');
await page.waitForVisible('table');

setupTest({ actionsMode: 'inline' }, async page => {
await page.click('button[aria-label="Delete item"]');
await expect(page.isFocused('button[aria-label="Delete item"]')).resolves.toBe(true);
})
);

test(
'table has a single tab stop',
useBrowser({ width: 1800, height: 800 }, async browser => {
const page = new BasePageObject(browser);
await browser.url('#/light/table-fragments/grid-navigation-custom');
await page.waitForVisible('table');
'cell focus stays in the same position when row gets removed with auto-refresh',
setupTest({}, async page => {
const firstButtonDropdown = createWrapper().findButtonDropdown();
await page.click(firstButtonDropdown.toSelector());
await page.click(firstButtonDropdown.findHighlightedItem().toSelector());
await expect(page.isFocused(firstButtonDropdown.findNativeButton().toSelector())).resolves.toBe(true);

await page.refreshItems();
await expect(page.isFocused(firstButtonDropdown.findNativeButton().toSelector())).resolves.toBe(true);
})
);

test(
'keeps last focused cell position when row gets removed from outside table',
setupTest({}, async page => {
const firstButtonDropdown = createWrapper().findButtonDropdown();
await page.click(firstButtonDropdown.toSelector());
await page.click('button[aria-label="Refresh"]');
await expect(page.isFocused('button[aria-label="Refresh"]')).resolves.toBe(true);

await page.keys(['Tab', 'Tab']);
await expect(page.isFocused(firstButtonDropdown.findNativeButton().toSelector())).resolves.toBe(true);
})
);

test(
'retains cell focus when existing inline edit',
setupTest({}, async page => {
await page.click('[aria-label="Edit DNS name"]');
await page.click('button[aria-label="Save"]');
await expect(page.isFocused('[aria-label="Edit DNS name"]')).resolves.toBe(true);
})
);

test(
'table has a single tab stop',
setupTest({}, async page => {
await page.click('[data-testid="link-before"]');
await page.keys('Tab');
await expect(page.isFocused('tr[aria-rowindex="1"] > th[aria-colindex="1"] button')).resolves.toBe(true);
Expand Down
15 changes: 15 additions & 0 deletions src/table/table-role/__integ__/page-object.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import { BasePageObject } from '@cloudscape-design/browser-test-tools/page-objects';

interface ExtendedWindow extends Window {
refreshItems: () => void;
}
declare const window: ExtendedWindow;

export class GridNavigationPageObject extends BasePageObject {
async refreshItems() {
await this.browser.execute(() => window.refreshItems());
}
}
Loading

0 comments on commit 342874a

Please sign in to comment.