APG Patterns

Tabs

A set of layered sections of content, known as tab panels, that display one panel of content at a time.

Demo

Automatic Activation (Default)

Tabs are activated automatically when focused with arrow keys.

This is the overview panel content. It provides a general introduction to the product or service.

Manual Activation

Tabs require Enter or Space to activate after focusing.

Content for tab one. Press Enter or Space to activate tabs.

Vertical Orientation

Tabs arranged vertically with Up/Down arrow navigation.

Configure your application settings here.

Accessibility Features

WAI-ARIA Roles

Role Target Element Description
tablist Container Container for tab elements
tab Each tab Individual tab element
tabpanel Panel Content area for each tab

WAI-ARIA tablist role (opens in new tab)

WAI-ARIA Properties

Attribute Target Values Required Configuration
aria-orientation tablist "horizontal" | "vertical" No orientation prop
aria-controls tab ID reference to associated panel Yes Auto-generated
aria-labelledby tabpanel ID reference to associated tab Yes Auto-generated

WAI-ARIA States

aria-selected

Indicates the currently active tab.

Target tab element
Values true | false
Required Yes
Change Trigger Tab click, Arrow keys (automatic), Enter/Space (manual)
Reference aria-selected (opens in new tab)

Keyboard Support

Key Action
Tab Move focus into/out of the tablist
Arrow Right / Arrow Left Navigate between tabs (horizontal)
Arrow Down / Arrow Up Navigate between tabs (vertical)
Home Move focus to first tab
End Move focus to last tab
Enter / Space Activate tab (manual mode only)

Source Code

Tabs.tsx
import {
  useCallback,
  useEffect,
  useId,
  useMemo,
  useRef,
  useState,
} from "react";

export interface TabItem {
  id: string;
  label: string;
  content: React.ReactNode;
  disabled?: boolean;
}

export interface TabsProps {
  /** Array of tab items */
  tabs: TabItem[];
  /** Initially selected tab ID */
  defaultSelectedId?: string;
  /** Orientation of the tabs */
  orientation?: "horizontal" | "vertical";
  /** Activation mode */
  activation?: "automatic" | "manual";
  /** Callback when tab selection changes */
  onSelectionChange?: (tabId: string) => void;
  /** Additional CSS class */
  className?: string;
}

