mirror of
https://github.com/penpot/penpot.git
synced 2026-08-05 04:18:41 +00:00
✨ Add Checkbox component to @penpot/ui
This commit is contained in:
parent
2991da47c3
commit
da90e03197
@ -49,3 +49,5 @@ export type {
|
|||||||
} from "./lib/controls/utilities/HintMessage";
|
} from "./lib/controls/utilities/HintMessage";
|
||||||
export { Switch } from "./lib/controls/Switch";
|
export { Switch } from "./lib/controls/Switch";
|
||||||
export type { SwitchProps } from "./lib/controls/Switch";
|
export type { SwitchProps } from "./lib/controls/Switch";
|
||||||
|
export { Checkbox } from "./lib/controls/Checkbox";
|
||||||
|
export type { CheckboxProps } from "./lib/controls/Checkbox";
|
||||||
|
|||||||
78
frontend/packages/ui/src/lib/controls/Checkbox.module.scss
Normal file
78
frontend/packages/ui/src/lib/controls/Checkbox.module.scss
Normal file
@ -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;
|
||||||
|
}
|
||||||
77
frontend/packages/ui/src/lib/controls/Checkbox.spec.tsx
Normal file
77
frontend/packages/ui/src/lib/controls/Checkbox.spec.tsx
Normal file
@ -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(<Checkbox id="cb" onChange={() => {}} />);
|
||||||
|
expect(baseElement).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render an <input type='checkbox'>", () => {
|
||||||
|
const { container } = render(<Checkbox id="cb" onChange={() => {}} />);
|
||||||
|
const input = container.querySelector("input[type='checkbox']");
|
||||||
|
expect(input).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should associate label with input via id", () => {
|
||||||
|
const { container } = render(
|
||||||
|
<Checkbox id="cb" label="My field" onChange={() => {}} />,
|
||||||
|
);
|
||||||
|
const label = container.querySelector("label");
|
||||||
|
expect(label?.getAttribute("for")).toBe("cb");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render label text", () => {
|
||||||
|
const { container } = render(
|
||||||
|
<Checkbox id="cb" label="Accept" onChange={() => {}} />,
|
||||||
|
);
|
||||||
|
const text = container.querySelector("[class*='checkbox-text']");
|
||||||
|
expect(text?.textContent).toBe("Accept");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render tick icon when checked", () => {
|
||||||
|
const { container } = render(
|
||||||
|
<Checkbox id="cb" checked onChange={() => {}} />,
|
||||||
|
);
|
||||||
|
const svg = container.querySelector("svg");
|
||||||
|
expect(svg).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not render tick icon when unchecked", () => {
|
||||||
|
const { container } = render(
|
||||||
|
<Checkbox id="cb" checked={false} onChange={() => {}} />,
|
||||||
|
);
|
||||||
|
const svg = container.querySelector("svg");
|
||||||
|
expect(svg).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should pass disabled to the input", () => {
|
||||||
|
const { container } = render(
|
||||||
|
<Checkbox id="cb" disabled onChange={() => {}} />,
|
||||||
|
);
|
||||||
|
const input = container.querySelector("input") as HTMLInputElement;
|
||||||
|
expect(input.disabled).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should merge className on the wrapper div", () => {
|
||||||
|
const { container } = render(
|
||||||
|
<Checkbox id="cb" className="extra" onChange={() => {}} />,
|
||||||
|
);
|
||||||
|
const wrapper = container.firstElementChild;
|
||||||
|
expect(wrapper?.getAttribute("class")).toContain("extra");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should pass through additional HTML attributes to the input", () => {
|
||||||
|
const { container } = render(
|
||||||
|
<Checkbox id="cb" data-testid="my-cb" onChange={() => {}} />,
|
||||||
|
);
|
||||||
|
const input = container.querySelector("input");
|
||||||
|
expect(input?.getAttribute("data-testid")).toBe("my-cb");
|
||||||
|
});
|
||||||
|
});
|
||||||
39
frontend/packages/ui/src/lib/controls/Checkbox.stories.tsx
Normal file
39
frontend/packages/ui/src/lib/controls/Checkbox.stories.tsx
Normal file
@ -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<typeof Checkbox>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
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 },
|
||||||
|
};
|
||||||
55
frontend/packages/ui/src/lib/controls/Checkbox.tsx
Normal file
55
frontend/packages/ui/src/lib/controls/Checkbox.tsx
Normal file
@ -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 (
|
||||||
|
<div className={clsx(styles.checkbox, className)}>
|
||||||
|
<label htmlFor={id} className={styles["checkbox-label"]}>
|
||||||
|
<div
|
||||||
|
className={clsx(styles["checkbox-box"], {
|
||||||
|
[styles.checked]: checked,
|
||||||
|
[styles.disabled]: disabled,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{checked && <Icon iconId="tick" size="s" />}
|
||||||
|
</div>
|
||||||
|
<div className={styles["checkbox-text"]}>{label}</div>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id={id}
|
||||||
|
checked={checked}
|
||||||
|
disabled={disabled}
|
||||||
|
onChange={onChange}
|
||||||
|
className={styles["checkbox-input"]}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Checkbox = memo(CheckboxInner);
|
||||||
Loading…
x
Reference in New Issue
Block a user