Toolbar
A container for grouping a set of controls, such as buttons, toggle buttons, or other input elements.
Demo
Text Formatting Toolbar
A horizontal toolbar with toggle buttons and regular buttons.
Vertical Toolbar
Use arrow up/down keys to navigate.
With Disabled Items
Disabled items are skipped during keyboard navigation.
Controlled Toggle Buttons
Toggle buttons with controlled state. The current state is displayed and applied to the sample text.
Current state: {"bold":false,"italic":false,"underline":false}
Default Pressed States
Toggle buttons with defaultPressed for initial state, including disabled states.
Accessibility
WAI-ARIA Roles
| Role | Target Element | Description |
|---|---|---|
toolbar | Container | Container for grouping controls |
button | Button elements | Implicit role for <button> elements |
separator | Separator | Visual and semantic separator between groups |
WAI-ARIA toolbar role (opens in new tab)
WAI-ARIA Properties
| Attribute | Target | Values | Required | Configuration |
|---|---|---|---|---|
aria-label | toolbar | String | Yes* | aria-label prop |
aria-labelledby | toolbar | ID reference | Yes* | aria-labelledby prop |
aria-orientation | toolbar | "horizontal" | "vertical" | No | orientation prop (default: horizontal) |
* Either aria-label or aria-labelledby is required
WAI-ARIA States
aria-pressed
Indicates the pressed state of toggle buttons.
| Target | ToolbarToggleButton |
| Values | true | false |
| Required | Yes (for toggle buttons) |
| Change Trigger | Click, Enter, Space |
| Reference | aria-pressed (opens in new tab) |
Keyboard Support
| Key | Action |
|---|---|
| Tab | Move focus into/out of the toolbar (single tab stop) |
| Arrow Right / Arrow Left | Navigate between controls (horizontal toolbar) |
| Arrow Down / Arrow Up | Navigate between controls (vertical toolbar) |
| Home | Move focus to first control |
| End | Move focus to last control |
| Enter / Space | Activate button / toggle pressed state |
Focus Management
This component uses the Roving Tabindex pattern for focus management:
- Only one control has
tabindex="0"at a time - Other controls have
tabindex="-1" - Arrow keys move focus between controls
- Disabled controls and separators are skipped
- Focus does not wrap (stops at edges)
Source Code
import { createContext, useCallback, useContext, useEffect, useRef, useState } from 'react';
/**
* Toolbar context for managing focus state
*/
interface ToolbarContextValue {
orientation: 'horizontal' | 'vertical';
}
// Default context value for SSR compatibility
const defaultContext: ToolbarContextValue = {
orientation: 'horizontal',
};
const ToolbarContext = createContext<ToolbarContextValue>(defaultContext);
function useToolbarContext() {
return useContext(ToolbarContext);
}
/**
* Props for the Toolbar component
* @see https://www.w3.org/WAI/ARIA/apg/patterns/toolbar/
*/
export interface ToolbarProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'role'> {
/** Direction of the toolbar */
orientation?: 'horizontal' | 'vertical';
/** Child elements (ToolbarButton, ToolbarToggleButton, ToolbarSeparator) */
children: React.ReactNode;
}
/**
* Toolbar container component implementing WAI-ARIA Toolbar pattern
*
* @example
* ```tsx
* <Toolbar aria-label="Text formatting">
* <ToolbarToggleButton>Bold</ToolbarToggleButton>
* <ToolbarToggleButton>Italic</ToolbarToggleButton>
* <ToolbarSeparator />
* <ToolbarButton>Copy</ToolbarButton>
* </Toolbar>
* ```
*/
export function Toolbar({
orientation = 'horizontal',
children,
className = '',
onKeyDown,
...props
}: ToolbarProps): React.ReactElement {
const toolbarRef = useRef<HTMLDivElement>(null);
const [focusedIndex, setFocusedIndex] = useState(0);
const getButtons = useCallback((): HTMLButtonElement[] => {
if (!toolbarRef.current) return [];
return Array.from(
toolbarRef.current.querySelectorAll<HTMLButtonElement>('button:not([disabled])')
);
}, []);
const handleKeyDown = useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
const buttons = getButtons();
if (buttons.length === 0) return;
const currentIndex = buttons.findIndex((btn) => btn === document.activeElement);
if (currentIndex === -1) return;
const nextKey = orientation === 'vertical' ? 'ArrowDown' : 'ArrowRight';
const prevKey = orientation === 'vertical' ? 'ArrowUp' : 'ArrowLeft';
const invalidKeys =
orientation === 'vertical' ? ['ArrowLeft', 'ArrowRight'] : ['ArrowUp', 'ArrowDown'];
// Ignore invalid direction keys
if (invalidKeys.includes(event.key)) {
return;
}
let newIndex = currentIndex;
let shouldPreventDefault = false;
switch (event.key) {
case nextKey:
// No wrap - stop at end
if (currentIndex < buttons.length - 1) {
newIndex = currentIndex + 1;
}
shouldPreventDefault = true;
break;
case prevKey:
// No wrap - stop at start
if (currentIndex > 0) {
newIndex = currentIndex - 1;
}
shouldPreventDefault = true;
break;
case 'Home':
newIndex = 0;
shouldPreventDefault = true;
break;
case 'End':
newIndex = buttons.length - 1;
shouldPreventDefault = true;
break;
}
if (shouldPreventDefault) {
event.preventDefault();
if (newIndex !== currentIndex) {
buttons[newIndex].focus();
setFocusedIndex(newIndex);
}
}
onKeyDown?.(event);
},
[orientation, getButtons, onKeyDown]
);
const handleFocus = useCallback(
(event: React.FocusEvent<HTMLDivElement>) => {
const { target } = event;
if (!(target instanceof HTMLButtonElement)) return;
const buttons = getButtons();
const targetIndex = buttons.findIndex((btn) => btn === target);
if (targetIndex !== -1) {
setFocusedIndex(targetIndex);
}
},
[getButtons]
);
// Roving tabindex: only the focused button should have tabIndex=0
useEffect(() => {
const buttons = getButtons();
if (buttons.length === 0) return;
// Clamp focusedIndex to valid range
const validIndex = Math.min(focusedIndex, buttons.length - 1);
if (validIndex !== focusedIndex) {
setFocusedIndex(validIndex);
return; // Will re-run with corrected index
}
buttons.forEach((btn, index) => {
btn.tabIndex = index === focusedIndex ? 0 : -1;
});
}, [focusedIndex, getButtons, children]);
return (
<ToolbarContext.Provider value={{ orientation }}>
<div
ref={toolbarRef}
role="toolbar"
aria-orientation={orientation}
className={`apg-toolbar ${className}`.trim()}
onKeyDown={handleKeyDown}
onFocus={handleFocus}
{...props}
>
{children}
</div>
</ToolbarContext.Provider>
);
}
/**
* Props for the ToolbarButton component
*/
export interface ToolbarButtonProps extends Omit<
React.ButtonHTMLAttributes<HTMLButtonElement>,
'type'
> {
/** Button content */
children: React.ReactNode;
}
/**
* Button component for use within a Toolbar
*/
export function ToolbarButton({
children,
className = '',
disabled,
...props
}: ToolbarButtonProps): React.ReactElement {
// Verify we're inside a Toolbar
useToolbarContext();
return (
<button
type="button"
className={`apg-toolbar-button ${className}`.trim()}
disabled={disabled}
{...props}
>
{children}
</button>
);
}
/**
* Props for the ToolbarToggleButton component
*/
export interface ToolbarToggleButtonProps extends Omit<
React.ButtonHTMLAttributes<HTMLButtonElement>,
'type' | 'aria-pressed'
> {
/** Controlled pressed state */
pressed?: boolean;
/** Default pressed state (uncontrolled) */
defaultPressed?: boolean;
/** Callback when pressed state changes */
onPressedChange?: (pressed: boolean) => void;
/** Button content */
children: React.ReactNode;
}
/**
* Toggle button component for use within a Toolbar
*/
export function ToolbarToggleButton({
pressed: controlledPressed,
defaultPressed = false,
onPressedChange,
children,
className = '',
disabled,
onClick,
...props
}: ToolbarToggleButtonProps): React.ReactElement {
// Verify we're inside a Toolbar
useToolbarContext();
const [internalPressed, setInternalPressed] = useState(defaultPressed);
const isControlled = controlledPressed !== undefined;
const pressed = isControlled ? controlledPressed : internalPressed;
const handleClick = useCallback(
(event: React.MouseEvent<HTMLButtonElement>) => {
if (disabled) return;
const newPressed = !pressed;
if (!isControlled) {
setInternalPressed(newPressed);
}
onPressedChange?.(newPressed);
onClick?.(event);
},
[disabled, pressed, isControlled, onPressedChange, onClick]
);
return (
<button
type="button"
aria-pressed={pressed}
className={`apg-toolbar-button ${className}`.trim()}
disabled={disabled}
onClick={handleClick}
{...props}
>
{children}
</button>
);
}
/**
* Props for the ToolbarSeparator component
*/
export interface ToolbarSeparatorProps {
/** Additional CSS class */
className?: string;
}
/**
* Separator component for use within a Toolbar
*/
export function ToolbarSeparator({ className = '' }: ToolbarSeparatorProps): React.ReactElement {
const { orientation } = useToolbarContext();
// Separator orientation is perpendicular to toolbar orientation
const separatorOrientation = orientation === 'horizontal' ? 'vertical' : 'horizontal';
return (
<div
role="separator"
aria-orientation={separatorOrientation}
className={`apg-toolbar-separator ${className}`.trim()}
/>
);
} Usage
import {
Toolbar,
ToolbarButton,
ToolbarToggleButton,
ToolbarSeparator
} from '@patterns/toolbar/Toolbar';
// Basic usage
<Toolbar aria-label="Text formatting">
<ToolbarToggleButton>Bold</ToolbarToggleButton>
<ToolbarToggleButton>Italic</ToolbarToggleButton>
<ToolbarSeparator />
<ToolbarButton>Copy</ToolbarButton>
<ToolbarButton>Paste</ToolbarButton>
</Toolbar>
// Vertical toolbar
<Toolbar orientation="vertical" aria-label="Actions">
<ToolbarButton>New</ToolbarButton>
<ToolbarButton>Open</ToolbarButton>
<ToolbarButton>Save</ToolbarButton>
</Toolbar>
// Controlled toggle button
const [isBold, setIsBold] = useState(false);
<Toolbar aria-label="Formatting">
<ToolbarToggleButton
pressed={isBold}
onPressedChange={setIsBold}
>
Bold
</ToolbarToggleButton>
</Toolbar> API
Toolbar Props
| Prop | Type | Default | Description |
|---|---|---|---|
orientation | 'horizontal' | 'vertical' | 'horizontal' | Direction of the toolbar |
aria-label | string | - | Accessible label for the toolbar |
children | React.ReactNode | - | Toolbar content |
ToolbarButton Props
| Prop | Type | Default | Description |
|---|---|---|---|
disabled | boolean | false | Whether the button is disabled |
onClick | () => void | - | Click handler |
ToolbarToggleButton Props
| Prop | Type | Default | Description |
|---|---|---|---|
pressed | boolean | - | Controlled pressed state |
defaultPressed | boolean | false | Initial pressed state (uncontrolled) |
onPressedChange | (pressed: boolean) => void | - | Callback when pressed state changes |
disabled | boolean | false | Whether the button is disabled |
Testing
Tests verify APG compliance across keyboard interaction, ARIA attributes, and accessibility requirements. The Toolbar component uses a two-layer testing strategy.
Testing Strategy
Unit Tests (Testing Library)
Verify the component's rendered output using framework-specific testing libraries. These tests ensure correct HTML structure and ARIA attributes.
- ARIA attributes (aria-pressed, aria-orientation)
- Keyboard interaction (Arrow keys, Home, End)
- Roving tabindex behavior
- Accessibility via jest-axe
E2E Tests (Playwright)
Verify component behavior in a real browser environment across all frameworks. These tests cover interactions and cross-framework consistency.
- Click interactions
- Arrow key navigation (horizontal and vertical)
- Toggle button state changes
- Disabled item handling
- ARIA structure validation in live browser
- axe-core accessibility scanning
- Cross-framework consistency checks
Test Categories
High Priority : APG Keyboard Interaction (Unit + E2E)
| Test | Description |
|---|---|
ArrowRight/Left | Moves focus between items (horizontal) |
ArrowDown/Up | Moves focus between items (vertical) |
Home | Moves focus to first item |
End | Moves focus to last item |
No wrap | Focus stops at edges (no looping) |
Disabled skip | Skips disabled items during navigation |
Enter/Space | Activates button or toggles toggle button |
High Priority : APG ARIA Attributes (Unit + E2E)
| Test | Description |
|---|---|
role="toolbar" | Container has toolbar role |
aria-orientation | Reflects horizontal/vertical orientation |
aria-label/labelledby | Toolbar has accessible name |
aria-pressed | Toggle buttons reflect pressed state |
role="separator" | Separator has correct role and orientation |
type="button" | Buttons have explicit type attribute |
High Priority : Focus Management - Roving Tabindex (Unit + E2E)
| Test | Description |
|---|---|
tabIndex=0 | First enabled item has tabIndex=0 |
tabIndex=-1 | Other items have tabIndex=-1 |
Click updates focus | Clicking an item updates roving focus position |
High Priority : Toggle Button State (Unit + E2E)
| Test | Description |
|---|---|
aria-pressed | Toggle button has aria-pressed attribute |
Click toggles | Clicking toggle button changes aria-pressed |
defaultPressed | Toggle with defaultPressed starts as pressed |
Medium Priority : Accessibility (Unit + E2E)
| Test | Description |
|---|---|
axe violations | No WCAG 2.1 AA violations (via jest-axe/axe-core) |
Vertical toolbar | Vertical orientation also passes axe |
Low Priority : Cross-framework Consistency (E2E)
| Test | Description |
|---|---|
All frameworks render | React, Vue, Svelte, Astro all render toolbars |
Consistent keyboard | All frameworks support keyboard navigation |
Consistent ARIA | All frameworks have consistent ARIA structure |
Toggle buttons | All frameworks support toggle button state |
Example E2E Test Code
The following is the actual E2E test file (e2e/toolbar.spec.ts).
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
/**
* E2E Tests for Toolbar Pattern
*
* A container for grouping a set of controls, such as buttons, toggle buttons,
* or menus. Toolbar uses roving tabindex for keyboard navigation.
*
* APG Reference: https://www.w3.org/WAI/ARIA/apg/patterns/toolbar/
*/
const frameworks = ['react', 'vue', 'svelte', 'astro'] as const;
// ============================================
// Helper Functions
// ============================================
const getToolbar = (page: import('@playwright/test').Page) => {
return page.getByRole('toolbar');
};
const getToolbarButtons = (page: import('@playwright/test').Page, toolbarIndex = 0) => {
const toolbar = getToolbar(page).nth(toolbarIndex);
return toolbar.getByRole('button');
};
const getSeparators = (page: import('@playwright/test').Page, toolbarIndex = 0) => {
const toolbar = getToolbar(page).nth(toolbarIndex);
return toolbar.getByRole('separator');
};
// ============================================
// Framework-specific Tests
// ============================================
for (const framework of frameworks) {
test.describe(`Toolbar (${framework})`, () => {
test.beforeEach(async ({ page }) => {
await page.goto(`patterns/toolbar/${framework}/demo/`);
await getToolbar(page).first().waitFor();
// Wait for hydration - first button should have tabindex="0"
const firstButton = getToolbarButtons(page, 0).first();
await expect
.poll(async () => {
const tabindex = await firstButton.getAttribute('tabindex');
return tabindex === '0';
})
.toBe(true);
});
// ------------------------------------------
// 🔴 High Priority: APG ARIA Structure
// ------------------------------------------
test.describe('APG: ARIA Structure', () => {
test('container has role="toolbar"', async ({ page }) => {
const toolbar = getToolbar(page).first();
await expect(toolbar).toHaveRole('toolbar');
});
test('toolbar has aria-label for accessible name', async ({ page }) => {
const toolbar = getToolbar(page).first();
const ariaLabel = await toolbar.getAttribute('aria-label');
expect(ariaLabel).toBeTruthy();
});
test('horizontal toolbar has aria-orientation="horizontal"', async ({ page }) => {
const toolbar = getToolbar(page).first();
await expect(toolbar).toHaveAttribute('aria-orientation', 'horizontal');
});
test('vertical toolbar has aria-orientation="vertical"', async ({ page }) => {
// Second toolbar is vertical
const toolbar = getToolbar(page).nth(1);
await expect(toolbar).toHaveAttribute('aria-orientation', 'vertical');
});
test('buttons have implicit role="button"', async ({ page }) => {
const buttons = getToolbarButtons(page, 0);
const count = await buttons.count();
expect(count).toBeGreaterThan(0);
for (let i = 0; i < Math.min(3, count); i++) {
await expect(buttons.nth(i)).toHaveRole('button');
}
});
test('separator has role="separator"', async ({ page }) => {
const separators = getSeparators(page, 0);
const count = await separators.count();
expect(count).toBeGreaterThan(0);
await expect(separators.first()).toHaveRole('separator');
});
test('separator in horizontal toolbar has aria-orientation="vertical"', async ({ page }) => {
const separator = getSeparators(page, 0).first();
await expect(separator).toHaveAttribute('aria-orientation', 'vertical');
});
test('separator in vertical toolbar has aria-orientation="horizontal"', async ({ page }) => {
// Second toolbar is vertical
const separator = getSeparators(page, 1).first();
await expect(separator).toHaveAttribute('aria-orientation', 'horizontal');
});
});
// ------------------------------------------
// 🔴 High Priority: Toggle Button ARIA
// ------------------------------------------
test.describe('APG: Toggle Button ARIA', () => {
test('toggle button has aria-pressed attribute', async ({ page }) => {
const buttons = getToolbarButtons(page, 0);
const firstButton = buttons.first();
const ariaPressed = await firstButton.getAttribute('aria-pressed');
expect(ariaPressed === 'true' || ariaPressed === 'false').toBe(true);
});
test('clicking toggle button changes aria-pressed', async ({ page }) => {
const buttons = getToolbarButtons(page, 0);
const toggleButton = buttons.first();
const initialPressed = await toggleButton.getAttribute('aria-pressed');
await toggleButton.click();
const newPressed = await toggleButton.getAttribute('aria-pressed');
expect(newPressed).not.toBe(initialPressed);
});
test('toggle button with defaultPressed starts as pressed', async ({ page }) => {
// Fifth toolbar has defaultPressed toggle buttons
const toolbar = getToolbar(page).nth(4);
const buttons = toolbar.getByRole('button');
const firstButton = buttons.first();
await expect(firstButton).toHaveAttribute('aria-pressed', 'true');
});
});
// ------------------------------------------
// 🔴 High Priority: Keyboard Interaction (Horizontal)
// ------------------------------------------
test.describe('APG: Keyboard Interaction (Horizontal)', () => {
test('ArrowRight moves focus to next button', async ({ page }) => {
const buttons = getToolbarButtons(page, 0);
const firstButton = buttons.first();
const secondButton = buttons.nth(1);
await firstButton.click();
await expect(firstButton).toBeFocused();
await page.keyboard.press('ArrowRight');
await expect(secondButton).toBeFocused();
});
test('ArrowLeft moves focus to previous button', async ({ page }) => {
const buttons = getToolbarButtons(page, 0);
const firstButton = buttons.first();
const secondButton = buttons.nth(1);
await secondButton.click();
await expect(secondButton).toBeFocused();
await page.keyboard.press('ArrowLeft');
await expect(firstButton).toBeFocused();
});
test('Home moves focus to first button', async ({ page }) => {
const buttons = getToolbarButtons(page, 0);
const firstButton = buttons.first();
const lastButton = buttons.last();
await lastButton.click();
await expect(lastButton).toBeFocused();
await page.keyboard.press('Home');
await expect(firstButton).toBeFocused();
});
test('End moves focus to last button', async ({ page }) => {
const buttons = getToolbarButtons(page, 0);
const firstButton = buttons.first();
const lastButton = buttons.last();
await firstButton.click();
await expect(firstButton).toBeFocused();
await page.keyboard.press('End');
await expect(lastButton).toBeFocused();
});
test('focus does not wrap at end (stops at edge)', async ({ page }) => {
const buttons = getToolbarButtons(page, 0);
const lastButton = buttons.last();
await lastButton.click();
await expect(lastButton).toBeFocused();
await page.keyboard.press('ArrowRight');
// Should still be on last button (no wrap)
await expect(lastButton).toBeFocused();
});
test('focus does not wrap at start (stops at edge)', async ({ page }) => {
const buttons = getToolbarButtons(page, 0);
const firstButton = buttons.first();
await firstButton.click();
await expect(firstButton).toBeFocused();
await page.keyboard.press('ArrowLeft');
// Should still be on first button (no wrap)
await expect(firstButton).toBeFocused();
});
test('ArrowUp/Down are ignored in horizontal toolbar', async ({ page }) => {
const buttons = getToolbarButtons(page, 0);
const firstButton = buttons.first();
await firstButton.click();
await expect(firstButton).toBeFocused();
await page.keyboard.press('ArrowDown');
await expect(firstButton).toBeFocused();
await page.keyboard.press('ArrowUp');
await expect(firstButton).toBeFocused();
});
});
// ------------------------------------------
// 🔴 High Priority: Keyboard Interaction (Vertical)
// ------------------------------------------
test.describe('APG: Keyboard Interaction (Vertical)', () => {
test('ArrowDown moves focus to next button in vertical toolbar', async ({ page }) => {
// Second toolbar is vertical
const buttons = getToolbarButtons(page, 1);
const firstButton = buttons.first();
const secondButton = buttons.nth(1);
await firstButton.click();
await expect(firstButton).toBeFocused();
await page.keyboard.press('ArrowDown');
await expect(secondButton).toBeFocused();
});
test('ArrowUp moves focus to previous button in vertical toolbar', async ({ page }) => {
const buttons = getToolbarButtons(page, 1);
const firstButton = buttons.first();
const secondButton = buttons.nth(1);
await secondButton.click();
await expect(secondButton).toBeFocused();
await page.keyboard.press('ArrowUp');
await expect(firstButton).toBeFocused();
});
test('ArrowLeft/Right are ignored in vertical toolbar', async ({ page }) => {
const buttons = getToolbarButtons(page, 1);
const firstButton = buttons.first();
await firstButton.click();
await expect(firstButton).toBeFocused();
await page.keyboard.press('ArrowRight');
await expect(firstButton).toBeFocused();
await page.keyboard.press('ArrowLeft');
await expect(firstButton).toBeFocused();
});
});
// ------------------------------------------
// 🔴 High Priority: Disabled Items
// ------------------------------------------
test.describe('APG: Disabled Items', () => {
test('disabled button has disabled attribute', async ({ page }) => {
// Third toolbar has disabled items
const toolbar = getToolbar(page).nth(2);
const buttons = toolbar.getByRole('button');
let foundDisabled = false;
const count = await buttons.count();
for (let i = 0; i < count; i++) {
const isDisabled = await buttons.nth(i).isDisabled();
if (isDisabled) {
foundDisabled = true;
break;
}
}
expect(foundDisabled).toBe(true);
});
test('arrow key navigation skips disabled buttons', async ({ page }) => {
// Third toolbar has disabled items: Undo, Redo(disabled), Cut, Copy, Paste(disabled)
const toolbar = getToolbar(page).nth(2);
const buttons = toolbar.getByRole('button');
const undoButton = buttons.filter({ hasText: 'Undo' });
await undoButton.click();
await expect(undoButton).toBeFocused();
// ArrowRight should skip Redo (disabled) and go to Cut
await page.keyboard.press('ArrowRight');
// Should be on Cut, not Redo
const focusedButton = page.locator(':focus');
const focusedText = await focusedButton.textContent();
expect(focusedText).not.toContain('Redo');
});
});
// ------------------------------------------
// 🔴 High Priority: Roving Tabindex
// ------------------------------------------
test.describe('APG: Roving Tabindex', () => {
test('first button has tabindex="0" initially', async ({ page }) => {
const buttons = getToolbarButtons(page, 0);
const firstButton = buttons.first();
await expect(firstButton).toHaveAttribute('tabindex', '0');
});
test('other buttons have tabindex="-1" initially', async ({ page }) => {
const buttons = getToolbarButtons(page, 0);
const count = await buttons.count();
for (let i = 1; i < count; i++) {
await expect(buttons.nth(i)).toHaveAttribute('tabindex', '-1');
}
});
test('focused button gets tabindex="0", previous loses it', async ({ page }) => {
const buttons = getToolbarButtons(page, 0);
const firstButton = buttons.first();
const secondButton = buttons.nth(1);
await firstButton.click();
await page.keyboard.press('ArrowRight');
// Second button should now have tabindex="0"
await expect(secondButton).toHaveAttribute('tabindex', '0');
// First button should have tabindex="-1"
await expect(firstButton).toHaveAttribute('tabindex', '-1');
});
test('only one enabled button has tabindex="0" at a time', async ({ page }) => {
const buttons = getToolbarButtons(page, 0);
const count = await buttons.count();
let tabbableCount = 0;
for (let i = 0; i < count; i++) {
const button = buttons.nth(i);
const isDisabled = await button.isDisabled();
if (isDisabled) continue;
const tabindex = await button.getAttribute('tabindex');
if (tabindex === '0') {
tabbableCount++;
}
}
expect(tabbableCount).toBe(1);
});
});
// ------------------------------------------
// 🔴 High Priority: Button Activation
// ------------------------------------------
test.describe('APG: Button Activation', () => {
test('Enter activates button', async ({ page }) => {
const buttons = getToolbarButtons(page, 0);
const toggleButton = buttons.first();
// Focus the button without clicking (to avoid changing state)
await toggleButton.focus();
await expect(toggleButton).toBeFocused();
const initialPressed = await toggleButton.getAttribute('aria-pressed');
await page.keyboard.press('Enter');
const newPressed = await toggleButton.getAttribute('aria-pressed');
expect(newPressed).not.toBe(initialPressed);
});
test('Space activates button', async ({ page }) => {
const buttons = getToolbarButtons(page, 0);
const toggleButton = buttons.first();
// Focus the button without clicking (to avoid changing state)
await toggleButton.focus();
await expect(toggleButton).toBeFocused();
const initialPressed = await toggleButton.getAttribute('aria-pressed');
await page.keyboard.press('Space');
const newPressed = await toggleButton.getAttribute('aria-pressed');
expect(newPressed).not.toBe(initialPressed);
});
});
// ------------------------------------------
// 🟢 Low Priority: Accessibility
// ------------------------------------------
test.describe('Accessibility', () => {
test('has no axe-core violations', async ({ page }) => {
await getToolbar(page).first().waitFor();
const results = await new AxeBuilder({ page })
.include('[role="toolbar"]')
.disableRules(['color-contrast'])
.analyze();
expect(results.violations).toEqual([]);
});
});
});
}
// ============================================
// Cross-framework Consistency Tests
// ============================================
test.describe('Toolbar - Cross-framework Consistency', () => {
test('all frameworks render toolbars', async ({ page }) => {
for (const framework of frameworks) {
await page.goto(`patterns/toolbar/${framework}/demo/`);
await getToolbar(page).first().waitFor();
const toolbars = getToolbar(page);
const count = await toolbars.count();
expect(count).toBeGreaterThan(0);
}
});
test('all frameworks support keyboard navigation', async ({ page }) => {
test.setTimeout(60000);
for (const framework of frameworks) {
await page.goto(`patterns/toolbar/${framework}/demo/`);
await getToolbar(page).first().waitFor();
const buttons = getToolbarButtons(page, 0);
const firstButton = buttons.first();
const secondButton = buttons.nth(1);
await firstButton.click();
await expect(firstButton).toBeFocused();
await page.keyboard.press('ArrowRight');
await expect(secondButton).toBeFocused();
}
});
test('all frameworks have consistent ARIA structure', async ({ page }) => {
test.setTimeout(60000);
for (const framework of frameworks) {
await page.goto(`patterns/toolbar/${framework}/demo/`);
await getToolbar(page).first().waitFor();
// Check toolbar role
const toolbar = getToolbar(page).first();
await expect(toolbar).toHaveRole('toolbar');
// Check aria-label
const ariaLabel = await toolbar.getAttribute('aria-label');
expect(ariaLabel).toBeTruthy();
// Check aria-orientation
const orientation = await toolbar.getAttribute('aria-orientation');
expect(orientation).toBe('horizontal');
// Check buttons exist
const buttons = getToolbarButtons(page, 0);
const count = await buttons.count();
expect(count).toBeGreaterThan(0);
}
});
test('all frameworks support toggle buttons', async ({ page }) => {
test.setTimeout(60000);
for (const framework of frameworks) {
await page.goto(`patterns/toolbar/${framework}/demo/`);
await getToolbar(page).first().waitFor();
const buttons = getToolbarButtons(page, 0);
const toggleButton = buttons.first();
// Should have aria-pressed
const initialPressed = await toggleButton.getAttribute('aria-pressed');
expect(initialPressed === 'true' || initialPressed === 'false').toBe(true);
// Should toggle on click
await toggleButton.click();
const newPressed = await toggleButton.getAttribute('aria-pressed');
expect(newPressed).not.toBe(initialPressed);
}
});
}); Running Tests
# Run unit tests for Toolbar
npm run test -- toolbar
# Run E2E tests for Toolbar (all frameworks)
npm run test:e2e:pattern --pattern=toolbar
# Run E2E tests for specific framework
npm run test:e2e:react:pattern --pattern=toolbar Testing Tools
- Vitest (opens in new tab) - Test runner for unit tests
- Testing Library (opens in new tab) - Framework-specific testing utilities (React, Vue, Svelte)
- Playwright (opens in new tab) - Browser automation for E2E tests
- axe-core/playwright (opens in new tab) - Automated accessibility testing in E2E
See testing-strategy.md (opens in new tab) for full documentation.
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { axe } from 'jest-axe';
import { describe, expect, it, vi } from 'vitest';
import { Toolbar, ToolbarButton, ToolbarToggleButton, ToolbarSeparator } from './Toolbar';
describe('Toolbar', () => {
// 🔴 High Priority: APG Core Compliance
describe('APG: ARIA Attributes', () => {
it('has role="toolbar"', () => {
render(
<Toolbar aria-label="Test toolbar">
<ToolbarButton>Button</ToolbarButton>
</Toolbar>
);
expect(screen.getByRole('toolbar')).toBeInTheDocument();
});
it('has aria-orientation="horizontal" by default', () => {
render(
<Toolbar aria-label="Test toolbar">
<ToolbarButton>Button</ToolbarButton>
</Toolbar>
);
expect(screen.getByRole('toolbar')).toHaveAttribute('aria-orientation', 'horizontal');
});
it('aria-orientation reflects orientation prop', () => {
const { rerender } = render(
<Toolbar aria-label="Test toolbar" orientation="vertical">
<ToolbarButton>Button</ToolbarButton>
</Toolbar>
);
expect(screen.getByRole('toolbar')).toHaveAttribute('aria-orientation', 'vertical');
rerender(
<Toolbar aria-label="Test toolbar" orientation="horizontal">
<ToolbarButton>Button</ToolbarButton>
</Toolbar>
);
expect(screen.getByRole('toolbar')).toHaveAttribute('aria-orientation', 'horizontal');
});
it('passes through aria-label', () => {
render(
<Toolbar aria-label="Text formatting">
<ToolbarButton>Button</ToolbarButton>
</Toolbar>
);
expect(screen.getByRole('toolbar')).toHaveAttribute('aria-label', 'Text formatting');
});
it('passes through aria-labelledby', () => {
render(
<>
<h2 id="toolbar-label">Toolbar Label</h2>
<Toolbar aria-labelledby="toolbar-label">
<ToolbarButton>Button</ToolbarButton>
</Toolbar>
</>
);
expect(screen.getByRole('toolbar')).toHaveAttribute('aria-labelledby', 'toolbar-label');
});
});
describe('APG: Keyboard Interaction (Horizontal)', () => {
it('moves focus to next button with ArrowRight', async () => {
const user = userEvent.setup();
render(
<Toolbar aria-label="Test toolbar">
<ToolbarButton>First</ToolbarButton>
<ToolbarButton>Second</ToolbarButton>
<ToolbarButton>Third</ToolbarButton>
</Toolbar>
);
const firstButton = screen.getByRole('button', { name: 'First' });
firstButton.focus();
await user.keyboard('{ArrowRight}');
expect(screen.getByRole('button', { name: 'Second' })).toHaveFocus();
});
it('moves focus to previous button with ArrowLeft', async () => {
const user = userEvent.setup();
render(
<Toolbar aria-label="Test toolbar">
<ToolbarButton>First</ToolbarButton>
<ToolbarButton>Second</ToolbarButton>
<ToolbarButton>Third</ToolbarButton>
</Toolbar>
);
const secondButton = screen.getByRole('button', { name: 'Second' });
secondButton.focus();
await user.keyboard('{ArrowLeft}');
expect(screen.getByRole('button', { name: 'First' })).toHaveFocus();
});
it('does not wrap from last to first with ArrowRight (stops at edge)', async () => {
const user = userEvent.setup();
render(
<Toolbar aria-label="Test toolbar">
<ToolbarButton>First</ToolbarButton>
<ToolbarButton>Second</ToolbarButton>
<ToolbarButton>Third</ToolbarButton>
</Toolbar>
);
const thirdButton = screen.getByRole('button', { name: 'Third' });
thirdButton.focus();
await user.keyboard('{ArrowRight}');
expect(thirdButton).toHaveFocus();
});
it('does not wrap from first to last with ArrowLeft (stops at edge)', async () => {
const user = userEvent.setup();
render(
<Toolbar aria-label="Test toolbar">
<ToolbarButton>First</ToolbarButton>
<ToolbarButton>Second</ToolbarButton>
<ToolbarButton>Third</ToolbarButton>
</Toolbar>
);
const firstButton = screen.getByRole('button', { name: 'First' });
firstButton.focus();
await user.keyboard('{ArrowLeft}');
expect(firstButton).toHaveFocus();
});
it('ArrowUp/Down are disabled in horizontal toolbar', async () => {
const user = userEvent.setup();
render(
<Toolbar aria-label="Test toolbar">
<ToolbarButton>First</ToolbarButton>
<ToolbarButton>Second</ToolbarButton>
</Toolbar>
);
const firstButton = screen.getByRole('button', { name: 'First' });
firstButton.focus();
await user.keyboard('{ArrowDown}');
expect(firstButton).toHaveFocus();
await user.keyboard('{ArrowUp}');
expect(firstButton).toHaveFocus();
});
it('moves focus to first button with Home', async () => {
const user = userEvent.setup();
render(
<Toolbar aria-label="Test toolbar">
<ToolbarButton>First</ToolbarButton>
<ToolbarButton>Second</ToolbarButton>
<ToolbarButton>Third</ToolbarButton>
</Toolbar>
);
const thirdButton = screen.getByRole('button', { name: 'Third' });
thirdButton.focus();
await user.keyboard('{Home}');
expect(screen.getByRole('button', { name: 'First' })).toHaveFocus();
});
it('moves focus to last button with End', async () => {
const user = userEvent.setup();
render(
<Toolbar aria-label="Test toolbar">
<ToolbarButton>First</ToolbarButton>
<ToolbarButton>Second</ToolbarButton>
<ToolbarButton>Third</ToolbarButton>
</Toolbar>
);
const firstButton = screen.getByRole('button', { name: 'First' });
firstButton.focus();
await user.keyboard('{End}');
expect(screen.getByRole('button', { name: 'Third' })).toHaveFocus();
});
it('skips disabled items when navigating', async () => {
const user = userEvent.setup();
render(
<Toolbar aria-label="Test toolbar">
<ToolbarButton>First</ToolbarButton>
<ToolbarButton disabled>Second (disabled)</ToolbarButton>
<ToolbarButton>Third</ToolbarButton>
</Toolbar>
);
const firstButton = screen.getByRole('button', { name: 'First' });
firstButton.focus();
await user.keyboard('{ArrowRight}');
expect(screen.getByRole('button', { name: 'Third' })).toHaveFocus();
});
});
describe('APG: Keyboard Interaction (Vertical)', () => {
it('moves focus to next button with ArrowDown', async () => {
const user = userEvent.setup();
render(
<Toolbar aria-label="Test toolbar" orientation="vertical">
<ToolbarButton>First</ToolbarButton>
<ToolbarButton>Second</ToolbarButton>
<ToolbarButton>Third</ToolbarButton>
</Toolbar>
);
const firstButton = screen.getByRole('button', { name: 'First' });
firstButton.focus();
await user.keyboard('{ArrowDown}');
expect(screen.getByRole('button', { name: 'Second' })).toHaveFocus();
});
it('moves focus to previous button with ArrowUp', async () => {
const user = userEvent.setup();
render(
<Toolbar aria-label="Test toolbar" orientation="vertical">
<ToolbarButton>First</ToolbarButton>
<ToolbarButton>Second</ToolbarButton>
<ToolbarButton>Third</ToolbarButton>
</Toolbar>
);
const secondButton = screen.getByRole('button', { name: 'Second' });
secondButton.focus();
await user.keyboard('{ArrowUp}');
expect(screen.getByRole('button', { name: 'First' })).toHaveFocus();
});
it('ArrowLeft/Right are disabled in vertical toolbar', async () => {
const user = userEvent.setup();
render(
<Toolbar aria-label="Test toolbar" orientation="vertical">
<ToolbarButton>First</ToolbarButton>
<ToolbarButton>Second</ToolbarButton>
</Toolbar>
);
const firstButton = screen.getByRole('button', { name: 'First' });
firstButton.focus();
await user.keyboard('{ArrowRight}');
expect(firstButton).toHaveFocus();
await user.keyboard('{ArrowLeft}');
expect(firstButton).toHaveFocus();
});
it('stops at edge with ArrowDown (does not wrap)', async () => {
const user = userEvent.setup();
render(
<Toolbar aria-label="Test toolbar" orientation="vertical">
<ToolbarButton>First</ToolbarButton>
<ToolbarButton>Second</ToolbarButton>
</Toolbar>
);
const secondButton = screen.getByRole('button', { name: 'Second' });
secondButton.focus();
await user.keyboard('{ArrowDown}');
expect(secondButton).toHaveFocus();
});
});
describe('APG: Focus Management', () => {
it('first enabled item has tabIndex=0, others have tabIndex=-1 (Roving Tabindex)', () => {
render(
<Toolbar aria-label="Test toolbar">
<ToolbarButton>First</ToolbarButton>
<ToolbarButton>Second</ToolbarButton>
</Toolbar>
);
const buttons = screen.getAllByRole('button');
expect(buttons[0]).toHaveAttribute('tabIndex', '0');
expect(buttons[1]).toHaveAttribute('tabIndex', '-1');
});
it('updates focus position on click', async () => {
const user = userEvent.setup();
render(
<Toolbar aria-label="Test toolbar">
<ToolbarButton>First</ToolbarButton>
<ToolbarButton>Second</ToolbarButton>
<ToolbarButton>Third</ToolbarButton>
</Toolbar>
);
await user.click(screen.getByRole('button', { name: 'Second' }));
await user.keyboard('{ArrowRight}');
expect(screen.getByRole('button', { name: 'Third' })).toHaveFocus();
});
});
});
describe('ToolbarButton', () => {
describe('ARIA Attributes', () => {
it('has implicit role="button"', () => {
render(
<Toolbar aria-label="Test toolbar">
<ToolbarButton>Click me</ToolbarButton>
</Toolbar>
);
expect(screen.getByRole('button', { name: 'Click me' })).toBeInTheDocument();
});
it('has type="button"', () => {
render(
<Toolbar aria-label="Test toolbar">
<ToolbarButton>Click me</ToolbarButton>
</Toolbar>
);
expect(screen.getByRole('button')).toHaveAttribute('type', 'button');
});
});
describe('Functionality', () => {
it('fires onClick on click', async () => {
const handleClick = vi.fn();
const user = userEvent.setup();
render(
<Toolbar aria-label="Test toolbar">
<ToolbarButton onClick={handleClick}>Click me</ToolbarButton>
</Toolbar>
);
await user.click(screen.getByRole('button'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
it('fires onClick on Enter', async () => {
const handleClick = vi.fn();
const user = userEvent.setup();
render(
<Toolbar aria-label="Test toolbar">
<ToolbarButton onClick={handleClick}>Click me</ToolbarButton>
</Toolbar>
);
const button = screen.getByRole('button');
button.focus();
await user.keyboard('{Enter}');
expect(handleClick).toHaveBeenCalledTimes(1);
});
it('fires onClick on Space', async () => {
const handleClick = vi.fn();
const user = userEvent.setup();
render(
<Toolbar aria-label="Test toolbar">
<ToolbarButton onClick={handleClick}>Click me</ToolbarButton>
</Toolbar>
);
const button = screen.getByRole('button');
button.focus();
await user.keyboard(' ');
expect(handleClick).toHaveBeenCalledTimes(1);
});
it('does not fire onClick when disabled', async () => {
const handleClick = vi.fn();
const user = userEvent.setup();
render(
<Toolbar aria-label="Test toolbar">
<ToolbarButton onClick={handleClick} disabled>
Click me
</ToolbarButton>
</Toolbar>
);
await user.click(screen.getByRole('button'));
expect(handleClick).not.toHaveBeenCalled();
});
it('is not focusable when disabled (disabled attribute)', () => {
render(
<Toolbar aria-label="Test toolbar">
<ToolbarButton disabled>Click me</ToolbarButton>
</Toolbar>
);
expect(screen.getByRole('button')).toBeDisabled();
});
});
});
describe('ToolbarToggleButton', () => {
describe('ARIA Attributes', () => {
it('has implicit role="button"', () => {
render(
<Toolbar aria-label="Test toolbar">
<ToolbarToggleButton>Toggle</ToolbarToggleButton>
</Toolbar>
);
expect(screen.getByRole('button', { name: 'Toggle' })).toBeInTheDocument();
});
it('has type="button"', () => {
render(
<Toolbar aria-label="Test toolbar">
<ToolbarToggleButton>Toggle</ToolbarToggleButton>
</Toolbar>
);
expect(screen.getByRole('button')).toHaveAttribute('type', 'button');
});
it('has aria-pressed="false" in initial state', () => {
render(
<Toolbar aria-label="Test toolbar">
<ToolbarToggleButton>Toggle</ToolbarToggleButton>
</Toolbar>
);
expect(screen.getByRole('button')).toHaveAttribute('aria-pressed', 'false');
});
it('has aria-pressed="true" when pressed', () => {
render(
<Toolbar aria-label="Test toolbar">
<ToolbarToggleButton defaultPressed>Toggle</ToolbarToggleButton>
</Toolbar>
);
expect(screen.getByRole('button')).toHaveAttribute('aria-pressed', 'true');
});
});
describe('Functionality', () => {
it('toggles aria-pressed on click', async () => {
const user = userEvent.setup();
render(
<Toolbar aria-label="Test toolbar">
<ToolbarToggleButton>Toggle</ToolbarToggleButton>
</Toolbar>
);
const button = screen.getByRole('button');
expect(button).toHaveAttribute('aria-pressed', 'false');
await user.click(button);
expect(button).toHaveAttribute('aria-pressed', 'true');
await user.click(button);
expect(button).toHaveAttribute('aria-pressed', 'false');
});
it('toggles aria-pressed on Enter', async () => {
const user = userEvent.setup();
render(
<Toolbar aria-label="Test toolbar">
<ToolbarToggleButton>Toggle</ToolbarToggleButton>
</Toolbar>
);
const button = screen.getByRole('button');
button.focus();
expect(button).toHaveAttribute('aria-pressed', 'false');
await user.keyboard('{Enter}');
expect(button).toHaveAttribute('aria-pressed', 'true');
});
it('toggles aria-pressed on Space', async () => {
const user = userEvent.setup();
render(
<Toolbar aria-label="Test toolbar">
<ToolbarToggleButton>Toggle</ToolbarToggleButton>
</Toolbar>
);
const button = screen.getByRole('button');
button.focus();
expect(button).toHaveAttribute('aria-pressed', 'false');
await user.keyboard(' ');
expect(button).toHaveAttribute('aria-pressed', 'true');
});
it('fires onPressedChange', async () => {
const handlePressedChange = vi.fn();
const user = userEvent.setup();
render(
<Toolbar aria-label="Test toolbar">
<ToolbarToggleButton onPressedChange={handlePressedChange}>Toggle</ToolbarToggleButton>
</Toolbar>
);
await user.click(screen.getByRole('button'));
expect(handlePressedChange).toHaveBeenCalledWith(true);
await user.click(screen.getByRole('button'));
expect(handlePressedChange).toHaveBeenCalledWith(false);
});
it('sets initial state with defaultPressed', () => {
render(
<Toolbar aria-label="Test toolbar">
<ToolbarToggleButton defaultPressed>Toggle</ToolbarToggleButton>
</Toolbar>
);
expect(screen.getByRole('button')).toHaveAttribute('aria-pressed', 'true');
});
it('controlled state with pressed prop', async () => {
const user = userEvent.setup();
const Controlled = () => {
const [pressed, setPressed] = React.useState(false);
return (
<Toolbar aria-label="Test toolbar">
<ToolbarToggleButton pressed={pressed} onPressedChange={setPressed}>
Toggle
</ToolbarToggleButton>
</Toolbar>
);
};
render(<Controlled />);
const button = screen.getByRole('button');
expect(button).toHaveAttribute('aria-pressed', 'false');
await user.click(button);
expect(button).toHaveAttribute('aria-pressed', 'true');
});
it('does not toggle when disabled', async () => {
const user = userEvent.setup();
render(
<Toolbar aria-label="Test toolbar">
<ToolbarToggleButton disabled>Toggle</ToolbarToggleButton>
</Toolbar>
);
const button = screen.getByRole('button');
expect(button).toHaveAttribute('aria-pressed', 'false');
await user.click(button);
expect(button).toHaveAttribute('aria-pressed', 'false');
});
it('does not fire onPressedChange when disabled', async () => {
const handlePressedChange = vi.fn();
const user = userEvent.setup();
render(
<Toolbar aria-label="Test toolbar">
<ToolbarToggleButton disabled onPressedChange={handlePressedChange}>
Toggle
</ToolbarToggleButton>
</Toolbar>
);
await user.click(screen.getByRole('button'));
expect(handlePressedChange).not.toHaveBeenCalled();
});
it('is not focusable when disabled (disabled attribute)', () => {
render(
<Toolbar aria-label="Test toolbar">
<ToolbarToggleButton disabled>Toggle</ToolbarToggleButton>
</Toolbar>
);
expect(screen.getByRole('button')).toBeDisabled();
});
});
});
describe('ToolbarSeparator', () => {
describe('ARIA Attributes', () => {
it('has role="separator"', () => {
render(
<Toolbar aria-label="Test toolbar">
<ToolbarButton>Before</ToolbarButton>
<ToolbarSeparator />
<ToolbarButton>After</ToolbarButton>
</Toolbar>
);
expect(screen.getByRole('separator')).toBeInTheDocument();
});
it('has aria-orientation="vertical" in horizontal toolbar', () => {
render(
<Toolbar aria-label="Test toolbar" orientation="horizontal">
<ToolbarButton>Before</ToolbarButton>
<ToolbarSeparator />
<ToolbarButton>After</ToolbarButton>
</Toolbar>
);
expect(screen.getByRole('separator')).toHaveAttribute('aria-orientation', 'vertical');
});
it('has aria-orientation="horizontal" in vertical toolbar', () => {
render(
<Toolbar aria-label="Test toolbar" orientation="vertical">
<ToolbarButton>Before</ToolbarButton>
<ToolbarSeparator />
<ToolbarButton>After</ToolbarButton>
</Toolbar>
);
expect(screen.getByRole('separator')).toHaveAttribute('aria-orientation', 'horizontal');
});
});
});
describe('Accessibility', () => {
it('has no WCAG 2.1 AA violations', async () => {
const { container } = render(
<Toolbar aria-label="Text formatting">
<ToolbarToggleButton>Bold</ToolbarToggleButton>
<ToolbarToggleButton>Italic</ToolbarToggleButton>
<ToolbarSeparator />
<ToolbarButton>Copy</ToolbarButton>
<ToolbarButton>Paste</ToolbarButton>
</Toolbar>
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it('has no WCAG 2.1 AA violations in vertical toolbar', async () => {
const { container } = render(
<Toolbar aria-label="Actions" orientation="vertical">
<ToolbarButton>New</ToolbarButton>
<ToolbarButton>Open</ToolbarButton>
<ToolbarSeparator />
<ToolbarButton>Save</ToolbarButton>
</Toolbar>
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
});
describe('HTML Attribute Inheritance', () => {
it('applies className to container', () => {
render(
<Toolbar aria-label="Test toolbar" className="custom-toolbar">
<ToolbarButton>Button</ToolbarButton>
</Toolbar>
);
expect(screen.getByRole('toolbar')).toHaveClass('custom-toolbar');
});
it('applies className to ToolbarButton', () => {
render(
<Toolbar aria-label="Test toolbar">
<ToolbarButton className="custom-button">Button</ToolbarButton>
</Toolbar>
);
expect(screen.getByRole('button')).toHaveClass('custom-button');
});
it('applies className to ToolbarToggleButton', () => {
render(
<Toolbar aria-label="Test toolbar">
<ToolbarToggleButton className="custom-toggle">Toggle</ToolbarToggleButton>
</Toolbar>
);
expect(screen.getByRole('button')).toHaveClass('custom-toggle');
});
it('applies className to ToolbarSeparator', () => {
render(
<Toolbar aria-label="Test toolbar">
<ToolbarButton>Before</ToolbarButton>
<ToolbarSeparator className="custom-separator" />
<ToolbarButton>After</ToolbarButton>
</Toolbar>
);
expect(screen.getByRole('separator')).toHaveClass('custom-separator');
});
});
// Import React for the controlled component test
import React from 'react'; Resources
- WAI-ARIA APG: Toolbar Pattern (opens in new tab)
- WAI-ARIA: toolbar role (opens in new tab)
- AI Implementation Guide (llm.md) (opens in new tab) - ARIA specs, keyboard support, test checklist