export function Tabs({
  tabs,
  defaultSelectedId,
  orientation = "horizontal",
  activation = "automatic",
  onSelectionChange,
  className = "",
}: TabsProps): React.ReactElement {
  // availableTabsใฎๅฎ‰ๅฎšๅŒ–๏ผˆใƒ‘ใƒ•ใ‚ฉใƒผใƒžใƒณใ‚นๆœ€้ฉๅŒ–๏ผ‰
  const availableTabs = useMemo(
    () => tabs.filter((tab) => !tab.disabled),
    [tabs]
  );

  const initialTab = defaultSelectedId
    ? availableTabs.find((tab) => tab.id === defaultSelectedId)
    : availableTabs[0];

  const [selectedId, setSelectedId] = useState(
    initialTab?.id || availableTabs[0]?.id
  );
  const [focusedIndex, setFocusedIndex] = useState(() => {
    const index = availableTabs.findIndex((tab) => tab.id === initialTab?.id);
    return index >= 0 ? index : 0;
  });

  const tablistRef = useRef<HTMLDivElement>(null);
  const tabRefs = useRef<Map<string, HTMLButtonElement>>(new Map());

  const tablistId = useId();

  const handleTabSelection = useCallback(
    (tabId: string) => {
      setSelectedId(tabId);
      onSelectionChange?.(tabId);
    },
    [onSelectionChange]
  );

  const handleTabFocus = useCallback(
    (index: number) => {
      setFocusedIndex(index);
      const tab = availableTabs[index];
      if (tab) {
        tabRefs.current.get(tab.id)?.focus();
      }
    },
    [availableTabs]
  );

  const handleKeyDown = useCallback(
    (event: React.KeyboardEvent) => {
      const { key } = event;
      const target = event.target;
      if (
        !tablistRef.current ||
        !(target instanceof Node) ||
        !tablistRef.current.contains(target)
      ) {
        return;
      }

      let newIndex = focusedIndex;
      let shouldPreventDefault = false;

      switch (key) {
        case "ArrowRight":
        case "ArrowDown":
          newIndex = (focusedIndex + 1) % availableTabs.length;
          shouldPreventDefault = true;
          break;

        case "ArrowLeft":
        case "ArrowUp":
          newIndex =
            (focusedIndex - 1 + availableTabs.length) % availableTabs.length;
          shouldPreventDefault = true;
          break;

        case "Home":
          newIndex = 0;
          shouldPreventDefault = true;
          break;

        case "End":
          newIndex = availableTabs.length - 1;
          shouldPreventDefault = true;
          break;

        case "Enter":
        case " ":
          if (activation === "manual") {
            const focusedTab = availableTabs[focusedIndex];
            if (focusedTab) {
              handleTabSelection(focusedTab.id);
            }
          }
          shouldPreventDefault = true;
          break;
      }

      if (shouldPreventDefault) {
        event.preventDefault();

        if (newIndex !== focusedIndex) {
          handleTabFocus(newIndex);

          if (activation === "automatic") {
            const newTab = availableTabs[newIndex];
            if (newTab) {
              handleTabSelection(newTab.id);
            }
          }
        }
      }
    },
    [focusedIndex, availableTabs, activation, handleTabSelection, handleTabFocus]
  );

  // ใƒ•ใ‚ฉใƒผใ‚ซใ‚นๅŒๆœŸ๏ผˆActivation mode่€ƒๆ…ฎ๏ผ‰
  useEffect(() => {
    if (activation === "manual") {
      // Manual: tabsใฎๅค‰ๆ›ดใซใ‚ˆใ‚Š็ฏ„ๅ›ฒๅค–ใซใชใฃใŸๅ ดๅˆใฎใฟไฟฎๆญฃ
      if (focusedIndex >= availableTabs.length) {
        setFocusedIndex(Math.max(0, availableTabs.length - 1));
      }
      return;
    }

    // Automatic: ้ธๆŠžใซ่ฟฝๅพ“
    const selectedIndex = availableTabs.findIndex(
      (tab) => tab.id === selectedId
    );
    if (selectedIndex >= 0 && selectedIndex !== focusedIndex) {
      setFocusedIndex(selectedIndex);
    }
  }, [selectedId, availableTabs, activation, focusedIndex]);

  const containerClass = `apg-tabs ${
    orientation === "vertical" ? "apg-tabs--vertical" : "apg-tabs--horizontal"
  } ${className}`.trim();

  const tablistClass = `apg-tablist ${
    orientation === "vertical"
      ? "apg-tablist--vertical"
      : "apg-tablist--horizontal"
  }`;

  return (
    <div className={containerClass}>
      <div
        ref={tablistRef}
        role="tablist"
        aria-orientation={orientation}
        className={tablistClass}
        onKeyDown={handleKeyDown}
      >
        {tabs.map((tab) => {
          const isSelected = tab.id === selectedId;
          // APGๆบ–ๆ‹ : Manual Activationใงใฏใƒ•ใ‚ฉใƒผใ‚ซใ‚นไฝ็ฝฎใงtabIndexใ‚’ๅˆถๅพก
          const isFocusTarget =
            activation === "manual"
              ? tab.id === availableTabs[focusedIndex]?.id
              : isSelected;
          const tabIndex = tab.disabled ? -1 : isFocusTarget ? 0 : -1;
          const tabPanelId = `${tablistId}-panel-${tab.id}`;

          const tabClass = `apg-tab ${
            orientation === "vertical"
              ? "apg-tab--vertical"
              : "apg-tab--horizontal"
          } ${isSelected ? "apg-tab--selected" : ""} ${
            tab.disabled ? "apg-tab--disabled" : ""
          }`.trim();

          return (
            <button
              key={tab.id}
              ref={(el) => {
                if (el) {
                  tabRefs.current.set(tab.id, el);
                } else {
                  tabRefs.current.delete(tab.id);
                }
              }}
              role="tab"
              type="button"
              id={`${tablistId}-tab-${tab.id}`}
              aria-selected={isSelected}
              aria-controls={isSelected ? tabPanelId : undefined}
              tabIndex={tabIndex}
              disabled={tab.disabled}
              className={tabClass}
              onClick={() => !tab.disabled && handleTabSelection(tab.id)}
            >
              <span className="apg-tab-label">{tab.label}</span>
            </button>
          );
        })}
      </div>

      <div className="apg-tabpanels">
        {tabs.map((tab) => {
          const isSelected = tab.id === selectedId;
          const tabPanelId = `${tablistId}-panel-${tab.id}`;

          return (
            <div
              key={tab.id}
              role="tabpanel"
              id={tabPanelId}
              aria-labelledby={`${tablistId}-tab-${tab.id}`}
              hidden={!isSelected}
              className={`apg-tabpanel ${
                isSelected ? "apg-tabpanel--active" : "apg-tabpanel--inactive"
              }`}
              tabIndex={isSelected ? 0 : -1}
            >
              {tab.content}
            </div>
          );
        })}
      </div>
    </div>
  );
}

