APG Patterns

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}

Sample text with applied formatting

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

Toolbar.tsx
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.

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

See testing-strategy.md (opens in new tab) for full documentation.

Toolbar.test.tsx
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 ๆบ–ๆ‹ ใฎๆ ธๅฟƒ
  describe("APG: ARIA ๅฑžๆ€ง", () => {
    it('role="toolbar" ใŒ่จญๅฎšใ•ใ‚Œใ‚‹', () => {
      render(
        <Toolbar aria-label="Test toolbar">
          <ToolbarButton>Button</ToolbarButton>
        </Toolbar>
      );
      expect(screen.getByRole("toolbar")).toBeInTheDocument();
    });

    it('aria-orientation ใŒใƒ‡ใƒ•ใ‚ฉใƒซใƒˆใง "horizontal"', () => {
      render(
        <Toolbar aria-label="Test toolbar">
          <ToolbarButton>Button</ToolbarButton>
        </Toolbar>
      );
      expect(screen.getByRole("toolbar")).toHaveAttribute(
        "aria-orientation",
        "horizontal"
      );
    });

    it('aria-orientation ใŒ 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("aria-label ใŒ้€้Žใ•ใ‚Œใ‚‹", () => {
      render(
        <Toolbar aria-label="Text formatting">
          <ToolbarButton>Button</ToolbarButton>
        </Toolbar>
      );
      expect(screen.getByRole("toolbar")).toHaveAttribute(
        "aria-label",
        "Text formatting"
      );
    });

    it("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: ใ‚ญใƒผใƒœใƒผใƒ‰ๆ“ไฝœ (Horizontal)", () => {
    it("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("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("ArrowRight ใงๆœ€ๅพŒใ‹ใ‚‰ๅ…ˆ้ ญใซใƒฉใƒƒใƒ—ใ—ใชใ„๏ผˆ็ซฏใงๆญขใพใ‚‹๏ผ‰", 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("ArrowLeft ใงๅ…ˆ้ ญใ‹ใ‚‰ๆœ€ๅพŒใซใƒฉใƒƒใƒ—ใ—ใชใ„๏ผˆ็ซฏใงๆญขใพใ‚‹๏ผ‰", 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 ใฏๆฐดๅนณใƒ„ใƒผใƒซใƒใƒผใงใฏ็„กๅŠน", 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("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("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("disabled ใ‚ขใ‚คใƒ†ใƒ ใ‚’ใ‚นใ‚ญใƒƒใƒ—ใ—ใฆ็งปๅ‹•", 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: ใ‚ญใƒผใƒœใƒผใƒ‰ๆ“ไฝœ (Vertical)", () => {
    it("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("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 ใฏๅž‚็›ดใƒ„ใƒผใƒซใƒใƒผใงใฏ็„กๅŠน", 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("ArrowDown ใง็ซฏใงๆญขใพใ‚‹๏ผˆใƒฉใƒƒใƒ—ใ—ใชใ„๏ผ‰", 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: ใƒ•ใ‚ฉใƒผใ‚ซใ‚น็ฎก็†", () => {
    it("ๆœ€ๅˆใฎๆœ‰ๅŠนใชใ‚ขใ‚คใƒ†ใƒ ใŒ tabIndex=0ใ€ไป–ใฏ 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("ใ‚ฏใƒชใƒƒใ‚ฏใงใƒ•ใ‚ฉใƒผใ‚ซใ‚นไฝ็ฝฎใŒๆ›ดๆ–ฐใ•ใ‚Œใ‚‹", 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 ๅฑžๆ€ง", () => {
    it('role="button" ใŒๆš—้ป™็š„ใซ่จญๅฎšใ•ใ‚Œใ‚‹', () => {
      render(
        <Toolbar aria-label="Test toolbar">
          <ToolbarButton>Click me</ToolbarButton>
        </Toolbar>
      );
      expect(screen.getByRole("button", { name: "Click me" })).toBeInTheDocument();
    });

    it('type="button" ใŒ่จญๅฎšใ•ใ‚Œใ‚‹', () => {
      render(
        <Toolbar aria-label="Test toolbar">
          <ToolbarButton>Click me</ToolbarButton>
        </Toolbar>
      );
      expect(screen.getByRole("button")).toHaveAttribute("type", "button");
    });
  });

  describe("ๆฉŸ่ƒฝ", () => {
    it("ใ‚ฏใƒชใƒƒใ‚ฏใง onClick ใŒ็™บ็ซ", 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("Enter ใง onClick ใŒ็™บ็ซ", 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("Space ใง onClick ใŒ็™บ็ซ", 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("disabled ๆ™‚ใฏ onClick ใŒ็™บ็ซใ—ใชใ„", 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("disabled ๆ™‚ใฏใƒ•ใ‚ฉใƒผใ‚ซใ‚นๅฏพ่ฑกๅค–๏ผˆdisabledๅฑžๆ€งใง้žใƒ•ใ‚ฉใƒผใ‚ซใ‚น๏ผ‰", () => {
      render(
        <Toolbar aria-label="Test toolbar">
          <ToolbarButton disabled>Click me</ToolbarButton>
        </Toolbar>
      );
      expect(screen.getByRole("button")).toBeDisabled();
    });
  });
});

describe("ToolbarToggleButton", () => {
  describe("ARIA ๅฑžๆ€ง", () => {
    it('role="button" ใŒๆš—้ป™็š„ใซ่จญๅฎšใ•ใ‚Œใ‚‹', () => {
      render(
        <Toolbar aria-label="Test toolbar">
          <ToolbarToggleButton>Toggle</ToolbarToggleButton>
        </Toolbar>
      );
      expect(screen.getByRole("button", { name: "Toggle" })).toBeInTheDocument();
    });

    it('type="button" ใŒ่จญๅฎšใ•ใ‚Œใ‚‹', () => {
      render(
        <Toolbar aria-label="Test toolbar">
          <ToolbarToggleButton>Toggle</ToolbarToggleButton>
        </Toolbar>
      );
      expect(screen.getByRole("button")).toHaveAttribute("type", "button");
    });

    it('aria-pressed="false" ใŒๅˆๆœŸ็Šถๆ…‹ใง่จญๅฎšใ•ใ‚Œใ‚‹', () => {
      render(
        <Toolbar aria-label="Test toolbar">
          <ToolbarToggleButton>Toggle</ToolbarToggleButton>
        </Toolbar>
      );
      expect(screen.getByRole("button")).toHaveAttribute("aria-pressed", "false");
    });

    it('aria-pressed="true" ใŒๆŠผไธ‹็Šถๆ…‹ใง่จญๅฎšใ•ใ‚Œใ‚‹', () => {
      render(
        <Toolbar aria-label="Test toolbar">
          <ToolbarToggleButton defaultPressed>Toggle</ToolbarToggleButton>
        </Toolbar>
      );
      expect(screen.getByRole("button")).toHaveAttribute("aria-pressed", "true");
    });
  });

  describe("ๆฉŸ่ƒฝ", () => {
    it("ใ‚ฏใƒชใƒƒใ‚ฏใง aria-pressed ใŒใƒˆใ‚ฐใƒซ", 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("Enter ใง aria-pressed ใŒใƒˆใ‚ฐใƒซ", 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("Space ใง aria-pressed ใŒใƒˆใ‚ฐใƒซ", 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("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("defaultPressed ใงๅˆๆœŸ็Šถๆ…‹ใ‚’่จญๅฎš", () => {
      render(
        <Toolbar aria-label="Test toolbar">
          <ToolbarToggleButton defaultPressed>Toggle</ToolbarToggleButton>
        </Toolbar>
      );
      expect(screen.getByRole("button")).toHaveAttribute("aria-pressed", "true");
    });

    it("pressed ใงๅˆถๅพกใ•ใ‚ŒใŸ็Šถๆ…‹", 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("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("disabled ๆ™‚ใฏ onPressedChange ใŒ็™บ็ซใ—ใชใ„", 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("disabled ๆ™‚ใฏใƒ•ใ‚ฉใƒผใ‚ซใ‚นๅฏพ่ฑกๅค–๏ผˆdisabledๅฑžๆ€งใง้žใƒ•ใ‚ฉใƒผใ‚ซใ‚น๏ผ‰", () => {
      render(
        <Toolbar aria-label="Test toolbar">
          <ToolbarToggleButton disabled>Toggle</ToolbarToggleButton>
        </Toolbar>
      );
      expect(screen.getByRole("button")).toBeDisabled();
    });
  });
});

describe("ToolbarSeparator", () => {
  describe("ARIA ๅฑžๆ€ง", () => {
    it('role="separator" ใŒ่จญๅฎšใ•ใ‚Œใ‚‹', () => {
      render(
        <Toolbar aria-label="Test toolbar">
          <ToolbarButton>Before</ToolbarButton>
          <ToolbarSeparator />
          <ToolbarButton>After</ToolbarButton>
        </Toolbar>
      );
      expect(screen.getByRole("separator")).toBeInTheDocument();
    });

    it('horizontal toolbar ๆ™‚ใซ aria-orientation="vertical"', () => {
      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('vertical toolbar ๆ™‚ใซ aria-orientation="horizontal"', () => {
      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("ใ‚ขใ‚ฏใ‚ปใ‚ทใƒ“ใƒชใƒ†ใ‚ฃ", () => {
  it("axe ใซใ‚ˆใ‚‹ WCAG 2.1 AA ้•ๅใŒใชใ„", 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("vertical toolbar ใงใ‚‚ WCAG 2.1 AA ้•ๅใŒใชใ„", 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 ๅฑžๆ€ง็ถ™ๆ‰ฟ", () => {
  it("className ใŒใ‚ณใƒณใƒ†ใƒŠใซ้ฉ็”จใ•ใ‚Œใ‚‹", () => {
    render(
      <Toolbar aria-label="Test toolbar" className="custom-toolbar">
        <ToolbarButton>Button</ToolbarButton>
      </Toolbar>
    );
    expect(screen.getByRole("toolbar")).toHaveClass("custom-toolbar");
  });

  it("ToolbarButton ใฎ className ใŒ้ฉ็”จใ•ใ‚Œใ‚‹", () => {
    render(
      <Toolbar aria-label="Test toolbar">
        <ToolbarButton className="custom-button">Button</ToolbarButton>
      </Toolbar>
    );
    expect(screen.getByRole("button")).toHaveClass("custom-button");
  });

  it("ToolbarToggleButton ใฎ className ใŒ้ฉ็”จใ•ใ‚Œใ‚‹", () => {
    render(
      <Toolbar aria-label="Test toolbar">
        <ToolbarToggleButton className="custom-toggle">Toggle</ToolbarToggleButton>
      </Toolbar>
    );
    expect(screen.getByRole("button")).toHaveClass("custom-toggle");
  });

  it("ToolbarSeparator ใฎ className ใŒ้ฉ็”จใ•ใ‚Œใ‚‹", () => {
    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