Skip to content

Commit

Permalink
fix: Switch hidden input receiving focus (#1243)
Browse files Browse the repository at this point in the history
* fix: `Switch` hidden input receiving focus

* add test case

* remove unused
  • Loading branch information
huntabyte authored Feb 24, 2025
1 parent 23b6816 commit 34eb6b3
Show file tree
Hide file tree
Showing 3 changed files with 88 additions and 75 deletions.
5 changes: 5 additions & 0 deletions .changeset/tricky-chicken-flash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"bits-ui": patch
---

fix: `Switch` hidden input receiving focus
1 change: 1 addition & 0 deletions packages/bits-ui/src/lib/bits/switch/switch.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ class SwitchInputState {
required: this.root.opts.required.current,
"aria-hidden": getAriaHidden(true),
style: styleToString(srOnlyStyles),
tabindex: -1,
}) as const
);
}
Expand Down
157 changes: 82 additions & 75 deletions tests/src/tests/switch/switch.test.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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");
});

0 comments on commit 34eb6b3

Please sign in to comment.