Update checkbox component

This commit is contained in:
adilallo
2026-02-04 13:31:04 -07:00
parent 0e7985287f
commit 05e403e3c6
5 changed files with 162 additions and 130 deletions
+53 -26
View File
@@ -2,13 +2,16 @@
import { useState } from "react"; import { useState } from "react";
import TextInput from "../components/TextInput"; import TextInput from "../components/TextInput";
import SelectInput from "../components/SelectInput"; import Checkbox from "../components/Checkbox";
import RadioGroup from "../components/RadioGroup";
export default function ComponentsPreview() { export default function ComponentsPreview() {
const [defaultInputValue, setDefaultInputValue] = useState(""); const [defaultInputValue, setDefaultInputValue] = useState("");
const [activeInputValue, setActiveInputValue] = useState(""); const [activeInputValue, setActiveInputValue] = useState("");
const [errorInputValue, setErrorInputValue] = useState(""); const [errorInputValue, setErrorInputValue] = useState("");
const [selectValue, setSelectValue] = useState(""); const [standardCheckbox, setStandardCheckbox] = useState(false);
const [inverseCheckbox, setInverseCheckbox] = useState(false);
const [radioValue, setRadioValue] = useState("");
return ( return (
<div className="min-h-screen bg-[var(--color-surface-default-primary)] p-[var(--spacing-scale-032)]"> <div className="min-h-screen bg-[var(--color-surface-default-primary)] p-[var(--spacing-scale-032)]">
@@ -67,10 +70,48 @@ export default function ComponentsPreview() {
</div> </div>
</section> </section>
{/* Select Input Section */} {/* Checkbox Section */}
<section className="space-y-[var(--spacing-scale-024)]"> <section className="space-y-[var(--spacing-scale-024)]">
<h2 className="font-bricolage-grotesque text-[32px] leading-[40px] font-bold text-[var(--color-content-default-primary)]"> <h2 className="font-bricolage-grotesque text-[32px] leading-[40px] font-bold text-[var(--color-content-default-primary)]">
Select Input Component Checkbox Component
</h2>
<div className="bg-[var(--color-surface-default-secondary)] rounded-[var(--radius-300,12px)] p-[var(--spacing-scale-032)] space-y-[var(--spacing-scale-024)]">
<div className="space-y-[var(--spacing-scale-016)]">
<div>
<h3 className="font-inter text-[20px] leading-[24px] font-semibold text-[var(--color-content-default-primary)] mb-[var(--spacing-scale-012)]">
Standard Mode
</h3>
<div className="space-y-[var(--spacing-scale-016)]">
<Checkbox
label="Standard Checkbox"
checked={standardCheckbox}
mode="standard"
onChange={({ checked }) => setStandardCheckbox(checked)}
/>
</div>
</div>
<div>
<h3 className="font-inter text-[20px] leading-[24px] font-semibold text-[var(--color-content-default-primary)] mb-[var(--spacing-scale-012)]">
Inverse Mode
</h3>
<div className="space-y-[var(--spacing-scale-016)]">
<Checkbox
label="Inverse Checkbox"
checked={inverseCheckbox}
mode="inverse"
onChange={({ checked }) => setInverseCheckbox(checked)}
/>
</div>
</div>
</div>
</div>
</section>
{/* Radio Group Section */}
<section className="space-y-[var(--spacing-scale-024)]">
<h2 className="font-bricolage-grotesque text-[32px] leading-[40px] font-bold text-[var(--color-content-default-primary)]">
Radio Group Component
</h2> </h2>
<div className="bg-[var(--color-surface-default-secondary)] rounded-[var(--radius-300,12px)] p-[var(--spacing-scale-032)] space-y-[var(--spacing-scale-024)]"> <div className="bg-[var(--color-surface-default-secondary)] rounded-[var(--radius-300,12px)] p-[var(--spacing-scale-032)] space-y-[var(--spacing-scale-024)]">
@@ -80,9 +121,8 @@ export default function ComponentsPreview() {
States States
</h3> </h3>
<div className="space-y-[var(--spacing-scale-016)]"> <div className="space-y-[var(--spacing-scale-016)]">
<SelectInput <RadioGroup
label="Default Select Input" name="default-radio"
placeholder="Choose an option"
value="" value=""
options={[ options={[
{ value: "option1", label: "Option 1" }, { value: "option1", label: "Option 1" },
@@ -90,20 +130,18 @@ export default function ComponentsPreview() {
{ value: "option3", label: "Option 3" }, { value: "option3", label: "Option 3" },
]} ]}
/> />
<SelectInput <RadioGroup
label="Interactive Select Input (click = active)" name="interactive-radio"
placeholder="Choose an option" value={radioValue}
value={selectValue} onChange={({ value }) => setRadioValue(value)}
onChange={(data) => setSelectValue(data.target.value)}
options={[ options={[
{ value: "option1", label: "Option 1" }, { value: "option1", label: "Option 1" },
{ value: "option2", label: "Option 2" }, { value: "option2", label: "Option 2" },
{ value: "option3", label: "Option 3" }, { value: "option3", label: "Option 3" },
]} ]}
/> />
<SelectInput <RadioGroup
label="Disabled Select Input" name="disabled-radio"
placeholder="Choose an option"
value="" value=""
disabled disabled
options={[ options={[
@@ -112,17 +150,6 @@ export default function ComponentsPreview() {
{ value: "option3", label: "Option 3" }, { value: "option3", label: "Option 3" },
]} ]}
/> />
<SelectInput
label="Error Select Input"
placeholder="Choose an option"
value=""
error
options={[
{ value: "option1", label: "Option 1" },
{ value: "option2", label: "Option 2" },
{ value: "option3", label: "Option 3" },
]}
/>
</div> </div>
</div> </div>
</div> </div>
+45 -42
View File
@@ -21,49 +21,59 @@ const CheckboxContainer = memo<CheckboxProps>(
...props ...props
}) => { }) => {
const isInverse = mode === "inverse"; const isInverse = mode === "inverse";
const isStandard = mode === "standard";
// Base tokens (rough placeholders leveraging existing CSS variables) // Generate unique ID for accessibility if not provided
const colorContent = isInverse const { id: checkboxId, labelId } = useComponentId("checkbox", id);
? "var(--color-content-inverse-primary)"
: "var(--color-content-default-primary)";
// Visual container depending on state // Base box styles per Figma
const baseBox = `flex items-center justify-center shrink-0 w-[var(--measures-sizing-024)] h-[var(--measures-sizing-024)] rounded-[var(--measures-radius-medium)] transition-all duration-200 ease-in-out`; const baseBox = `
flex
items-center
justify-center
shrink-0
w-[24px]
h-[24px]
rounded-[4px]
transition-all
duration-200
ease-in-out
`.trim().replace(/\s+/g, " ");
const stateStyles: Record<string, string> = { // Get box styles based on state and checked status per Figma designs
default: "", const getBoxStyles = (): string => {
hover: "", // Standard mode styles
focus: "", if (isStandard) {
// Default state: tertiary border, with hover and focus states via CSS
// Hover changes border to brand primary color
// Focus removes border and shows shadow (double ring: 2px white inner, 4px dark outer)
return `${baseBox} bg-[var(--color-surface-default-primary)] border border-solid border-[var(--color-border-default-tertiary,#464646)] hover:border-[var(--color-border-default-brand-primary,#fdfaa8)] focus:border-transparent focus:shadow-[0px_0px_0px_2px_var(--color-border-invert-primary,white),0px_0px_0px_4px_var(--color-border-default-primary,#141414)] focus:outline-none`;
}
// Inverse mode styles per Figma
if (isInverse) {
// Inverse: transparent background, white border
// Hover changes border to brand primary color
// Focus shows shadow (2px dark inner, 4px white outer) - note: reversed from standard
return `${baseBox} bg-transparent border border-solid border-[var(--color-border-invert-primary,white)] hover:border-[var(--color-border-default-brand-primary,#fdfaa8)] focus:shadow-[0px_0px_0px_2px_var(--color-border-default-primary,#141414),0px_0px_0px_4px_var(--color-border-invert-primary,white)] focus:outline-none`;
}
return baseBox;
}; };
// Background behavior: const combinedBoxStyles = getBoxStyles();
// - Standard: background does not change on check; only checkmark appears
// - Inverse: transparent background, checkmark appears on check // Checkmark color per Figma
const backgroundWhenChecked = isInverse
? "var(--color-surface-default-transparent)"
: "var(--color-surface-default-primary)";
const checkGlyphColor = checked const checkGlyphColor = checked
? isInverse ? isStandard
? "var(--color-content-inverse-primary)" ? "var(--color-content-default-brand-primary, #fefcc9)" // Light yellow/cream for standard mode
: "var(--color-border-default-brand-primary)" : "var(--color-content-inverse-primary, #000000)" // Black for inverse mode
: "transparent"; : "transparent";
const labelColor = colorContent;
const combinedBoxStyles = `${baseBox} ${stateStyles[state]}`; // Label color
const labelColor = isInverse
// Force visible outline for standard / default / unchecked ? "var(--color-content-inverse-primary)"
// Outline classes instead of inline styles so hover can override : "var(--color-content-default-primary)";
const defaultOutlineClass = isInverse
? "outline outline-1 outline-[var(--color-border-inverse-primary)]"
: "outline outline-1 outline-[var(--color-border-default-tertiary)]";
// Apply brand outline only on actual :hover, and only when standard/unchecked
const conditionalHoverOutlineClass =
"hover:outline hover:outline-1 hover:outline-[var(--color-border-default-brand-primary)]";
// Focus state for standard/unchecked with brand primary color and specific blur/spread
const conditionalFocusClass =
"focus:outline focus:outline-1 focus:outline-[var(--color-border-default-utility-info)] focus:shadow-[0_0_10px_1px_var(--color-surface-inverse-brand-primary)]";
const handleToggle = (e: React.MouseEvent | React.KeyboardEvent) => { const handleToggle = (e: React.MouseEvent | React.KeyboardEvent) => {
if (disabled) return; if (disabled) return;
@@ -74,9 +84,6 @@ const CheckboxContainer = memo<CheckboxProps>(
}); });
}; };
// Generate unique ID for accessibility if not provided
const { id: checkboxId, labelId } = useComponentId("checkbox", id);
const accessibilityProps = { const accessibilityProps = {
role: "checkbox" as const, role: "checkbox" as const,
"aria-checked": checked, "aria-checked": checked,
@@ -107,10 +114,6 @@ const CheckboxContainer = memo<CheckboxProps>(
value={value} value={value}
className={className} className={className}
combinedBoxStyles={combinedBoxStyles} combinedBoxStyles={combinedBoxStyles}
defaultOutlineClass={defaultOutlineClass}
conditionalHoverOutlineClass={conditionalHoverOutlineClass}
conditionalFocusClass={conditionalFocusClass}
backgroundWhenChecked={backgroundWhenChecked}
checkGlyphColor={checkGlyphColor} checkGlyphColor={checkGlyphColor}
labelColor={labelColor} labelColor={labelColor}
accessibilityProps={accessibilityProps} accessibilityProps={accessibilityProps}
@@ -27,10 +27,6 @@ export interface CheckboxViewProps {
value?: string; value?: string;
className: string; className: string;
combinedBoxStyles: string; combinedBoxStyles: string;
defaultOutlineClass: string;
conditionalHoverOutlineClass: string;
conditionalFocusClass: string;
backgroundWhenChecked: string;
checkGlyphColor: string; checkGlyphColor: string;
labelColor: string; labelColor: string;
accessibilityProps: React.HTMLAttributes<HTMLSpanElement>; accessibilityProps: React.HTMLAttributes<HTMLSpanElement>;
+4 -10
View File
@@ -9,10 +9,6 @@ export function CheckboxView({
value, value,
className, className,
combinedBoxStyles, combinedBoxStyles,
defaultOutlineClass,
conditionalHoverOutlineClass,
conditionalFocusClass,
backgroundWhenChecked,
checkGlyphColor, checkGlyphColor,
labelColor, labelColor,
accessibilityProps, accessibilityProps,
@@ -30,18 +26,16 @@ export function CheckboxView({
{...accessibilityProps} {...accessibilityProps}
onClick={onToggle} onClick={onToggle}
onKeyDown={onKeyDown} onKeyDown={onKeyDown}
className={`${combinedBoxStyles} ${defaultOutlineClass} ${conditionalHoverOutlineClass} ${conditionalFocusClass} p-[var(--measures-spacing-004)]`} className={`${combinedBoxStyles} p-[4px] ${disabled ? "" : "cursor-pointer"}`}
style={{
backgroundColor: backgroundWhenChecked,
}}
> >
{/* Simple check glyph */} {/* Checkmark SVG per Figma - 16px size */}
<svg <svg
width="16" width="16"
height="16" height="16"
viewBox="0 0 12 12" viewBox="0 0 12 12"
aria-hidden="true" aria-hidden="true"
focusable="false" focusable="false"
className="block"
> >
<polyline <polyline
points="2.5 6 5 8.5 10 3.5" points="2.5 6 5 8.5 10 3.5"
@@ -63,7 +57,7 @@ export function CheckboxView({
{label} {label}
</span> </span>
)} )}
{/* Hidden native input for form compatibility (optional for now) */} {/* Hidden native input for form compatibility */}
<input <input
type="checkbox" type="checkbox"
name={name} name={name}
+60 -48
View File
@@ -26,34 +26,6 @@ const CheckedInteraction = {
}, },
}; };
const StandardInteraction = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const checkboxes = canvas.getAllByRole("checkbox");
expect(checkboxes).toHaveLength(2);
expect(checkboxes[0]).toHaveAttribute("aria-checked", "false");
await userEvent.click(checkboxes[0]);
expect(checkboxes[0]).toHaveAttribute("aria-checked", "true");
expect(checkboxes[1]).toHaveAttribute("aria-checked", "true");
await userEvent.click(checkboxes[1]);
expect(checkboxes[1]).toHaveAttribute("aria-checked", "false");
},
};
const InverseInteraction = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const checkboxes = canvas.getAllByRole("checkbox");
expect(checkboxes).toHaveLength(2);
expect(checkboxes[0]).toHaveAttribute("aria-checked", "false");
await userEvent.click(checkboxes[0]);
expect(checkboxes[0]).toHaveAttribute("aria-checked", "true");
expect(checkboxes[1]).toHaveAttribute("aria-checked", "true");
await userEvent.click(checkboxes[1]);
expect(checkboxes[1]).toHaveAttribute("aria-checked", "false");
},
};
export default { export default {
title: "Forms/Checkbox", title: "Forms/Checkbox",
component: Checkbox, component: Checkbox,
@@ -137,8 +109,7 @@ export const Checked = {
export const Standard = { export const Standard = {
render: () => { render: () => {
const [unchecked, setUnchecked] = React.useState(false); const [checked, setChecked] = React.useState(false);
const [checked, setChecked] = React.useState(true);
return ( return (
<div className="space-y-4"> <div className="space-y-4">
@@ -146,13 +117,7 @@ export const Standard = {
<h3 className="text-white font-medium">Standard Mode</h3> <h3 className="text-white font-medium">Standard Mode</h3>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Checkbox <Checkbox
label="Unchecked" label="Standard Checkbox"
checked={unchecked}
mode="standard"
onChange={({ checked: newChecked }) => setUnchecked(newChecked)}
/>
<Checkbox
label="Checked"
checked={checked} checked={checked}
mode="standard" mode="standard"
onChange={({ checked: newChecked }) => setChecked(newChecked)} onChange={({ checked: newChecked }) => setChecked(newChecked)}
@@ -162,13 +127,11 @@ export const Standard = {
</div> </div>
); );
}, },
play: StandardInteraction.play,
}; };
export const Inverse = { export const Inverse = {
render: () => { render: () => {
const [unchecked, setUnchecked] = React.useState(false); const [checked, setChecked] = React.useState(false);
const [checked, setChecked] = React.useState(true);
return ( return (
<div className="space-y-4"> <div className="space-y-4">
@@ -176,13 +139,7 @@ export const Inverse = {
<h3 className="text-white font-medium">Inverse Mode</h3> <h3 className="text-white font-medium">Inverse Mode</h3>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Checkbox <Checkbox
label="Unchecked" label="Inverse Checkbox"
checked={unchecked}
mode="inverse"
onChange={({ checked: newChecked }) => setUnchecked(newChecked)}
/>
<Checkbox
label="Checked"
checked={checked} checked={checked}
mode="inverse" mode="inverse"
onChange={({ checked: newChecked }) => setChecked(newChecked)} onChange={({ checked: newChecked }) => setChecked(newChecked)}
@@ -192,5 +149,60 @@ export const Inverse = {
</div> </div>
); );
}, },
play: InverseInteraction.play, };
export const Disabled = {
args: {
checked: false,
mode: "standard",
state: "default",
disabled: true,
label: "Disabled checkbox",
},
render: (args) => <Checkbox {...args} />,
};
export const DisabledChecked = {
args: {
checked: true,
mode: "standard",
state: "default",
disabled: true,
label: "Disabled checked checkbox",
},
render: (args) => <Checkbox {...args} />,
};
// All modes comparison
export const AllModes = () => {
const [standardChecked, setStandardChecked] = React.useState(false);
const [inverseChecked, setInverseChecked] = React.useState(false);
return (
<div className="space-y-6">
<div>
<h3 className="text-lg font-semibold mb-4 text-white">Standard Mode</h3>
<div className="space-y-4">
<Checkbox
label="Standard Checkbox"
checked={standardChecked}
mode="standard"
onChange={({ checked }) => setStandardChecked(checked)}
/>
</div>
</div>
<div>
<h3 className="text-lg font-semibold mb-4 text-white">Inverse Mode</h3>
<div className="space-y-4">
<Checkbox
label="Inverse Checkbox"
checked={inverseChecked}
mode="inverse"
onChange={({ checked }) => setInverseChecked(checked)}
/>
</div>
</div>
</div>
);
}; };