From e3231cf7234c9f4a6454cd77108d01c74ba936b7 Mon Sep 17 00:00:00 2001 From: Arno V Date: Fri, 15 Mar 2024 19:28:08 -0400 Subject: [PATCH] fix(Toggle): better dark and light modes (#418) ## Summary by CodeRabbit - **New Features** - Introduced a `focusMode` property for the Toggle component, allowing users to customize the focus ring color according to their preference or system settings. - **Documentation** - Updated the Toggle component stories to showcase the new `focusMode` feature and other enhancements like `labelHidden` and `label` props. - **Tests** - Enhanced Toggle component tests to include checks for classes based on toggle state and mode. - **Refactor** - Improved Toggle component logic and utilities for handling focus styles and color classes more efficiently. --- packages/documentation/nodemon.json | 3 +- .../documentation/src/Form/Toggle.stories.tsx | 19 ++--- .../ui-form/src/components/Toggle/Toggle.tsx | 8 +- .../src/components/Toggle/ToggleTypes.d.ts | 11 ++- .../Toggle/__tests__/Toggle.test.tsx | 83 +++++++++++++++++-- .../src/components/Toggle/utilities.ts | 63 +++++++++----- 6 files changed, 147 insertions(+), 40 deletions(-) diff --git a/packages/documentation/nodemon.json b/packages/documentation/nodemon.json index 4af950bb..c72b9d7a 100644 --- a/packages/documentation/nodemon.json +++ b/packages/documentation/nodemon.json @@ -5,6 +5,7 @@ "src/**/*.*", "../ui-components/src/**/*.*", "../ui-components/lib/**/*.*", - "../ui-styles/src/**/*.*" + "../ui-styles/src/**/*.*", + "../ui-form/src/**/*.*" ] } diff --git a/packages/documentation/src/Form/Toggle.stories.tsx b/packages/documentation/src/Form/Toggle.stories.tsx index 9f761b9f..1f9bc38e 100644 --- a/packages/documentation/src/Form/Toggle.stories.tsx +++ b/packages/documentation/src/Form/Toggle.stories.tsx @@ -11,28 +11,27 @@ export default { }; export const Basic: Story = (args) => { - const [checked, setChecked] = useState(false); + const [checked, setChecked] = useState(true); return (
- +
); }; Basic.args = { - checked: false, - disabled: false, mode: "system", + focusMode: "system", + labelHidden: false, + label: "Toggle", }; Basic.argTypes = { mode: { options: ["dark", "light", "system", "alt-system"], control: { type: "radio" }, }, + focusMode: { + options: ["dark", "light", "system", "alt-system"], + control: { type: "radio" }, + }, }; diff --git a/packages/ui-form/src/components/Toggle/Toggle.tsx b/packages/ui-form/src/components/Toggle/Toggle.tsx index d8ee8a36..c245b5e6 100644 --- a/packages/ui-form/src/components/Toggle/Toggle.tsx +++ b/packages/ui-form/src/components/Toggle/Toggle.tsx @@ -8,9 +8,15 @@ export const Toggle = ({ labelHidden = false, name, mode = "system", + focusMode = "system", spacing, }: ToggleProps) => { - const toggleClasses = getToggleClasses({ mode, labelHidden, spacing }); + const toggleClasses = getToggleClasses({ + mode, + focusMode, + labelHidden, + spacing, + }); const handleChange = (e: React.ChangeEvent) => { onChange?.(e.target.checked); }; diff --git a/packages/ui-form/src/components/Toggle/ToggleTypes.d.ts b/packages/ui-form/src/components/Toggle/ToggleTypes.d.ts index 1634416b..cdb65640 100644 --- a/packages/ui-form/src/components/Toggle/ToggleTypes.d.ts +++ b/packages/ui-form/src/components/Toggle/ToggleTypes.d.ts @@ -1,7 +1,6 @@ import type { SpacingProps } from "@versini/ui-private/dist/utilities"; export type ToggleProps = { - checked: boolean; /** * The label to use for the component. */ @@ -15,6 +14,16 @@ export type ToggleProps = { * @param checked whether or not the component is checked */ onChange: (checked: boolean) => void; + /** + * Whether or not the component is checked. + * @default false + */ + checked?: boolean; + /** + * The type of focus for the Button. This will change the color + * of the focus ring around the Button. + */ + focusMode?: "dark" | "light" | "system" | "alt-system"; /** * Whether or not to render the label. * @default false diff --git a/packages/ui-form/src/components/Toggle/__tests__/Toggle.test.tsx b/packages/ui-form/src/components/Toggle/__tests__/Toggle.test.tsx index a1054b37..d2e9a27c 100644 --- a/packages/ui-form/src/components/Toggle/__tests__/Toggle.test.tsx +++ b/packages/ui-form/src/components/Toggle/__tests__/Toggle.test.tsx @@ -3,6 +3,7 @@ import { fireEvent, render, screen } from "@testing-library/react"; import { expectToHaveClasses } from "../../../../../../configuration/tests-helpers"; +import { TOGGLE_CLASSNAME } from "../../../common/constants"; import { Toggle } from "../.."; describe("Toggle (exceptions)", () => { @@ -22,11 +23,47 @@ describe("Toggle modifiers", () => { onChange={() => {}} />, ); - const node = screen.getByText("toto"); - expectToHaveClasses(node, ["ml-3", "text-sm", "text-copy-lighter"]); + const label = screen.getByText("toto"); + const input = screen.getByRole("checkbox"); + const toggle = input.nextElementSibling; + + expectToHaveClasses(label, ["ml-3", "text-sm", "text-copy-lighter"]); + if (toggle) { + expectToHaveClasses(toggle, [ + TOGGLE_CLASSNAME, + "after:absolute", + "after:bg-surface-light", + "after:border-surface-light", + "after:border", + "after:content-['']", + "after:h-5", + "after:left-[2px]", + "after:rounded-full", + "after:top-[2px]", + "after:transition-all", + "after:w-5", + "bg-surface-darker", + "border-border-light", + "dark:after:bg-surface-medium", + "dark:after:border-surface-medium", + "dark:peer-focus:outline-focus-light", + "h-6", + "peer-checked:after:bg-white", + "peer-checked:after:border-white", + "peer-checked:after:translate-x-full", + "peer-checked:bg-[#5bc236]", + "peer-focus:outline-2", + "peer-focus:outline-focus-dark", + "peer-focus:outline-offset-2", + "peer-focus:outline", + "peer", + "rounded-full", + "w-11", + ]); + } }); - it("should render a light Toggle ", async () => { + it("should render a light Toggle", async () => { render( { onChange={() => {}} />, ); - const node = screen.getByText("toto"); - expectToHaveClasses(node, ["ml-3", "text-sm", "text-copy-dark"]); + + const label = screen.getByText("toto"); + const input = screen.getByRole("checkbox"); + const toggle = input.nextElementSibling; + expectToHaveClasses(label, ["ml-3", "text-sm", "text-copy-dark"]); + if (toggle) { + expectToHaveClasses(toggle, [ + TOGGLE_CLASSNAME, + "peer", + "h-6", + "w-11", + "rounded-full", + "border-border-dark", + "bg-surface-medium", + "peer-focus:outline", + "peer-focus:outline-2", + "peer-focus:outline-offset-2", + "peer-focus:outline-focus-dark", + "dark:peer-focus:outline-focus-light", + "after:left-[2px]", + "after:top-[2px]", + "after:border", + "after:border-surface-light", + "dark:after:border-surface-medium", + "after:bg-surface-light", + "dark:after:bg-surface-medium", + "after:absolute", + "after:h-5", + "after:w-5", + "after:rounded-full", + "after:transition-all", + "after:content-['']", + "peer-checked:bg-[#5bc236]", + "peer-checked:after:translate-x-full", + "peer-checked:after:bg-white", + "peer-checked:after:border-white", + ]); + } }); it("should render a Toggle with no label", async () => { diff --git a/packages/ui-form/src/components/Toggle/utilities.ts b/packages/ui-form/src/components/Toggle/utilities.ts index f6cf38ec..07f5df8a 100644 --- a/packages/ui-form/src/components/Toggle/utilities.ts +++ b/packages/ui-form/src/components/Toggle/utilities.ts @@ -5,27 +5,38 @@ import clsx from "clsx"; import { TOGGLE_CLASSNAME } from "../../common/constants"; const getToggleBaseClasses = () => { - return clsx(TOGGLE_CLASSNAME, "peer h-6 w-11 rounded-full"); + return clsx(TOGGLE_CLASSNAME, "peer", "h-6", "w-11", "rounded-full"); }; -const getToggleKnobClasses = () => { +const getToggleKnobFocusClasses = ({ + focusMode, +}: { + focusMode?: "dark" | "light" | "system" | "alt-system"; +}) => { return clsx( - "after:absolute", - "after:h-5", - "after:w-5", - "after:rounded-full", - "after:transition-all", - "after:content-['']", - "peer-focus:outline-none", - "peer-focus:ring-2", - "peer-focus:ring-white", + "peer-focus:outline", + "peer-focus:outline-2", + "peer-focus:outline-offset-2", + { + "peer-focus:outline-focus-dark": focusMode === "dark", + "peer-focus:outline-focus-light": focusMode === "light", + + "peer-focus:outline-focus-light dark:peer-focus:outline-focus-dark": + focusMode === "alt-system", + + "peer-focus:outline-focus-dark dark:peer-focus:outline-focus-light": + focusMode === "system", + }, ); }; const getToggleKnobOnClasses = () => { return clsx( "peer-checked:bg-[#5bc236]", + "peer-checked:after:translate-x-full", + "peer-checked:after:bg-white", + "peer-checked:after:border-white", ); }; @@ -34,12 +45,22 @@ const getToggleKnobOffClasses = () => { "after:left-[2px]", "after:top-[2px]", "after:border", - "after:border-white", - "after:bg-white", + "after:border-surface-light dark:after:border-surface-medium", + "after:bg-surface-light dark:after:bg-surface-medium", + "after:absolute", + "after:h-5", + "after:w-5", + "after:rounded-full", + "after:transition-all", + "after:content-['']", ); }; -const getToggleColorClasses = ({ mode }: { mode: string }) => { +const getToggleColorClasses = ({ + mode, +}: { + mode: "dark" | "light" | "system" | "alt-system"; +}) => { return clsx({ "border-border-dark bg-surface-medium": mode === "light", "border-border-light bg-surface-darker": mode === "dark", @@ -55,7 +76,7 @@ const getLabelClasses = ({ labelHidden, }: { labelHidden: boolean; - mode: string; + mode: "dark" | "light" | "system" | "alt-system"; }) => { return labelHidden ? "sr-only" @@ -67,32 +88,30 @@ const getLabelClasses = ({ }); }; -const getInputClasses = () => { - return "peer sr-only"; -}; - const getWrapperClasses = ({ spacing }: SpacingProps) => { return clsx("relative flex cursor-pointer items-center", getSpacing(spacing)); }; export const getToggleClasses = ({ mode, + focusMode, labelHidden, spacing, }: { + focusMode: "dark" | "light" | "system" | "alt-system"; labelHidden: boolean; - mode: string; + mode: "dark" | "light" | "system" | "alt-system"; } & SpacingProps) => { return { toggle: clsx( getToggleBaseClasses(), getToggleColorClasses({ mode }), - getToggleKnobClasses(), + getToggleKnobFocusClasses({ focusMode }), getToggleKnobOffClasses(), getToggleKnobOnClasses(), ), label: getLabelClasses({ mode, labelHidden }), - input: getInputClasses(), + input: "peer sr-only", wrapper: getWrapperClasses({ spacing }), }; };