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 using v-model. 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 default-pressed 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
<script lang="ts">
export interface ToolbarProps {
/** Direction of the toolbar */
orientation?: 'horizontal' | 'vertical'
}
export { ToolbarContextKey, type ToolbarContext } from './toolbar-context'
</script>
<template>
<div
ref="toolbarRef"
role="toolbar"
:aria-orientation="orientation"
class="apg-toolbar"
v-bind="$attrs"
@keydown="handleKeyDown"
@focus.capture="handleFocus"
>
<slot />
</div>
</template>
<script setup lang="ts">
import { ref, computed, provide, watch, onMounted, useSlots } from 'vue'
import { ToolbarContextKey, type ToolbarContext } from './toolbar-context'
defineOptions({
inheritAttrs: false
})
const props = withDefaults(defineProps<{
/** Direction of the toolbar */
orientation?: 'horizontal' | 'vertical'
}>(), {
orientation: 'horizontal'
})
// Provide reactive context to child components
const orientationComputed = computed(() => props.orientation)
provide<ToolbarContext>(ToolbarContextKey, {
orientation: orientationComputed
})
const toolbarRef = ref<HTMLDivElement | null>(null)
const focusedIndex = ref(0)
const slots = useSlots()
const getButtons = (): HTMLButtonElement[] => {
if (!toolbarRef.value) return []
return Array.from(
toolbarRef.value.querySelectorAll<HTMLButtonElement>('button:not([disabled])')
)
}
// Roving tabindex: only the focused button should have tabIndex=0
const updateTabIndices = () => {
const buttons = getButtons()
if (buttons.length === 0) return
// Clamp focusedIndex to valid range
if (focusedIndex.value >= buttons.length) {
focusedIndex.value = buttons.length - 1
return // Will re-run with corrected index
}
buttons.forEach((btn, index) => {
btn.tabIndex = index === focusedIndex.value ? 0 : -1
})
}
onMounted(updateTabIndices)
watch(focusedIndex, updateTabIndices)
watch(() => slots.default?.(), updateTabIndices, { flush: 'post' })
const handleFocus = (event: FocusEvent) => {
const buttons = getButtons()
const targetIndex = buttons.findIndex(btn => btn === event.target)
if (targetIndex !== -1) {
focusedIndex.value = targetIndex
}
}
const handleKeyDown = (event: KeyboardEvent) => {
const buttons = getButtons()
if (buttons.length === 0) return
const currentIndex = buttons.findIndex(btn => btn === document.activeElement)
if (currentIndex === -1) return
const nextKey = props.orientation === 'vertical' ? 'ArrowDown' : 'ArrowRight'
const prevKey = props.orientation === 'vertical' ? 'ArrowUp' : 'ArrowLeft'
const invalidKeys = props.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()
focusedIndex.value = newIndex
}
}
}
</script> <script lang="ts">
export interface ToolbarButtonProps {
/** Whether the button is disabled */
disabled?: boolean
}
</script>
<template>
<button
type="button"
class="apg-toolbar-button"
:disabled="disabled"
v-bind="$attrs"
>
<slot />
</button>
</template>
<script setup lang="ts">
import { inject } from 'vue'
import { ToolbarContextKey } from './toolbar-context'
defineOptions({
inheritAttrs: false
})
withDefaults(defineProps<{
/** Whether the button is disabled */
disabled?: boolean
}>(), {
disabled: false
})
// Verify we're inside a Toolbar
const context = inject(ToolbarContextKey)
if (!context) {
console.warn('ToolbarButton must be used within a Toolbar')
}
</script> <script lang="ts">
export interface ToolbarToggleButtonProps {
/** Controlled pressed state */
pressed?: boolean
/** Default pressed state (uncontrolled) */
defaultPressed?: boolean
/** Whether the button is disabled */
disabled?: boolean
}
</script>
<template>
<button
type="button"
class="apg-toolbar-button"
:aria-pressed="currentPressed"
:disabled="disabled"
v-bind="$attrs"
@click="handleClick"
>
<slot />
</button>
</template>
<script setup lang="ts">
import { ref, inject, computed } from 'vue'
import { ToolbarContextKey } from './toolbar-context'
defineOptions({
inheritAttrs: false
})
const props = withDefaults(defineProps<{
/** Controlled pressed state */
pressed?: boolean
/** Default pressed state (uncontrolled) */
defaultPressed?: boolean
/** Whether the button is disabled */
disabled?: boolean
}>(), {
pressed: undefined,
defaultPressed: false,
disabled: false
})
const emit = defineEmits<{
'update:pressed': [pressed: boolean]
'pressed-change': [pressed: boolean]
}>()
// Verify we're inside a Toolbar
const context = inject(ToolbarContextKey)
if (!context) {
console.warn('ToolbarToggleButton must be used within a Toolbar')
}
const internalPressed = ref(props.defaultPressed)
const isControlled = computed(() => props.pressed !== undefined)
const currentPressed = computed(() => isControlled.value ? props.pressed : internalPressed.value)
const handleClick = () => {
if (props.disabled) return
const newPressed = !currentPressed.value
if (!isControlled.value) {
internalPressed.value = newPressed
}
emit('update:pressed', newPressed)
emit('pressed-change', newPressed)
}
</script> <template>
<div
role="separator"
:aria-orientation="separatorOrientation"
class="apg-toolbar-separator"
/>
</template>
<script setup lang="ts">
import { inject, computed } from 'vue'
import { ToolbarContextKey } from './toolbar-context'
// Verify we're inside a Toolbar
const context = inject(ToolbarContextKey)
if (!context) {
console.warn('ToolbarSeparator must be used within a Toolbar')
}
// Separator orientation is perpendicular to toolbar orientation
const separatorOrientation = computed(() =>
context?.orientation.value === 'horizontal' ? 'vertical' : 'horizontal'
)
</script> Usage
<script setup>
import Toolbar from '@patterns/toolbar/Toolbar.vue'
import ToolbarButton from '@patterns/toolbar/ToolbarButton.vue'
import ToolbarToggleButton from '@patterns/toolbar/ToolbarToggleButton.vue'
import ToolbarSeparator from '@patterns/toolbar/ToolbarSeparator.vue'
</script>
<template>
<Toolbar aria-label="Text formatting">
<ToolbarToggleButton>Bold</ToolbarToggleButton>
<ToolbarToggleButton>Italic</ToolbarToggleButton>
<ToolbarSeparator />
<ToolbarButton>Copy</ToolbarButton>
<ToolbarButton>Paste</ToolbarButton>
</Toolbar>
</template> API
Toolbar Props
| Prop | Type | Default | Description |
|---|---|---|---|
orientation | 'horizontal' | 'vertical' | 'horizontal' | Direction of the toolbar |
ToolbarToggleButton Events
| Event | Payload | Description |
|---|---|---|
update:pressed | boolean | Emitted when pressed state changes (v-model) |
pressed-change | boolean | Emitted when pressed state changes |
Testing
Tests verify APG compliance across keyboard interaction, ARIA attributes, and accessibility requirements.
Test Categories
High Priority: APG Keyboard Interaction
| 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
| 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)
| 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 |
Medium Priority: Accessibility
| Test | Description |
|---|---|
axe violations | No WCAG 2.1 AA violations (via jest-axe) |
Vertical toolbar | Vertical orientation also passes axe |
Low Priority: HTML Attribute Inheritance
| Test | Description |
|---|---|
className | Custom classes applied to all components |
Testing Tools
- Vitest (opens in new tab) - Test runner
- Testing Library (opens in new tab) - Framework-specific testing utilities
- jest-axe (opens in new tab) - Automated accessibility testing
See testing-strategy.md (opens in new tab) for full documentation.
import { render, screen } from "@testing-library/vue";
import userEvent from "@testing-library/user-event";
import { axe } from "jest-axe";
import { describe, expect, it, vi } from "vitest";
import { h, ref } from "vue";
import Toolbar from "./Toolbar.vue";
import ToolbarButton from "./ToolbarButton.vue";
import ToolbarToggleButton from "./ToolbarToggleButton.vue";
import ToolbarSeparator from "./ToolbarSeparator.vue";
// ヘルパー: Toolbar と子コンポーネントをレンダリング
function renderToolbar(
props: Record<string, unknown> = {},
children: ReturnType<typeof h>[]
) {
return render(Toolbar, {
props,
slots: {
default: () => children,
},
global: {
components: {
ToolbarButton,
ToolbarToggleButton,
ToolbarSeparator,
},
},
});
}
describe("Toolbar (Vue)", () => {
// 🔴 High Priority: APG 準拠の核心
describe("APG: ARIA 属性", () => {
it('role="toolbar" が設定される', () => {
renderToolbar(
{ "aria-label": "Test toolbar" },
[h(ToolbarButton, null, () => "Button")]
);
expect(screen.getByRole("toolbar")).toBeInTheDocument();
});
it('aria-orientation がデフォルトで "horizontal"', () => {
renderToolbar(
{ "aria-label": "Test toolbar" },
[h(ToolbarButton, null, () => "Button")]
);
expect(screen.getByRole("toolbar")).toHaveAttribute(
"aria-orientation",
"horizontal"
);
});
it('aria-orientation が orientation prop を反映する', () => {
renderToolbar(
{ "aria-label": "Test toolbar", orientation: "vertical" },
[h(ToolbarButton, null, () => "Button")]
);
expect(screen.getByRole("toolbar")).toHaveAttribute(
"aria-orientation",
"vertical"
);
});
it("aria-label が透過される", () => {
renderToolbar(
{ "aria-label": "Text formatting" },
[h(ToolbarButton, null, () => "Button")]
);
expect(screen.getByRole("toolbar")).toHaveAttribute(
"aria-label",
"Text formatting"
);
});
});
describe("APG: キーボード操作 (Horizontal)", () => {
it("ArrowRight で次のボタンにフォーカス移動", async () => {
const user = userEvent.setup();
renderToolbar({ "aria-label": "Test toolbar" }, [
h(ToolbarButton, null, () => "First"),
h(ToolbarButton, null, () => "Second"),
h(ToolbarButton, null, () => "Third"),
]);
const firstButton = screen.getByRole("button", { name: "First" });
firstButton.focus();
await user.keyboard("{ArrowRight}");
expect(screen.getByRole("button", { name: "Second" })).toHaveFocus();
});
it("ArrowLeft で前のボタンにフォーカス移動", async () => {
const user = userEvent.setup();
renderToolbar({ "aria-label": "Test toolbar" }, [
h(ToolbarButton, null, () => "First"),
h(ToolbarButton, null, () => "Second"),
h(ToolbarButton, null, () => "Third"),
]);
const secondButton = screen.getByRole("button", { name: "Second" });
secondButton.focus();
await user.keyboard("{ArrowLeft}");
expect(screen.getByRole("button", { name: "First" })).toHaveFocus();
});
it("ArrowRight で最後から先頭にラップしない(端で止まる)", async () => {
const user = userEvent.setup();
renderToolbar({ "aria-label": "Test toolbar" }, [
h(ToolbarButton, null, () => "First"),
h(ToolbarButton, null, () => "Second"),
h(ToolbarButton, null, () => "Third"),
]);
const thirdButton = screen.getByRole("button", { name: "Third" });
thirdButton.focus();
await user.keyboard("{ArrowRight}");
expect(thirdButton).toHaveFocus();
});
it("ArrowLeft で先頭から最後にラップしない(端で止まる)", async () => {
const user = userEvent.setup();
renderToolbar({ "aria-label": "Test toolbar" }, [
h(ToolbarButton, null, () => "First"),
h(ToolbarButton, null, () => "Second"),
]);
const firstButton = screen.getByRole("button", { name: "First" });
firstButton.focus();
await user.keyboard("{ArrowLeft}");
expect(firstButton).toHaveFocus();
});
it("ArrowUp/Down は水平ツールバーでは無効", async () => {
const user = userEvent.setup();
renderToolbar({ "aria-label": "Test toolbar" }, [
h(ToolbarButton, null, () => "First"),
h(ToolbarButton, null, () => "Second"),
]);
const firstButton = screen.getByRole("button", { name: "First" });
firstButton.focus();
await user.keyboard("{ArrowDown}");
expect(firstButton).toHaveFocus();
await user.keyboard("{ArrowUp}");
expect(firstButton).toHaveFocus();
});
it("Home で最初のボタンにフォーカス移動", async () => {
const user = userEvent.setup();
renderToolbar({ "aria-label": "Test toolbar" }, [
h(ToolbarButton, null, () => "First"),
h(ToolbarButton, null, () => "Second"),
h(ToolbarButton, null, () => "Third"),
]);
const thirdButton = screen.getByRole("button", { name: "Third" });
thirdButton.focus();
await user.keyboard("{Home}");
expect(screen.getByRole("button", { name: "First" })).toHaveFocus();
});
it("End で最後のボタンにフォーカス移動", async () => {
const user = userEvent.setup();
renderToolbar({ "aria-label": "Test toolbar" }, [
h(ToolbarButton, null, () => "First"),
h(ToolbarButton, null, () => "Second"),
h(ToolbarButton, null, () => "Third"),
]);
const firstButton = screen.getByRole("button", { name: "First" });
firstButton.focus();
await user.keyboard("{End}");
expect(screen.getByRole("button", { name: "Third" })).toHaveFocus();
});
it("disabled アイテムをスキップして移動", async () => {
const user = userEvent.setup();
renderToolbar({ "aria-label": "Test toolbar" }, [
h(ToolbarButton, null, () => "First"),
h(ToolbarButton, { disabled: true }, () => "Second (disabled)"),
h(ToolbarButton, null, () => "Third"),
]);
const firstButton = screen.getByRole("button", { name: "First" });
firstButton.focus();
await user.keyboard("{ArrowRight}");
expect(screen.getByRole("button", { name: "Third" })).toHaveFocus();
});
});
describe("APG: キーボード操作 (Vertical)", () => {
it("ArrowDown で次のボタンにフォーカス移動", async () => {
const user = userEvent.setup();
renderToolbar(
{ "aria-label": "Test toolbar", orientation: "vertical" },
[
h(ToolbarButton, null, () => "First"),
h(ToolbarButton, null, () => "Second"),
h(ToolbarButton, null, () => "Third"),
]
);
const firstButton = screen.getByRole("button", { name: "First" });
firstButton.focus();
await user.keyboard("{ArrowDown}");
expect(screen.getByRole("button", { name: "Second" })).toHaveFocus();
});
it("ArrowUp で前のボタンにフォーカス移動", async () => {
const user = userEvent.setup();
renderToolbar(
{ "aria-label": "Test toolbar", orientation: "vertical" },
[
h(ToolbarButton, null, () => "First"),
h(ToolbarButton, null, () => "Second"),
h(ToolbarButton, null, () => "Third"),
]
);
const secondButton = screen.getByRole("button", { name: "Second" });
secondButton.focus();
await user.keyboard("{ArrowUp}");
expect(screen.getByRole("button", { name: "First" })).toHaveFocus();
});
it("ArrowLeft/Right は垂直ツールバーでは無効", async () => {
const user = userEvent.setup();
renderToolbar(
{ "aria-label": "Test toolbar", orientation: "vertical" },
[
h(ToolbarButton, null, () => "First"),
h(ToolbarButton, null, () => "Second"),
]
);
const firstButton = screen.getByRole("button", { name: "First" });
firstButton.focus();
await user.keyboard("{ArrowRight}");
expect(firstButton).toHaveFocus();
await user.keyboard("{ArrowLeft}");
expect(firstButton).toHaveFocus();
});
});
});
describe("ToolbarButton (Vue)", () => {
describe("ARIA 属性", () => {
it('role="button" が暗黙的に設定される', () => {
renderToolbar(
{ "aria-label": "Test toolbar" },
[h(ToolbarButton, null, () => "Click me")]
);
expect(screen.getByRole("button", { name: "Click me" })).toBeInTheDocument();
});
it('type="button" が設定される', () => {
renderToolbar(
{ "aria-label": "Test toolbar" },
[h(ToolbarButton, null, () => "Click me")]
);
expect(screen.getByRole("button")).toHaveAttribute("type", "button");
});
});
describe("機能", () => {
it("クリックで click イベントが発火", async () => {
const handleClick = vi.fn();
const user = userEvent.setup();
renderToolbar({ "aria-label": "Test toolbar" }, [
h(ToolbarButton, { onClick: handleClick }, () => "Click me"),
]);
await user.click(screen.getByRole("button"));
expect(handleClick).toHaveBeenCalledTimes(1);
});
it("disabled 時はフォーカス対象外(disabled属性で非フォーカス)", () => {
renderToolbar({ "aria-label": "Test toolbar" }, [
h(ToolbarButton, { disabled: true }, () => "Click me"),
]);
expect(screen.getByRole("button")).toBeDisabled();
});
});
});
describe("ToolbarToggleButton (Vue)", () => {
describe("ARIA 属性", () => {
it('role="button" が暗黙的に設定される', () => {
renderToolbar(
{ "aria-label": "Test toolbar" },
[h(ToolbarToggleButton, null, () => "Toggle")]
);
expect(screen.getByRole("button", { name: "Toggle" })).toBeInTheDocument();
});
it('type="button" が設定される', () => {
renderToolbar(
{ "aria-label": "Test toolbar" },
[h(ToolbarToggleButton, null, () => "Toggle")]
);
expect(screen.getByRole("button")).toHaveAttribute("type", "button");
});
it('aria-pressed="false" が初期状態で設定される', () => {
renderToolbar(
{ "aria-label": "Test toolbar" },
[h(ToolbarToggleButton, null, () => "Toggle")]
);
expect(screen.getByRole("button")).toHaveAttribute("aria-pressed", "false");
});
it('aria-pressed="true" が押下状態で設定される', () => {
renderToolbar(
{ "aria-label": "Test toolbar" },
[h(ToolbarToggleButton, { defaultPressed: true }, () => "Toggle")]
);
expect(screen.getByRole("button")).toHaveAttribute("aria-pressed", "true");
});
});
describe("機能", () => {
it("クリックで aria-pressed がトグル", async () => {
const user = userEvent.setup();
renderToolbar({ "aria-label": "Test toolbar" }, [
h(ToolbarToggleButton, null, () => "Toggle"),
]);
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("Enter で aria-pressed がトグル", async () => {
const user = userEvent.setup();
renderToolbar({ "aria-label": "Test toolbar" }, [
h(ToolbarToggleButton, null, () => "Toggle"),
]);
const button = screen.getByRole("button");
button.focus();
expect(button).toHaveAttribute("aria-pressed", "false");
await user.keyboard("{Enter}");
expect(button).toHaveAttribute("aria-pressed", "true");
});
it("Space で aria-pressed がトグル", async () => {
const user = userEvent.setup();
renderToolbar({ "aria-label": "Test toolbar" }, [
h(ToolbarToggleButton, null, () => "Toggle"),
]);
const button = screen.getByRole("button");
button.focus();
expect(button).toHaveAttribute("aria-pressed", "false");
await user.keyboard(" ");
expect(button).toHaveAttribute("aria-pressed", "true");
});
it("pressed-change イベントが発火", async () => {
const handlePressedChange = vi.fn();
const user = userEvent.setup();
renderToolbar({ "aria-label": "Test toolbar" }, [
h(ToolbarToggleButton, { onPressedChange: handlePressedChange }, () => "Toggle"),
]);
await user.click(screen.getByRole("button"));
expect(handlePressedChange).toHaveBeenCalledWith(true);
await user.click(screen.getByRole("button"));
expect(handlePressedChange).toHaveBeenCalledWith(false);
});
it("disabled 時はトグルしない", async () => {
const user = userEvent.setup();
renderToolbar({ "aria-label": "Test toolbar" }, [
h(ToolbarToggleButton, { disabled: true }, () => "Toggle"),
]);
const button = screen.getByRole("button");
expect(button).toHaveAttribute("aria-pressed", "false");
await user.click(button);
expect(button).toHaveAttribute("aria-pressed", "false");
});
it("disabled 時はフォーカス対象外(disabled属性で非フォーカス)", () => {
renderToolbar({ "aria-label": "Test toolbar" }, [
h(ToolbarToggleButton, { disabled: true }, () => "Toggle"),
]);
expect(screen.getByRole("button")).toBeDisabled();
});
});
});
describe("ToolbarSeparator (Vue)", () => {
describe("ARIA 属性", () => {
it('role="separator" が設定される', () => {
renderToolbar({ "aria-label": "Test toolbar" }, [
h(ToolbarButton, null, () => "Before"),
h(ToolbarSeparator),
h(ToolbarButton, null, () => "After"),
]);
expect(screen.getByRole("separator")).toBeInTheDocument();
});
it('horizontal toolbar 時に aria-orientation="vertical"', () => {
renderToolbar(
{ "aria-label": "Test toolbar", orientation: "horizontal" },
[
h(ToolbarButton, null, () => "Before"),
h(ToolbarSeparator),
h(ToolbarButton, null, () => "After"),
]
);
expect(screen.getByRole("separator")).toHaveAttribute(
"aria-orientation",
"vertical"
);
});
it('vertical toolbar 時に aria-orientation="horizontal"', () => {
renderToolbar(
{ "aria-label": "Test toolbar", orientation: "vertical" },
[
h(ToolbarButton, null, () => "Before"),
h(ToolbarSeparator),
h(ToolbarButton, null, () => "After"),
]
);
expect(screen.getByRole("separator")).toHaveAttribute(
"aria-orientation",
"horizontal"
);
});
});
});
describe("アクセシビリティ (Vue)", () => {
it("axe による WCAG 2.1 AA 違反がない", async () => {
const { container } = renderToolbar({ "aria-label": "Text formatting" }, [
h(ToolbarToggleButton, null, () => "Bold"),
h(ToolbarToggleButton, null, () => "Italic"),
h(ToolbarSeparator),
h(ToolbarButton, null, () => "Copy"),
h(ToolbarButton, null, () => "Paste"),
]);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it("vertical toolbar でも WCAG 2.1 AA 違反がない", async () => {
const { container } = renderToolbar(
{ "aria-label": "Actions", orientation: "vertical" },
[
h(ToolbarButton, null, () => "New"),
h(ToolbarButton, null, () => "Open"),
h(ToolbarSeparator),
h(ToolbarButton, null, () => "Save"),
]
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
});