diff --git a/.changeset/beige-lions-swim.md b/.changeset/beige-lions-swim.md
new file mode 100644
index 00000000000..bb6aba438c6
--- /dev/null
+++ b/.changeset/beige-lions-swim.md
@@ -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.
diff --git a/.changeset/orange-cats-mix.md b/.changeset/orange-cats-mix.md
new file mode 100644
index 00000000000..607aa047fdf
--- /dev/null
+++ b/.changeset/orange-cats-mix.md
@@ -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`.
diff --git a/apps/react-workshop/cypress-visual-screenshots/baseline/TransferList.test.ts-Basic.png b/apps/react-workshop/cypress-visual-screenshots/baseline/TransferList.test.ts-Basic.png
old mode 100644
new mode 100755
index 85c00a332a1..5c16c4b70f0
Binary files a/apps/react-workshop/cypress-visual-screenshots/baseline/TransferList.test.ts-Basic.png and b/apps/react-workshop/cypress-visual-screenshots/baseline/TransferList.test.ts-Basic.png differ
diff --git a/apps/react-workshop/cypress-visual-screenshots/baseline/TransferList.test.ts-With Label.png b/apps/react-workshop/cypress-visual-screenshots/baseline/TransferList.test.ts-With Label.png
index 71bf6c53c82..5954f862e2b 100755
Binary files a/apps/react-workshop/cypress-visual-screenshots/baseline/TransferList.test.ts-With Label.png and b/apps/react-workshop/cypress-visual-screenshots/baseline/TransferList.test.ts-With Label.png differ
diff --git a/packages/itwinui-react/src/core/TransferList/TransferList.test.tsx b/packages/itwinui-react/src/core/TransferList/TransferList.test.tsx
index 3eebb502c10..8946a867469 100644
--- a/packages/itwinui-react/src/core/TransferList/TransferList.test.tsx
+++ b/packages/itwinui-react/src/core/TransferList/TransferList.test.tsx
@@ -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(
+
+
+
+
+ ,
+ );
+
+ 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(
@@ -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(
diff --git a/packages/itwinui-react/src/core/TransferList/TransferList.tsx b/packages/itwinui-react/src/core/TransferList/TransferList.tsx
index 00b7eb080dd..d6827fb1c9d 100644
--- a/packages/itwinui-react/src/core/TransferList/TransferList.tsx
+++ b/packages/itwinui-react/src/core/TransferList/TransferList.tsx
@@ -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
@@ -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 (
+
+ {children}
+
+ );
+}) as PolymorphicForwardRefComponent<'div', object>;
if (process.env.NODE_ENV === 'development') {
TransferListToolbar.displayName = 'TransferList.Toolbar';
}
diff --git a/testing/e2e/app/routes/TransferList/route.tsx b/testing/e2e/app/routes/TransferList/route.tsx
new file mode 100644
index 00000000000..86185e90fda
--- /dev/null
+++ b/testing/e2e/app/routes/TransferList/route.tsx
@@ -0,0 +1,5 @@
+import { TransferListMainExample } from 'examples';
+
+export default function Page() {
+ return ;
+}
diff --git a/testing/e2e/app/routes/TransferList/spec.ts b/testing/e2e/app/routes/TransferList/spec.ts
new file mode 100644
index 00000000000..374faea352e
--- /dev/null
+++ b/testing/e2e/app/routes/TransferList/spec.ts
@@ -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();
+ });
+});