export default Tabs;

Usage

Example
import { Tabs } from './Tabs';

const tabs = [
  { id: 'tab1', label: 'First', content: 'First panel content' },
  { id: 'tab2', label: 'Second', content: 'Second panel content' },
  { id: 'tab3', label: 'Third', content: 'Third panel content' }
];

function App() {
  return (
    <Tabs
      tabs={tabs}
      defaultSelectedId="tab1"
      onSelectionChange={(id) => console.log('Tab changed:', id)}
    />
  );
}

API

Tabs Props

Prop Type Default Description
tabs TabItem[] required Array of tab items
defaultSelectedId string first tab ID of the initially selected tab
orientation 'horizontal' | 'vertical' 'horizontal' Tab layout direction
activation 'automatic' | 'manual' 'automatic' How tabs are activated
onSelectionChange (tabId: string) => void - Callback when tab changes

TabItem Interface

Types
interface TabItem {
  id: string;
  label: string;
  content: React.ReactNode;
  disabled?: boolean;
}

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 tabs (horizontal)
ArrowDown/Up Moves focus between tabs (vertical orientation)
Home/End Moves focus to first/last tab
Loop navigation Arrow keys loop from last to first and vice versa
Disabled skip Skips disabled tabs during navigation
Automatic activation Tab panel changes on focus (default mode)
Manual activation Enter/Space required to activate tab

High Priority: APG ARIA Attributes

Test Description
role="tablist" Container has tablist role
role="tab" Each tab button has tab role
role="tabpanel" Content panel has tabpanel role
aria-selected Selected tab has aria-selected="true"
aria-controls Tab references its panel via aria-controls
aria-labelledby Panel references its tab via aria-labelledby
aria-orientation Reflects horizontal/vertical orientation

High Priority: Focus Management (Roving Tabindex)

Test Description
tabIndex=0 Selected tab has tabIndex=0
tabIndex=-1 Non-selected tabs have tabIndex=-1
Tab to panel Tab key moves focus from tablist to panel
Panel focusable Panel has tabIndex=0 for focus

Medium Priority: Accessibility

Test Description
axe violations No WCAG 2.1 AA violations (via jest-axe)

Testing Tools

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

Tabs.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 { Tabs, type TabItem } from "./Tabs";

// ใƒ†ใ‚นใƒˆ็”จใฎใ‚ฟใƒ–ใƒ‡ใƒผใ‚ฟ
const defaultTabs: TabItem[] = [
  { id: "tab1", label: "Tab 1", content: "Content 1" },
  { id: "tab2", label: "Tab 2", content: "Content 2" },
  { id: "tab3", label: "Tab 3", content: "Content 3" },
];

const tabsWithDisabled: TabItem[] = [
  { id: "tab1", label: "Tab 1", content: "Content 1" },
  { id: "tab2", label: "Tab 2", content: "Content 2", disabled: true },
  { id: "tab3", label: "Tab 3", content: "Content 3" },
];

