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";