diff --git a/frontend/packages/ui/src/index.ts b/frontend/packages/ui/src/index.ts index 6bbc858162..bdbd1982cc 100644 --- a/frontend/packages/ui/src/index.ts +++ b/frontend/packages/ui/src/index.ts @@ -49,3 +49,5 @@ export type { } from "./lib/controls/utilities/HintMessage"; export { Switch } from "./lib/controls/Switch"; export type { SwitchProps } from "./lib/controls/Switch"; +export { Checkbox } from "./lib/controls/Checkbox"; +export type { CheckboxProps } from "./lib/controls/Checkbox"; diff --git a/frontend/packages/ui/src/lib/controls/Checkbox.module.scss b/frontend/packages/ui/src/lib/controls/Checkbox.module.scss new file mode 100644 index 0000000000..3c3eeb5300 --- /dev/null +++ b/frontend/packages/ui/src/lib/controls/Checkbox.module.scss @@ -0,0 +1,78 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@use "../_ds/_borders.scss" as *; +@use "../_ds/_sizes.scss" as *; +@use "../_ds/typography.scss" as t; + +.checkbox { + --input-checkbox-border-color: var(--color-foreground-secondary); + --input-checkbox-border-color-focus: var(--color-accent-primary); + --input-checkbox-border-color-hover: var(--color-accent-primary-muted); + --input-checkbox-foreground-color: var(--color-foreground-primary); + --input-checkbox-background-color: var(--color-background-quaternary); + --input-checkbox-border-color-checked: var(--color-background-quaternary); + --input-checkbox-foreground-color-checked: var(--color-background-primary); + --input-checkbox-background-color-checked: var(--color-accent-primary); + --input-checkbox-foreground-color-disabled: var(--color-background-primary); + --input-checkbox-background-color-disabled: var(--color-foreground-secondary); + --input-checkbox-text-color: var(--color-foreground-secondary); +} + +.checkbox-label { + display: grid; + grid-template-columns: var(--sp-l) 1fr 0; + align-items: center; +} + +.checkbox-label:hover .checkbox-box { + border-color: var(--input-checkbox-border-color-hover); +} + +.checkbox-label:focus .checkbox-box, +.checkbox-label:focus-within .checkbox-box { + border-color: var(--input-checkbox-border-color-focus); +} + +.checkbox-box { + display: flex; + align-items: center; + justify-content: center; + inline-size: $sz-16; + block-size: $sz-16; + border-radius: $br-4; + border: $b-1 solid var(--input-checkbox-border-color); + color: var(--input-checkbox-foreground-color); + background-color: var(--input-checkbox-background-color); +} + +.checkbox-box.disabled { + border: 0; +} + +.checkbox-box.checked { + --input-checkbox-border-color: var(--input-checkbox-border-color-checked); + --input-checkbox-foreground-color: var(--input-checkbox-foreground-color-checked); + --input-checkbox-background-color: var(--input-checkbox-background-color-checked); +} + +.checkbox-box.checked.disabled { + --input-checkbox-foreground-color: var(--input-checkbox-foreground-color-disabled); + --input-checkbox-background-color: var(--input-checkbox-background-color-disabled); +} + +.checkbox-text { + @include t.use-typography("body-small"); + + padding-inline-start: var(--sp-s); + color: var(--input-checkbox-text-color); +} + +.checkbox-input:focus { + outline: 0; + inline-size: 0; + block-size: 0; +} diff --git a/frontend/packages/ui/src/lib/controls/Checkbox.spec.tsx b/frontend/packages/ui/src/lib/controls/Checkbox.spec.tsx new file mode 100644 index 0000000000..353bfd5be3 --- /dev/null +++ b/frontend/packages/ui/src/lib/controls/Checkbox.spec.tsx @@ -0,0 +1,77 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +import { render } from "@testing-library/react"; +import { Checkbox } from "./Checkbox"; + +describe("Checkbox", () => { + it("should render successfully", () => { + const { baseElement } = render( {}} />); + expect(baseElement).toBeTruthy(); + }); + + it("should render an ", () => { + const { container } = render( {}} />); + const input = container.querySelector("input[type='checkbox']"); + expect(input).toBeTruthy(); + }); + + it("should associate label with input via id", () => { + const { container } = render( + {}} />, + ); + const label = container.querySelector("label"); + expect(label?.getAttribute("for")).toBe("cb"); + }); + + it("should render label text", () => { + const { container } = render( + {}} />, + ); + const text = container.querySelector("[class*='checkbox-text']"); + expect(text?.textContent).toBe("Accept"); + }); + + it("should render tick icon when checked", () => { + const { container } = render( + {}} />, + ); + const svg = container.querySelector("svg"); + expect(svg).toBeTruthy(); + }); + + it("should not render tick icon when unchecked", () => { + const { container } = render( + {}} />, + ); + const svg = container.querySelector("svg"); + expect(svg).toBeNull(); + }); + + it("should pass disabled to the input", () => { + const { container } = render( + {}} />, + ); + const input = container.querySelector("input") as HTMLInputElement; + expect(input.disabled).toBe(true); + }); + + it("should merge className on the wrapper div", () => { + const { container } = render( + {}} />, + ); + const wrapper = container.firstElementChild; + expect(wrapper?.getAttribute("class")).toContain("extra"); + }); + + it("should pass through additional HTML attributes to the input", () => { + const { container } = render( + {}} />, + ); + const input = container.querySelector("input"); + expect(input?.getAttribute("data-testid")).toBe("my-cb"); + }); +}); diff --git a/frontend/packages/ui/src/lib/controls/Checkbox.stories.tsx b/frontend/packages/ui/src/lib/controls/Checkbox.stories.tsx new file mode 100644 index 0000000000..2af16dc2f5 --- /dev/null +++ b/frontend/packages/ui/src/lib/controls/Checkbox.stories.tsx @@ -0,0 +1,39 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { Checkbox } from "./Checkbox"; + +const meta = { + title: "Controls/Checkbox", + component: Checkbox, + args: { + id: "my-checkbox", + label: "Accept terms and conditions", + checked: false, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; + +export const Checked: Story = { + args: { checked: true }, +}; + +export const Disabled: Story = { + args: { disabled: true }, +}; + +export const DisabledChecked: Story = { + args: { checked: true, disabled: true }, +}; + +export const NoLabel: Story = { + args: { label: undefined }, +}; diff --git a/frontend/packages/ui/src/lib/controls/Checkbox.tsx b/frontend/packages/ui/src/lib/controls/Checkbox.tsx new file mode 100644 index 0000000000..62b1f9312b --- /dev/null +++ b/frontend/packages/ui/src/lib/controls/Checkbox.tsx @@ -0,0 +1,55 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +import { type ComponentPropsWithRef, memo } from "react"; +import clsx from "clsx"; +import { Icon } from "../foundations/assets/Icon"; +import styles from "./Checkbox.module.scss"; + +export interface CheckboxProps extends Omit< + ComponentPropsWithRef<"input">, + "type" +> { + /** Text label rendered next to the checkbox */ + label?: string; +} + +function CheckboxInner({ + id, + label, + checked, + disabled, + className, + onChange, + ...rest +}: CheckboxProps) { + return ( +
+ +
+ ); +} + +export const Checkbox = memo(CheckboxInner);