diff --git a/.changeset/tricky-chicken-flash.md b/.changeset/tricky-chicken-flash.md new file mode 100644 index 000000000..fa71e2f1c --- /dev/null +++ b/.changeset/tricky-chicken-flash.md @@ -0,0 +1,5 @@ +--- +"bits-ui": patch +--- + +fix: `Switch` hidden input receiving focus diff --git a/packages/bits-ui/src/lib/bits/switch/switch.svelte.ts b/packages/bits-ui/src/lib/bits/switch/switch.svelte.ts index 410771d2c..79e7eaf1d 100644 --- a/packages/bits-ui/src/lib/bits/switch/switch.svelte.ts +++ b/packages/bits-ui/src/lib/bits/switch/switch.svelte.ts @@ -93,6 +93,7 @@ class SwitchInputState { required: this.root.opts.required.current, "aria-hidden": getAriaHidden(true), style: styleToString(srOnlyStyles), + tabindex: -1, }) as const ); } diff --git a/tests/src/tests/switch/switch.test.ts b/tests/src/tests/switch/switch.test.ts index 2963994d5..27cad9855 100644 --- a/tests/src/tests/switch/switch.test.ts +++ b/tests/src/tests/switch/switch.test.ts @@ -1,7 +1,7 @@ import { render } from "@testing-library/svelte/svelte5"; import { userEvent } from "@testing-library/user-event"; import { axe } from "jest-axe"; -import { describe, it } from "vitest"; +import { it } from "vitest"; import type { Switch } from "bits-ui"; import { getTestKbd } from "../utils.js"; import SwitchTest from "./switch-test.svelte"; @@ -23,89 +23,96 @@ function setup(props: Switch.RootProps = {}) { }; } -describe("switch", () => { - it("should have no accessibility violations", async () => { - const { container } = render(SwitchTest); - expect(await axe(container)).toHaveNoViolations(); - }); +it("should have no accessibility violations", async () => { + const { container } = render(SwitchTest); + expect(await axe(container)).toHaveNoViolations(); +}); + +it("should have bits data attrs", async () => { + const { root, thumb } = setup(); + expect(root).toHaveAttribute("data-switch-root"); + expect(thumb).toHaveAttribute("data-switch-thumb"); +}); - it("should have bits data attrs", async () => { - const { root, thumb } = setup(); - expect(root).toHaveAttribute("data-switch-root"); - expect(thumb).toHaveAttribute("data-switch-thumb"); - }); +it('should default the value to "on", when no value prop is passed', async () => { + const { input } = setup(); + expect(input).toHaveAttribute("value", "on"); +}); - it('should default the value to "on", when no value prop is passed', async () => { - const { input } = setup(); - expect(input).toHaveAttribute("value", "on"); - }); +it("should toggle when clicked", async () => { + const { user, root, input } = setup(); + expect(root).toHaveAttribute("data-state", "unchecked"); + expect(root).not.toHaveAttribute("data-checked"); + expect(input.checked).toBe(false); + await user.click(root); + expect(root).toHaveAttribute("data-state", "checked"); + expect(root).toHaveAttribute("aria-checked", "true"); + expect(input.checked).toBe(true); +}); - it("should toggle when clicked", async () => { - const { user, root, input } = setup(); - expect(root).toHaveAttribute("data-state", "unchecked"); - expect(root).not.toHaveAttribute("data-checked"); - expect(input.checked).toBe(false); - await user.click(root); - expect(root).toHaveAttribute("data-state", "checked"); - expect(root).toHaveAttribute("aria-checked", "true"); - expect(input.checked).toBe(true); - }); +it.each([kbd.ENTER, kbd.SPACE])("should toggle when the `%s` key is pressed", async (key) => { + const { user, root, input } = setup(); + expect(root).toHaveAttribute("data-state", "unchecked"); + expect(root).toHaveAttribute("aria-checked", "false"); + expect(input.checked).toBe(false); + root.focus(); + await user.keyboard(key); + expect(root).toHaveAttribute("data-state", "checked"); + expect(root).toHaveAttribute("aria-checked", "true"); + expect(input.checked).toBe(true); +}); - it.each([kbd.ENTER, kbd.SPACE])("should toggle when the `%s` key is pressed", async (key) => { - const { user, root, input } = setup(); - expect(root).toHaveAttribute("data-state", "unchecked"); - expect(root).toHaveAttribute("aria-checked", "false"); - expect(input.checked).toBe(false); - root.focus(); - await user.keyboard(key); - expect(root).toHaveAttribute("data-state", "checked"); - expect(root).toHaveAttribute("aria-checked", "true"); - expect(input.checked).toBe(true); - }); +it("should be disabled then the `disabled` prop is set to true", async () => { + const { root, input } = setup({ disabled: true }); + expect(root).toHaveAttribute("data-disabled"); + expect(root).toBeDisabled(); + expect(input.disabled).toBe(true); +}); - it("should be disabled then the `disabled` prop is set to true", async () => { - const { root, input } = setup({ disabled: true }); - expect(root).toHaveAttribute("data-disabled"); - expect(root).toBeDisabled(); - expect(input.disabled).toBe(true); - }); +it("should be required then the `required` prop is set to true", async () => { + const { root, input } = setup({ required: true }); + expect(root).toHaveAttribute("aria-required", "true"); + expect(input.required).toBe(true); +}); - it("should be required then the `required` prop is set to true", async () => { - const { root, input } = setup({ required: true }); - expect(root).toHaveAttribute("aria-required", "true"); - expect(input.required).toBe(true); - }); +it("should fire the `onChange` callback when changing", async () => { + let newValue = false; + function onCheckedChange(next: boolean) { + newValue = next; + } - it("should fire the `onChange` callback when changing", async () => { - let newValue = false; - function onCheckedChange(next: boolean) { - newValue = next; - } + const { user, root } = setup({ onCheckedChange }); + expect(newValue).toBe(false); + await user.click(root); + expect(newValue).toBe(true); +}); - const { user, root } = setup({ onCheckedChange }); - expect(newValue).toBe(false); - await user.click(root); - expect(newValue).toBe(true); - }); +it("should respect binding to the `checked` prop", async () => { + const { getByTestId, user, root, input } = setup(); + const binding = getByTestId("binding"); + expect(binding).toHaveTextContent("false"); + await user.click(binding); + expect(binding).toHaveTextContent("true"); + expect(root).toHaveAttribute("data-state", "checked"); + expect(input.checked).toBe(true); +}); - it("should respect binding to the `checked` prop", async () => { - const { getByTestId, user, root, input } = setup(); - const binding = getByTestId("binding"); - expect(binding).toHaveTextContent("false"); - await user.click(binding); - expect(binding).toHaveTextContent("true"); - expect(root).toHaveAttribute("data-state", "checked"); - expect(input.checked).toBe(true); - }); +it("should not include the input when the `name` prop isn't passed/undefined", async () => { + const { input } = setup({ name: undefined }); + expect(input).not.toBeInTheDocument(); +}); - it("should not include the input when the `name` prop isn't passed/undefined", async () => { - const { input } = setup({ name: undefined }); - expect(input).not.toBeInTheDocument(); - }); +it("should render the input when the `name` prop is passed", async () => { + // passed by default + const { input } = setup(); + expect(input).toBeInTheDocument(); +}); - it("should render the input when the `name` prop is passed", async () => { - // passed by default - const { input } = setup(); - expect(input).toBeInTheDocument(); - }); +it("should not focus the hidden input", async () => { + const { user, input, root } = setup(); + root.focus(); + expect(root).toHaveFocus(); + await user.keyboard(kbd.TAB); + expect(input).not.toHaveFocus(); + expect(input).toHaveAttribute("tabindex", "-1"); });