mirror of
https://github.com/logancyang/obsidian-copilot.git
synced 2026-07-22 07:50:24 +00:00
fix(ui): rebuild SettingSwitch on Obsidian native toggle vars (#2651)
The switch mixed CSS units: the track width used Tailwind's default `w-10` (2.5rem, font-relative) while height, thumb, and travel were fixed px via Obsidian's --size-4-* tokens. Obsidian's interface font-size setting drives the rem root, so changing it stretched the width alone and the thumb stopped landing at the edge (the reported break). Source all geometry from Obsidian's native small-toggle variables (--toggle-s-width/-thumb-width/-thumb-height/-border-width, --toggle-radius, --toggle-thumb-radius) so the switch is 100% px and pixel-matches Obsidian's own toggles; the thumb travel is derived from those sizes rather than a hardcoded distance. Public API, ARIA, and keyboard behavior unchanged. Adds a React Testing Library behavior test. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
55c9a2aa0c
commit
703529ac67
2 changed files with 72 additions and 3 deletions
67
src/components/ui/setting-switch.test.tsx
Normal file
67
src/components/ui/setting-switch.test.tsx
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import React from "react";
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import { SettingSwitch } from "./setting-switch";
|
||||
|
||||
describe("SettingSwitch", () => {
|
||||
it("renders an element with role=switch", () => {
|
||||
render(<SettingSwitch />);
|
||||
expect(screen.getByRole("switch")).not.toBeNull();
|
||||
});
|
||||
|
||||
it("reflects the checked state via aria-checked and data-state", () => {
|
||||
const { rerender } = render(<SettingSwitch checked />);
|
||||
const on = screen.getByRole("switch");
|
||||
expect(on.getAttribute("aria-checked")).toBe("true");
|
||||
expect(on.getAttribute("data-state")).toBe("checked");
|
||||
|
||||
rerender(<SettingSwitch checked={false} />);
|
||||
const off = screen.getByRole("switch");
|
||||
expect(off.getAttribute("aria-checked")).toBe("false");
|
||||
expect(off.getAttribute("data-state")).toBe("unchecked");
|
||||
});
|
||||
|
||||
it("calls onCheckedChange with the negation of checked when clicked", () => {
|
||||
const onCheckedChange = jest.fn();
|
||||
|
||||
const { rerender } = render(
|
||||
<SettingSwitch checked={false} onCheckedChange={onCheckedChange} />
|
||||
);
|
||||
fireEvent.click(screen.getByRole("switch"));
|
||||
expect(onCheckedChange).toHaveBeenLastCalledWith(true);
|
||||
|
||||
rerender(<SettingSwitch checked onCheckedChange={onCheckedChange} />);
|
||||
fireEvent.click(screen.getByRole("switch"));
|
||||
expect(onCheckedChange).toHaveBeenLastCalledWith(false);
|
||||
});
|
||||
|
||||
it("toggles on Enter and on Space", () => {
|
||||
const onCheckedChange = jest.fn();
|
||||
render(<SettingSwitch checked={false} onCheckedChange={onCheckedChange} />);
|
||||
const sw = screen.getByRole("switch");
|
||||
|
||||
fireEvent.keyDown(sw, { key: "Enter" });
|
||||
expect(onCheckedChange).toHaveBeenCalledTimes(1);
|
||||
|
||||
fireEvent.keyDown(sw, { key: " " });
|
||||
expect(onCheckedChange).toHaveBeenCalledTimes(2);
|
||||
expect(onCheckedChange).toHaveBeenNthCalledWith(1, true);
|
||||
expect(onCheckedChange).toHaveBeenNthCalledWith(2, true);
|
||||
|
||||
fireEvent.keyDown(sw, { key: "a" });
|
||||
expect(onCheckedChange).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("does not toggle and is not focusable when disabled", () => {
|
||||
const onCheckedChange = jest.fn();
|
||||
render(<SettingSwitch checked={false} disabled onCheckedChange={onCheckedChange} />);
|
||||
const sw = screen.getByRole("switch");
|
||||
|
||||
fireEvent.click(sw);
|
||||
fireEvent.keyDown(sw, { key: "Enter" });
|
||||
fireEvent.keyDown(sw, { key: " " });
|
||||
expect(onCheckedChange).not.toHaveBeenCalled();
|
||||
|
||||
expect(sw.getAttribute("aria-disabled")).toBe("true");
|
||||
expect(sw.tabIndex).toBe(-1);
|
||||
});
|
||||
});
|
||||
|
|
@ -33,7 +33,7 @@ const SettingSwitch = React.forwardRef<HTMLDivElement, SettingSwitchProps>(
|
|||
ref={ref}
|
||||
tabIndex={disabled ? -1 : 0}
|
||||
className={cn(
|
||||
"tw-relative tw-inline-flex tw-h-5.5 tw-w-10 tw-shrink-0 tw-cursor-pointer tw-items-center tw-rounded-full tw-transition-colors",
|
||||
"tw-relative tw-inline-flex tw-h-[calc(var(--toggle-s-thumb-height)_+_var(--toggle-s-border-width)*2)] tw-w-[var(--toggle-s-width)] tw-shrink-0 tw-cursor-pointer tw-items-center tw-rounded-[var(--toggle-radius)] tw-transition-colors",
|
||||
"focus-visible:tw-outline-none focus-visible:tw-ring-2 focus-visible:tw-ring-ring focus-visible:tw-ring-offset-2",
|
||||
checked ? "tw-bg-interactive-accent" : "tw-bg-[--background-modifier-border-hover]",
|
||||
disabled && "tw-cursor-not-allowed tw-opacity-50",
|
||||
|
|
@ -45,8 +45,10 @@ const SettingSwitch = React.forwardRef<HTMLDivElement, SettingSwitchProps>(
|
|||
>
|
||||
<div
|
||||
className={cn(
|
||||
"tw-pointer-events-none tw-block tw-size-4 tw-rounded-full tw-bg-toggle-thumb tw-shadow-lg tw-ring-0 tw-transition-transform",
|
||||
checked ? "tw-translate-x-5.5" : "tw-translate-x-0.5"
|
||||
"tw-pointer-events-none tw-block tw-h-[var(--toggle-s-thumb-height)] tw-w-[var(--toggle-s-thumb-width)] tw-rounded-[var(--toggle-thumb-radius)] tw-bg-toggle-thumb tw-shadow-sm tw-ring-0 tw-transition-transform",
|
||||
checked
|
||||
? "tw-translate-x-[calc(var(--toggle-s-width)_-_var(--toggle-s-thumb-width)_-_var(--toggle-s-border-width))]"
|
||||
: "tw-translate-x-[var(--toggle-s-border-width)]"
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in a new issue