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

TransferList.Toolbar implements toolbar pattern #2274

Merged
merged 12 commits into from
Oct 4, 2024
5 changes: 5 additions & 0 deletions .changeset/beige-lions-swim.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@itwin/itwinui-react': patch
---

`TransferList.Toolbar` implements the previously missing [toolbar pattern](https://www.w3.org/WAI/ARIA/apg/patterns/toolbar/), including the arrow-key navigation functionality.
5 changes: 5 additions & 0 deletions .changeset/orange-cats-mix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@itwin/itwinui-react': patch
---

`IconButton`s inside `TransferList.Toolbar` will now show tooltips on the right side by default to avoid obscuring adjacent buttons in the group. This placement can be changed using the `labelProps.placement` prop on the `IconButton`.
Binary file modified ...act-workshop/cypress-visual-screenshots/baseline/TransferList.test.ts-Basic.png
100644 → 100755
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,21 @@ it('should render in its most basic state', () => {
});
});

it('should handle keyboard navigation', () => {
it('should render Toolbar in its most basic state', () => {
const { container } = render(
<TransferList>
<TransferList.Toolbar>
<Button />
</TransferList.Toolbar>
</TransferList>,
);

const toolbar = container.querySelector('.iui-transfer-list-toolbar');
expect(toolbar).toBeTruthy();
expect(toolbar).toHaveAttribute('role', 'toolbar');
});

it('should handle keyboard navigation in Listbox', () => {
const { container } = render(
<TransferList>
<TransferList.ListboxWrapper>
Expand Down Expand Up @@ -105,7 +119,7 @@ it('should handle keyboard navigation', () => {
});
});

it('should handle key presses', async () => {
it('should handle key presses in Listbox', async () => {
const mockedOnClick = vi.fn();
render(
<TransferList>
Expand Down
18 changes: 15 additions & 3 deletions packages/itwinui-react/src/core/TransferList/TransferList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type { PolymorphicForwardRefComponent } from '../../utils/index.js';
import { List } from '../List/List.js';
import { ListItem } from '../List/ListItem.js';
import { Label } from '../Label/Label.js';
import { ButtonGroup } from '../ButtonGroup/ButtonGroup.js';

// ----------------------------------------------------------------------------
// TransferListComponent
Expand Down Expand Up @@ -226,9 +227,20 @@ if (process.env.NODE_ENV === 'development') {
// ----------------------------------------------------------------------------
// TransferList.Toolbar component

const TransferListToolbar = polymorphic.div('iui-transfer-list-toolbar', {
role: 'toolbar',
});
const TransferListToolbar = React.forwardRef((props, ref) => {
const { className, children, ...rest } = props;
return (
<ButtonGroup
role='toolbar'
ref={ref}
{...rest}
orientation='vertical'
className={cx('iui-transfer-list-toolbar', className)}
>
{children}
</ButtonGroup>
);
}) as PolymorphicForwardRefComponent<'div', object>;
if (process.env.NODE_ENV === 'development') {
TransferListToolbar.displayName = 'TransferList.Toolbar';
}
Expand Down
5 changes: 5 additions & 0 deletions testing/e2e/app/routes/TransferList/route.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { TransferListMainExample } from 'examples';

export default function Page() {
return <TransferListMainExample />;
}
56 changes: 56 additions & 0 deletions testing/e2e/app/routes/TransferList/spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { test, expect } from '@playwright/test';

test.describe('toolbar pattern in TransferList.Toolbar', () => {
test('should support toolbar arrow-key keyboard navigation', async ({
page,
}) => {
await page.goto('/TransferList');
const listboxes = page.getByRole('listbox');
const toolbar = page.getByRole('toolbar');
const buttons = toolbar.first().locator('button');

await page.keyboard.press('Tab');
await expect(listboxes.first()).toBeFocused();

await page.keyboard.press('Tab');
await expect(buttons.nth(0)).toBeFocused();
await page.keyboard.press('ArrowDown');
await expect(buttons.nth(1)).toBeFocused();
await page.keyboard.press('ArrowDown');
await expect(buttons.nth(2)).toBeFocused();
await page.keyboard.press('ArrowDown');
await expect(buttons.nth(3)).toBeFocused();
await page.keyboard.press('ArrowDown');
await expect(buttons.nth(0)).toBeFocused();
await page.keyboard.press('ArrowUp');
await expect(buttons.nth(3)).toBeFocused();
});

test('should have only one tab stop and support Tab/Shift+Tab key navigation', async ({
page,
}) => {
await page.goto('/TransferList');
const listboxes = page.getByRole('listbox');
const toolbar = page.getByRole('toolbar');
const buttons = toolbar.first().locator('button');

await page.keyboard.press('Tab');
await expect(listboxes.first()).toBeFocused();

await page.keyboard.press('Tab');
await expect(buttons.first()).toBeFocused();

await page.keyboard.press('Tab');
await expect(listboxes.last()).toBeFocused();

await page.keyboard.down('Shift');
await page.keyboard.press('Tab');
await page.keyboard.up('Shift');
await expect(buttons.first()).toBeFocused();

await page.keyboard.down('Shift');
await page.keyboard.press('Tab');
await page.keyboard.up('Shift');
await expect(listboxes.first()).toBeFocused();
});
});
Loading