Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions src/components/ui/toggle-group.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/**
* @vitest-environment jsdom
*
* Arrow-key movement in a ToggleGroup comes from Base UI's composite, which
* reads `orientation` off the primitive. Our wrapper takes `orientation` as its
* own prop, so it is one destructure away from being spent on the data
* attribute and never reaching the composite — which is exactly what happened:
* a vertical group answered to Left/Right instead of Up/Down.
*/
import { describe, it, expect } from "vitest";
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import { ToggleGroup, ToggleGroupItem } from "./toggle-group";

function renderGroup(orientation: "horizontal" | "vertical") {
render(
<ToggleGroup value={["a"]} onValueChange={() => {}} orientation={orientation} aria-label="Test">
<ToggleGroupItem value="a">A</ToggleGroupItem>
<ToggleGroupItem value="b">B</ToggleGroupItem>
<ToggleGroupItem value="c">C</ToggleGroupItem>
</ToggleGroup>,
);
return ["A", "B", "C"].map((n) => screen.getByRole("button", { name: n }));
}

async function movesFocus(items: HTMLElement[], key: string) {
items[0].focus();
fireEvent.keyDown(items[0], { key });
try {
await waitFor(() => expect(document.activeElement).toBe(items[1]), { timeout: 300 });
return true;
} catch {
return false;
}
}

describe("ToggleGroup keyboard navigation", () => {
it("is a single tab stop, not one per item", () => {
const items = renderGroup("horizontal");
expect(items.filter((el) => (el as HTMLButtonElement).tabIndex === 0)).toHaveLength(1);
});

it("moves a vertical group with ArrowDown", async () => {
expect(await movesFocus(renderGroup("vertical"), "ArrowDown")).toBe(true);
});

it("does not move a vertical group with ArrowRight", async () => {
expect(await movesFocus(renderGroup("vertical"), "ArrowRight")).toBe(false);
});

it("moves a horizontal group with ArrowRight", async () => {
expect(await movesFocus(renderGroup("horizontal"), "ArrowRight")).toBe(true);
});
});
4 changes: 4 additions & 0 deletions src/components/ui/toggle-group.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ function ToggleGroup({
data-variant={variant}
data-size={size}
data-spacing={spacing}
// Base UI drives arrow-key movement from this, so it has to reach the
// primitive and not just the data attribute the styles read. Without it
// a vertical group still answered to Left/Right instead of Up/Down.
orientation={orientation}
data-orientation={orientation}
style={{ "--gap": spacing } as React.CSSProperties}
className={cn(
Expand Down
5 changes: 4 additions & 1 deletion vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ export default defineConfig({
test: {
globals: true,
environment: "node",
include: ["src/**/*.test.ts", "tests/**/*.test.ts"],
// `.tsx` files are component tests; each one declares
// `@vitest-environment jsdom` in its own docblock, so the default node
// environment still applies to everything else.
include: ["src/**/*.test.ts", "src/**/*.test.tsx", "tests/**/*.test.ts"],
exclude: ["e2e/**", "node_modules/**"],
globalSetup: ["./tests/global-setup.ts"],
testTimeout: 30_000,
Expand Down