Toolbar
ボタン、トグルボタン、チェックボックスなどのコントロールセットをグループ化するコンテナ。
デモ
テキスト書式設定ツールバー
トグルボタンと通常ボタンを含む水平ツールバー。
垂直ツールバー
上下矢印キーでナビゲートできます。
無効化されたアイテムを含む
無効化されたアイテムはキーボードナビゲーション時にスキップされます。
制御されたトグルボタン
制御された状態を持つトグルボタン。現在の状態が表示され、サンプルテキストに適用されます。
Current state: {"bold":false,"italic":false,"underline":false}
デフォルトの押下状態
defaultPressedで初期状態を設定したトグルボタン(無効化状態を含む)。
アクセシビリティ
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)
ソースコード
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()}
/>
);
} 使い方
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
| プロパティ | 型 | デフォルト | 説明 |
|---|---|---|---|
orientation | 'horizontal' | 'vertical' | 'horizontal' | ツールバーの方向 |
aria-label | string | - | ツールバーのアクセシブルラベル |
children | React.ReactNode | - | ツールバーのコンテンツ |
ToolbarButton Props
| プロパティ | 型 | デフォルト | 説明 |
|---|---|---|---|
disabled | boolean | false | ボタンが無効化されているかどうか |
onClick | () => void | - | クリックハンドラー |
ToolbarToggleButton Props
| プロパティ | 型 | デフォルト | 説明 |
|---|---|---|---|
pressed | boolean | - | 制御された押下状態 |
defaultPressed | boolean | false | 初期押下状態(非制御) |
onPressedChange | (pressed: boolean) => void | - | 押下状態が変更された時のコールバック |
disabled | boolean | false | ボタンが無効化されているかどうか |
テスト
テストは、キーボード操作、ARIA属性、アクセシビリティ要件の観点からAPG準拠を検証します。Toolbarコンポーネントは2層のテスト戦略を使用しています。
テスト戦略
ユニットテスト(Testing Library)
フレームワーク固有のテストライブラリを使用して、コンポーネントの出力を検証します。正しいHTML構造とARIA属性を確認します。
- ARIA属性(aria-pressed、aria-orientation)
- キーボード操作(矢印キー、Home、End)
- Roving tabindexの動作
- jest-axeによるアクセシビリティ
E2Eテスト(Playwright)
実際のブラウザ環境で全フレームワークのコンポーネント動作を検証します。インタラクションとクロスフレームワークの一貫性をカバーします。
- クリック操作
- 矢印キーナビゲーション(水平・垂直)
- トグルボタンの状態変更
- 無効化アイテムの処理
- ライブブラウザでのARIA構造検証
- axe-coreアクセシビリティスキャン
- クロスフレームワーク一貫性チェック
テストカテゴリ
高優先度 : APGキーボード操作(Unit + E2E)
| テスト | 説明 |
|---|---|
ArrowRight/Left | アイテム間のフォーカス移動(水平) |
ArrowDown/Up | アイテム間のフォーカス移動(垂直) |
Home | 最初のアイテムにフォーカス移動 |
End | 最後のアイテムにフォーカス移動 |
No wrap | フォーカスは端で停止(ループなし) |
Disabled skip | ナビゲーション中に無効化アイテムをスキップ |
Enter/Space | ボタンをアクティブ化またはトグルボタンを切り替え |
高優先度 : APG ARIA属性(Unit + E2E)
| テスト | 説明 |
|---|---|
role="toolbar" | コンテナがtoolbarロールを持つ |
aria-orientation | 水平/垂直方向を反映 |
aria-label/labelledby | ツールバーがアクセシブルな名前を持つ |
aria-pressed | トグルボタンが押下状態を反映 |
role="separator" | セパレーターが正しいロールと方向を持つ |
type="button" | ボタンが明示的なtype属性を持つ |
高優先度 : フォーカス管理 - Roving Tabindex(Unit + E2E)
| テスト | 説明 |
|---|---|
tabIndex=0 | 最初の有効なアイテムがtabIndex=0を持つ |
tabIndex=-1 | 他のアイテムがtabIndex=-1を持つ |
Click updates focus | アイテムをクリックするとroving focus位置が更新される |
高優先度 : トグルボタンの状態(Unit + E2E)
| テスト | 説明 |
|---|---|
aria-pressed | トグルボタンがaria-pressed属性を持つ |
Click toggles | トグルボタンをクリックするとaria-pressedが変わる |
defaultPressed | defaultPressedを持つトグルは押下状態で開始 |
中優先度 : アクセシビリティ(Unit + E2E)
| テスト | 説明 |
|---|---|
axe violations | WCAG 2.1 AA違反なし(jest-axe/axe-core経由) |
Vertical toolbar | 垂直方向もaxeに合格 |
低優先度 : クロスフレームワーク一貫性(E2E)
| テスト | 説明 |
|---|---|
All frameworks render | React、Vue、Svelte、Astroすべてがツールバーをレンダリング |
Consistent keyboard | すべてのフレームワークがキーボードナビゲーションをサポート |
Consistent ARIA | すべてのフレームワークが一貫したARIA構造を持つ |
Toggle buttons | すべてのフレームワークがトグルボタン状態をサポート |
E2Eテストコード例
以下は実際のE2Eテストファイルです (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 firstButton.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 secondButton.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 lastButton.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 firstButton.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 lastButton.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 firstButton.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 firstButton.press('ArrowDown');
await expect(firstButton).toBeFocused();
await expect(firstButton).toBeFocused();
await firstButton.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 firstButton.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 secondButton.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 firstButton.press('ArrowRight');
await expect(firstButton).toBeFocused();
await expect(firstButton).toBeFocused();
await firstButton.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 undoButton.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 expect(firstButton).toBeFocused();
await firstButton.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 toggleButton.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 toggleButton.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 firstButton.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);
}
});
}); テストの実行
# Toolbarのユニットテストを実行
npm run test -- toolbar
# ToolbarのE2Eテストを実行(全フレームワーク)
npm run test:e2e:pattern --pattern=toolbar
# 特定のフレームワークでE2Eテストを実行
npm run test:e2e:react:pattern --pattern=toolbar テストツール
- Vitest (opens in new tab) - ユニットテストランナー
- Testing Library (opens in new tab) - フレームワーク別テストユーティリティ(React、Vue、Svelte)
- Playwright (opens in new tab) - E2Eテスト用ブラウザ自動化
- axe-core/playwright (opens in new tab) - E2Eでの自動アクセシビリティテスト
詳細は testing-strategy.md (opens in new tab) を参照してください。
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'; リソース
- WAI-ARIA APG: Toolbar パターン (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