From a42bb35793baf4e8a9c87499c069f51ce5cb8d06 Mon Sep 17 00:00:00 2001 From: Pierre Bertet Date: Thu, 2 May 2024 10:35:08 +0100 Subject: [PATCH] UI kit: Checkbox (#145) --- .../src/Checkbox/1. Default.fixture.tsx | 51 ++++ .../src/Checkbox/2. Disabled.fixture.tsx | 52 ++++ frontend/uikit/src/Checkbox/Checkbox.tsx | 23 ++ frontend/uikit/src/Radio/Radio.tsx | 242 +++++++++++------- frontend/uikit/src/Radio/RadioGroup.tsx | 52 ++-- frontend/uikit/src/index.ts | 1 + 6 files changed, 310 insertions(+), 111 deletions(-) create mode 100644 frontend/uikit-gallery/src/Checkbox/1. Default.fixture.tsx create mode 100644 frontend/uikit-gallery/src/Checkbox/2. Disabled.fixture.tsx create mode 100644 frontend/uikit/src/Checkbox/Checkbox.tsx diff --git a/frontend/uikit-gallery/src/Checkbox/1. Default.fixture.tsx b/frontend/uikit-gallery/src/Checkbox/1. Default.fixture.tsx new file mode 100644 index 00000000..d12c1e92 --- /dev/null +++ b/frontend/uikit-gallery/src/Checkbox/1. Default.fixture.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { Checkbox } from "@liquity2/uikit"; +import { useState } from "react"; +import type { ReactNode } from "react"; + +const options = ["Option 1", "Option 2", "Option 3"]; + +export default function CheckboxFixture() { + return ( +
+ {options.map((label, index) => ( + + ))} +
+ ); +} + +function CheckboxField({ label }: { label: string }) { + const [checked, setChecked] = useState(false); + const toggle = () => setChecked((c) => !c); + return ( + + ); +} + +function Label({ children }: { children: ReactNode }) { + return ( + + ); +} diff --git a/frontend/uikit-gallery/src/Checkbox/2. Disabled.fixture.tsx b/frontend/uikit-gallery/src/Checkbox/2. Disabled.fixture.tsx new file mode 100644 index 00000000..0b13704d --- /dev/null +++ b/frontend/uikit-gallery/src/Checkbox/2. Disabled.fixture.tsx @@ -0,0 +1,52 @@ +"use client"; + +import { Checkbox } from "@liquity2/uikit"; +import { useState } from "react"; +import type { ReactNode } from "react"; + +const options = ["Option 1", "Option 2", "Option 3"]; + +export default function CheckboxFixture() { + return ( +
+ {options.map((label, index) => ( + + ))} +
+ ); +} + +function CheckboxField({ label }: { label: string }) { + const [checked, setChecked] = useState(false); + const toggle = () => setChecked((c) => !c); + return ( + + ); +} + +function Label({ children }: { children: ReactNode }) { + return ( + + ); +} diff --git a/frontend/uikit/src/Checkbox/Checkbox.tsx b/frontend/uikit/src/Checkbox/Checkbox.tsx new file mode 100644 index 00000000..ea07081e --- /dev/null +++ b/frontend/uikit/src/Checkbox/Checkbox.tsx @@ -0,0 +1,23 @@ +import type { ComponentPropsWithoutRef } from "react"; + +import { Radio } from "../Radio/Radio"; + +type RadioProps = ComponentPropsWithoutRef; + +export function Checkbox({ + appearance = "checkbox", + ...props +}: + & Omit + & { + checked: NonNullable; + onChange: NonNullable; + }) +{ + return ( + + ); +} diff --git a/frontend/uikit/src/Radio/Radio.tsx b/frontend/uikit/src/Radio/Radio.tsx index 9227bfe1..efb18571 100644 --- a/frontend/uikit/src/Radio/Radio.tsx +++ b/frontend/uikit/src/Radio/Radio.tsx @@ -7,19 +7,21 @@ import { useTheme } from "../Theme/Theme"; import { useRadioGroup } from "./RadioGroup"; export function Radio({ + appearance = "radio", checked: checkedProp, disabled, index, onChange, tabIndex, }: { + appearance?: "radio" | "checkbox"; checked?: boolean; disabled?: boolean; index?: number; onChange?: (checked: boolean) => void; tabIndex?: number; }) { - const input = useRef(null); + const input = useRef(null); const radioGroup = useRadioGroup(index); const inRadioGroup = radioGroup !== null; const checked = checkedProp ?? (inRadioGroup && index === radioGroup.selected); @@ -36,12 +38,6 @@ export function Radio({ }; } - const handleChange = () => { - if (onChange) { - onChange(!checked); - } - }; - const handleClick = () => { if (onChange && !disabled) { onChange(!checked); @@ -63,76 +59,71 @@ export function Radio({ friction: 100, }, initial: { - dotColor: color(disabled ? "disabledBorder" : "controlSurface"), + tickColor: color(disabled ? "disabledBorder" : "controlSurface"), ringColor: color("accent"), - scale: 0.4, // 8px + tickProgress: 0, }, from: { - dotColor: color(disabled ? "disabledBorder" : "controlSurface"), + tickColor: color(disabled ? "disabledBorder" : "controlSurface"), ringColor: color("accent"), - scale: 0.9, // 18px + tickProgress: 1, }, enter: { - scale: 0.4, // 8px + tickProgress: 0, }, leave: { - dotColor: color(disabled ? "disabledBorder" : "controlSurface"), + tickColor: color(disabled ? "disabledBorder" : "controlSurface"), ringColor: color("controlBorder"), - scale: 0.9, // 18px + tickProgress: 1, }, }); return ( -
-
{checkTransition((style, checked) => ( checked && ( - div": { - transform: "scale(0.9)", - }, - }, - _peerDisabled: { - background: "disabledSurface!", - border: "1px solid token(colors.disabledBorder)!", - "& > div": { - transform: "scale(1)!", - }, - }, - })} - style={{ - "--ringColor": style.ringColor, - } as CSSProperties} - > -
+ appearance === "radio" + ? ( div": { + transform: "scale(0.9)", + }, + }, + _groupDisabled: { + background: "disabledSurface!", + border: "1px solid token(colors.disabledBorder)!", + "& > div": { + transform: "scale(1)!", + }, + }, })} style={{ - background: style.dotColor, - scale: style.scale, + "--ringColor": style.ringColor, + } as CSSProperties} + > +
+ +
+
+ ) + : ( + div": { + transform: "scale(0.9)", + }, + }, + _groupDisabled: { + background: "disabledSurface!", + border: "1px solid token(colors.disabledBorder)!", + "& > div": { + transform: "scale(1)!", + }, + }, + })} + style={{ + ...({ + "--ringColor": style.ringColor, + } as CSSProperties), + ...({ + opacity: style.tickProgress.to([0, 1], [1, 0]), + }), }} - /> -
-
+ > +
+ + + +
+ + ) ) ))} -
+ + ); +} + +function Tick() { + return ( + + + ); } diff --git a/frontend/uikit/src/Radio/RadioGroup.tsx b/frontend/uikit/src/Radio/RadioGroup.tsx index 360e0631..b0041826 100644 --- a/frontend/uikit/src/Radio/RadioGroup.tsx +++ b/frontend/uikit/src/Radio/RadioGroup.tsx @@ -5,7 +5,7 @@ import { noop } from "../utils"; type RadioGroupContext = { addRadio: (radioIndex: number) => void; - focusableIndex: number; + focusableIndex?: number; select: (radioIndex: number) => void; removeRadio: (radioIndex: number) => void; selectNext: () => void; @@ -13,9 +13,15 @@ type RadioGroupContext = { selected: number; }; -const RadioGroupContext = createContext( - {} as RadioGroupContext, -); +const RadioGroupContext = createContext({ + addRadio: noop, + focusableIndex: undefined, + select: noop, + removeRadio: noop, + selectNext: noop, + selectPrev: noop, + selected: 0, +}); export function RadioGroup({ children, @@ -85,7 +91,7 @@ type RadioGroupValue = RadioGroupContext & { }; export function useRadioGroup(radioIndex?: number): RadioGroupValue | null { - const radioGroup = useContext(RadioGroupContext) as RadioGroupValue; + const radioGroup = useContext(RadioGroupContext); const { addRadio, removeRadio } = radioGroup ?? {}; useEffect(() => { @@ -100,24 +106,26 @@ export function useRadioGroup(radioIndex?: number): RadioGroupValue | null { return null; } - // Handles key events and trigger changes in the RadioGroup as needed. - radioGroup.onKeyDown = (event: KeyboardEvent) => { - if (event.altKey || event.metaKey || event.ctrlKey) { - return; - } - - if (KEYS_PREV.includes(event.key)) { - radioGroup.selectPrev(); - event.preventDefault(); - } - - if (KEYS_NEXT.includes(event.key)) { - radioGroup.selectNext(); - event.preventDefault(); - } + return { + ...radioGroup, + + // Handles key events and trigger changes in the RadioGroup as needed. + onKeyDown: (event: KeyboardEvent) => { + if (event.altKey || event.metaKey || event.ctrlKey) { + return; + } + + if (KEYS_PREV.includes(event.key)) { + radioGroup.selectPrev(); + event.preventDefault(); + } + + if (KEYS_NEXT.includes(event.key)) { + radioGroup.selectNext(); + event.preventDefault(); + } + }, }; - - return radioGroup; } function findSiblingIndex( diff --git a/frontend/uikit/src/index.ts b/frontend/uikit/src/index.ts index d3ee9a0e..05409704 100644 --- a/frontend/uikit/src/index.ts +++ b/frontend/uikit/src/index.ts @@ -1,6 +1,7 @@ export type { BrandColorName, ThemeColorName, ThemeDescriptor } from "./Theme/Theme"; export { Button } from "./Button/Button"; +export { Checkbox } from "./Checkbox/Checkbox"; export { FormField } from "./FormField/FormField"; export * from "./icons"; export { TextInput } from "./Input/TextInput";