describe("Tabs", () => {
  // ๐Ÿ”ด High Priority: APG ๆบ–ๆ‹ ใฎๆ ธๅฟƒ
  describe("APG: ใ‚ญใƒผใƒœใƒผใƒ‰ๆ“ไฝœ (Horizontal)", () => {
    describe("Automatic Activation", () => {
      it("ArrowRight ใงๆฌกใฎใ‚ฟใƒ–ใซ็งปๅ‹•ใƒป้ธๆŠžใ™ใ‚‹", async () => {
        const user = userEvent.setup();
        render(<Tabs tabs={defaultTabs} />);

        const tab1 = screen.getByRole("tab", { name: "Tab 1" });
        tab1.focus();

        await user.keyboard("{ArrowRight}");

        const tab2 = screen.getByRole("tab", { name: "Tab 2" });
        expect(tab2).toHaveFocus();
        expect(tab2).toHaveAttribute("aria-selected", "true");
        expect(screen.getByRole("tabpanel")).toHaveTextContent("Content 2");
      });

      it("ArrowLeft ใงๅ‰ใฎใ‚ฟใƒ–ใซ็งปๅ‹•ใƒป้ธๆŠžใ™ใ‚‹", async () => {
        const user = userEvent.setup();
        render(<Tabs tabs={defaultTabs} defaultSelectedId="tab2" />);

        const tab2 = screen.getByRole("tab", { name: "Tab 2" });
        tab2.focus();

        await user.keyboard("{ArrowLeft}");

        const tab1 = screen.getByRole("tab", { name: "Tab 1" });
        expect(tab1).toHaveFocus();
        expect(tab1).toHaveAttribute("aria-selected", "true");
      });

      it("ArrowRight ใงๆœ€ๅพŒใ‹ใ‚‰ๆœ€ๅˆใซใƒซใƒผใƒ—ใ™ใ‚‹", async () => {
        const user = userEvent.setup();
        render(<Tabs tabs={defaultTabs} defaultSelectedId="tab3" />);

        const tab3 = screen.getByRole("tab", { name: "Tab 3" });
        tab3.focus();

        await user.keyboard("{ArrowRight}");

        const tab1 = screen.getByRole("tab", { name: "Tab 1" });
        expect(tab1).toHaveFocus();
        expect(tab1).toHaveAttribute("aria-selected", "true");
      });

      it("ArrowLeft ใงๆœ€ๅˆใ‹ใ‚‰ๆœ€ๅพŒใซใƒซใƒผใƒ—ใ™ใ‚‹", async () => {
        const user = userEvent.setup();
        render(<Tabs tabs={defaultTabs} />);

        const tab1 = screen.getByRole("tab", { name: "Tab 1" });
        tab1.focus();

        await user.keyboard("{ArrowLeft}");

        const tab3 = screen.getByRole("tab", { name: "Tab 3" });
        expect(tab3).toHaveFocus();
        expect(tab3).toHaveAttribute("aria-selected", "true");
      });

      it("Home ใงๆœ€ๅˆใฎใ‚ฟใƒ–ใซ็งปๅ‹•ใƒป้ธๆŠžใ™ใ‚‹", async () => {
        const user = userEvent.setup();
        render(<Tabs tabs={defaultTabs} defaultSelectedId="tab3" />);

        const tab3 = screen.getByRole("tab", { name: "Tab 3" });
        tab3.focus();

        await user.keyboard("{Home}");

        const tab1 = screen.getByRole("tab", { name: "Tab 1" });
        expect(tab1).toHaveFocus();
        expect(tab1).toHaveAttribute("aria-selected", "true");
      });

      it("End ใงๆœ€ๅพŒใฎใ‚ฟใƒ–ใซ็งปๅ‹•ใƒป้ธๆŠžใ™ใ‚‹", async () => {
        const user = userEvent.setup();
        render(<Tabs tabs={defaultTabs} />);

        const tab1 = screen.getByRole("tab", { name: "Tab 1" });
        tab1.focus();

        await user.keyboard("{End}");

        const tab3 = screen.getByRole("tab", { name: "Tab 3" });
        expect(tab3).toHaveFocus();
        expect(tab3).toHaveAttribute("aria-selected", "true");
      });

      it("disabled ใ‚ฟใƒ–ใ‚’ใ‚นใ‚ญใƒƒใƒ—ใ—ใฆๆฌกใฎๆœ‰ๅŠนใชใ‚ฟใƒ–ใซ็งปๅ‹•ใ™ใ‚‹", async () => {
        const user = userEvent.setup();
        render(<Tabs tabs={tabsWithDisabled} />);

        const tab1 = screen.getByRole("tab", { name: "Tab 1" });
        tab1.focus();

        await user.keyboard("{ArrowRight}");

        // Tab 2 ใฏใ‚นใ‚ญใƒƒใƒ—ใ•ใ‚Œใ€Tab 3 ใซ็งปๅ‹•
        const tab3 = screen.getByRole("tab", { name: "Tab 3" });
        expect(tab3).toHaveFocus();
        expect(tab3).toHaveAttribute("aria-selected", "true");
      });
    });

    describe("Manual Activation", () => {
      it("็Ÿขๅฐใ‚ญใƒผใงใƒ•ใ‚ฉใƒผใ‚ซใ‚น็งปๅ‹•ใ™ใ‚‹ใŒใƒ‘ใƒใƒซใฏๅˆ‡ใ‚Šๆ›ฟใ‚ใ‚‰ใชใ„", async () => {
        const user = userEvent.setup();
        render(<Tabs tabs={defaultTabs} activation="manual" />);

        const tab1 = screen.getByRole("tab", { name: "Tab 1" });
        tab1.focus();

        await user.keyboard("{ArrowRight}");

        const tab2 = screen.getByRole("tab", { name: "Tab 2" });
        expect(tab2).toHaveFocus();
        // ใƒ‘ใƒใƒซใฏๅˆ‡ใ‚Šๆ›ฟใ‚ใ‚‰ใชใ„
        expect(tab1).toHaveAttribute("aria-selected", "true");
        expect(tab2).toHaveAttribute("aria-selected", "false");
        expect(screen.getByRole("tabpanel")).toHaveTextContent("Content 1");
      });

      it("Enter ใงใƒ•ใ‚ฉใƒผใ‚ซใ‚นไธญใฎใ‚ฟใƒ–ใ‚’้ธๆŠžใ™ใ‚‹", async () => {
        const user = userEvent.setup();
        render(<Tabs tabs={defaultTabs} activation="manual" />);

        const tab1 = screen.getByRole("tab", { name: "Tab 1" });
        tab1.focus();

        await user.keyboard("{ArrowRight}");
        await user.keyboard("{Enter}");

        const tab2 = screen.getByRole("tab", { name: "Tab 2" });
        expect(tab2).toHaveAttribute("aria-selected", "true");
        expect(screen.getByRole("tabpanel")).toHaveTextContent("Content 2");
      });

      it("Space ใงใƒ•ใ‚ฉใƒผใ‚ซใ‚นไธญใฎใ‚ฟใƒ–ใ‚’้ธๆŠžใ™ใ‚‹", async () => {
        const user = userEvent.setup();
        render(<Tabs tabs={defaultTabs} activation="manual" />);

        const tab1 = screen.getByRole("tab", { name: "Tab 1" });
        tab1.focus();

        await user.keyboard("{ArrowRight}");
        await user.keyboard(" ");

        const tab2 = screen.getByRole("tab", { name: "Tab 2" });
        expect(tab2).toHaveAttribute("aria-selected", "true");
        expect(screen.getByRole("tabpanel")).toHaveTextContent("Content 2");
      });
    });
  });

  describe("APG: ใ‚ญใƒผใƒœใƒผใƒ‰ๆ“ไฝœ (Vertical)", () => {
    it("ArrowDown ใงๆฌกใฎใ‚ฟใƒ–ใซ็งปๅ‹•ใƒป้ธๆŠžใ™ใ‚‹", async () => {
      const user = userEvent.setup();
      render(<Tabs tabs={defaultTabs} orientation="vertical" />);

      const tab1 = screen.getByRole("tab", { name: "Tab 1" });
      tab1.focus();

      await user.keyboard("{ArrowDown}");

      const tab2 = screen.getByRole("tab", { name: "Tab 2" });
      expect(tab2).toHaveFocus();
      expect(tab2).toHaveAttribute("aria-selected", "true");
    });

    it("ArrowUp ใงๅ‰ใฎใ‚ฟใƒ–ใซ็งปๅ‹•ใƒป้ธๆŠžใ™ใ‚‹", async () => {
      const user = userEvent.setup();
      render(
        <Tabs tabs={defaultTabs} orientation="vertical" defaultSelectedId="tab2" />
      );

      const tab2 = screen.getByRole("tab", { name: "Tab 2" });
      tab2.focus();

      await user.keyboard("{ArrowUp}");

      const tab1 = screen.getByRole("tab", { name: "Tab 1" });
      expect(tab1).toHaveFocus();
      expect(tab1).toHaveAttribute("aria-selected", "true");
    });

    it("ArrowDown/Up ใงใƒซใƒผใƒ—ใ™ใ‚‹", async () => {
      const user = userEvent.setup();
      render(
        <Tabs tabs={defaultTabs} orientation="vertical" defaultSelectedId="tab3" />
      );

      const tab3 = screen.getByRole("tab", { name: "Tab 3" });
      tab3.focus();

      await user.keyboard("{ArrowDown}");

      const tab1 = screen.getByRole("tab", { name: "Tab 1" });
      expect(tab1).toHaveFocus();
    });
  });

  describe("APG: ARIA ๅฑžๆ€ง", () => {
    it('tablist ใŒ role="tablist" ใ‚’ๆŒใค', () => {
      render(<Tabs tabs={defaultTabs} />);
      expect(screen.getByRole("tablist")).toBeInTheDocument();
    });

    it('ๅ„ใ‚ฟใƒ–ใŒ role="tab" ใ‚’ๆŒใค', () => {
      render(<Tabs tabs={defaultTabs} />);
      const tabs = screen.getAllByRole("tab");
      expect(tabs).toHaveLength(3);
    });

    it('ๅ„ใƒ‘ใƒใƒซใŒ role="tabpanel" ใ‚’ๆŒใค', () => {
      render(<Tabs tabs={defaultTabs} />);
      // ้ธๆŠžไธญใฎใƒ‘ใƒใƒซใฎใฟ่กจ็คบ
      expect(screen.getByRole("tabpanel")).toBeInTheDocument();
    });

    it('้ธๆŠžไธญใ‚ฟใƒ–ใŒ aria-selected="true"ใ€้ž้ธๆŠžใŒ "false"', () => {
      render(<Tabs tabs={defaultTabs} />);
      const tabs = screen.getAllByRole("tab");

      expect(tabs[0]).toHaveAttribute("aria-selected", "true");
      expect(tabs[1]).toHaveAttribute("aria-selected", "false");
      expect(tabs[2]).toHaveAttribute("aria-selected", "false");
    });

    it("้ธๆŠžไธญใ‚ฟใƒ–ใฎ aria-controls ใŒใƒ‘ใƒใƒซ id ใจไธ€่‡ด", () => {
      render(<Tabs tabs={defaultTabs} />);
      const selectedTab = screen.getByRole("tab", { name: "Tab 1" });
      const tabpanel = screen.getByRole("tabpanel");

      const ariaControls = selectedTab.getAttribute("aria-controls");
      expect(ariaControls).toBe(tabpanel.id);
    });

    it("ใƒ‘ใƒใƒซใฎ aria-labelledby ใŒใ‚ฟใƒ– id ใจไธ€่‡ด", () => {
      render(<Tabs tabs={defaultTabs} />);
      const selectedTab = screen.getByRole("tab", { name: "Tab 1" });
      const tabpanel = screen.getByRole("tabpanel");

      const ariaLabelledby = tabpanel.getAttribute("aria-labelledby");
      expect(ariaLabelledby).toBe(selectedTab.id);
    });

    it("aria-orientation ใŒ orientation prop ใ‚’ๅๆ˜ ใ™ใ‚‹", () => {
      const { rerender } = render(<Tabs tabs={defaultTabs} />);
      expect(screen.getByRole("tablist")).toHaveAttribute(
        "aria-orientation",
        "horizontal"
      );

      rerender(<Tabs tabs={defaultTabs} orientation="vertical" />);
      expect(screen.getByRole("tablist")).toHaveAttribute(
        "aria-orientation",
        "vertical"
      );
    });
  });

  describe("APG: ใƒ•ใ‚ฉใƒผใ‚ซใ‚น็ฎก็† (Roving Tabindex)", () => {
    it("Automatic: ้ธๆŠžไธญใ‚ฟใƒ–ใฎใฟ tabIndex=0", () => {
      render(<Tabs tabs={defaultTabs} />);
      const tabs = screen.getAllByRole("tab");

      expect(tabs[0]).toHaveAttribute("tabIndex", "0");
      expect(tabs[1]).toHaveAttribute("tabIndex", "-1");
      expect(tabs[2]).toHaveAttribute("tabIndex", "-1");
    });

    it("Manual: ใƒ•ใ‚ฉใƒผใ‚ซใ‚นไธญใ‚ฟใƒ–ใŒ tabIndex=0", async () => {
      const user = userEvent.setup();
      render(<Tabs tabs={defaultTabs} activation="manual" />);

      const tab1 = screen.getByRole("tab", { name: "Tab 1" });
      tab1.focus();

      await user.keyboard("{ArrowRight}");

      const tabs = screen.getAllByRole("tab");
      // Manual ใงใฏ้ธๆŠžไธญใงใฏใชใใƒ•ใ‚ฉใƒผใ‚ซใ‚นไธญใฎใ‚ฟใƒ–ใŒ tabIndex=0
      expect(tabs[0]).toHaveAttribute("tabIndex", "-1");
      expect(tabs[1]).toHaveAttribute("tabIndex", "0");
      expect(tabs[2]).toHaveAttribute("tabIndex", "-1");
    });

    it("Tab ใ‚ญใƒผใง tabpanel ใซ็งปๅ‹•ใงใใ‚‹", async () => {
      const user = userEvent.setup();
      render(<Tabs tabs={defaultTabs} />);

      const tab1 = screen.getByRole("tab", { name: "Tab 1" });
      tab1.focus();

      await user.tab();

      expect(screen.getByRole("tabpanel")).toHaveFocus();
    });

    it("tabpanel ใŒ tabIndex=0 ใงใƒ•ใ‚ฉใƒผใ‚ซใ‚นๅฏ่ƒฝ", () => {
      render(<Tabs tabs={defaultTabs} />);
      const tabpanel = screen.getByRole("tabpanel");
      expect(tabpanel).toHaveAttribute("tabIndex", "0");
    });
  });

  // ๐ŸŸก Medium Priority: ใ‚ขใ‚ฏใ‚ปใ‚ทใƒ“ใƒชใƒ†ใ‚ฃๆคœ่จผ
  describe("ใ‚ขใ‚ฏใ‚ปใ‚ทใƒ“ใƒชใƒ†ใ‚ฃ", () => {
    it("axe ใซใ‚ˆใ‚‹ WCAG 2.1 AA ้•ๅใŒใชใ„", async () => {
      const { container } = render(<Tabs tabs={defaultTabs} />);
      const results = await axe(container);
      expect(results).toHaveNoViolations();
    });
  });

  describe("Props", () => {
    it("defaultSelectedId ใงๅˆๆœŸ้ธๆŠžใ‚ฟใƒ–ใ‚’ๆŒ‡ๅฎšใงใใ‚‹", () => {
      render(<Tabs tabs={defaultTabs} defaultSelectedId="tab2" />);

      const tab2 = screen.getByRole("tab", { name: "Tab 2" });
      expect(tab2).toHaveAttribute("aria-selected", "true");
      expect(screen.getByRole("tabpanel")).toHaveTextContent("Content 2");
    });

    it("onSelectionChange ใŒใ‚ฟใƒ–้ธๆŠžๆ™‚ใซๅ‘ผใณๅ‡บใ•ใ‚Œใ‚‹", async () => {
      const handleSelectionChange = vi.fn();
      const user = userEvent.setup();
      render(
        <Tabs tabs={defaultTabs} onSelectionChange={handleSelectionChange} />
      );

      await user.click(screen.getByRole("tab", { name: "Tab 2" }));

      expect(handleSelectionChange).toHaveBeenCalledWith("tab2");
    });
  });

  describe("็•ฐๅธธ็ณป", () => {
    it("defaultSelectedId ใŒๅญ˜ๅœจใ—ใชใ„ๅ ดๅˆใ€ๆœ€ๅˆใฎใ‚ฟใƒ–ใŒ้ธๆŠžใ•ใ‚Œใ‚‹", () => {
      render(<Tabs tabs={defaultTabs} defaultSelectedId="nonexistent" />);

      const tab1 = screen.getByRole("tab", { name: "Tab 1" });
      expect(tab1).toHaveAttribute("aria-selected", "true");
    });
  });

  // ๐ŸŸข Low Priority: ๆ‹กๅผตๆ€ง
  describe("HTML ๅฑžๆ€ง็ถ™ๆ‰ฟ", () => {
    it("className ใŒใ‚ณใƒณใƒ†ใƒŠใซ้ฉ็”จใ•ใ‚Œใ‚‹", () => {
      const { container } = render(
        <Tabs tabs={defaultTabs} className="custom-tabs" />
      );
      const tabsContainer = container.firstChild as HTMLElement;
      expect(tabsContainer).toHaveClass("custom-tabs");
    });
  });
});

Resources