Skip to content

Commit

Permalink
Merge pull request #68 from kunai-consulting/headless-checkbox
Browse files Browse the repository at this point in the history
Headless checkbox
  • Loading branch information
thejackshelton-kunaico authored Dec 10, 2024
2 parents 5c74a60 + 0dc00e4 commit 6c5d194
Show file tree
Hide file tree
Showing 21 changed files with 644 additions and 16 deletions.
4 changes: 3 additions & 1 deletion apps/docs/src/routes/checkbox/examples/checkbox.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
}

.checkbox-trigger {
background: #0a4d70;
width: 30px;
height: 30px;
border-radius: 8px;
position: relative;
background: gray;
}

.checkbox-trigger:focus-visible {
Expand All @@ -26,4 +26,6 @@
position: absolute;
inset: 0;
align-items: center;
background: #0a4d70;
border-radius: 8px;
}
28 changes: 28 additions & 0 deletions apps/docs/src/routes/checkbox/examples/description.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { component$, useStyles$ } from "@builder.io/qwik";
import { Checkbox } from "@kunai-consulting/qwik-components";
import { LuCheck } from "@qwikest/icons/lucide";

export default component$(() => {
useStyles$(styles);

const description = "By checking this box, you acknowledge that you have read, understood, and agree to our Terms of Service and Privacy Policy. This includes consent to process your personal data as described in our policies.";

return (
<Checkbox.Root isDescription>
<div style={{ display: "flex", alignItems: "center", gap: "8px", marginBottom: "8px" }}>
<Checkbox.Trigger class="checkbox-trigger">
<Checkbox.Indicator class="checkbox-indicator">
<LuCheck />
</Checkbox.Indicator>
</Checkbox.Trigger>
<Checkbox.Label>I accept the Terms and Conditions</Checkbox.Label>
</div>
<Checkbox.Description>
By checking this box, you acknowledge that you have read, understood, and agree to our Terms of Service and Privacy Policy. This includes consent to process your personal data as described in our policies.
</Checkbox.Description>
</Checkbox.Root>
);
});

// example styles
import styles from "./checkbox.css?inline";
54 changes: 54 additions & 0 deletions apps/docs/src/routes/checkbox/examples/form-mixed.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { $, component$, type Signal, useSignal, useStyles$ } from "@builder.io/qwik";
import { Checkbox } from "@kunai-consulting/qwik-components";
import { LuCheck } from "@qwikest/icons/lucide";

export default component$(() => {
useStyles$(styles);

const formData = useSignal<Record<string, FormDataEntryValue>>();

const isChecked = useSignal<boolean | "mixed">("mixed");

return (
<form
preventdefault:submit
onSubmit$={(e) => {
const form = e.target as HTMLFormElement;
formData.value = Object.fromEntries(new FormData(form));
}}
style={{ display: "flex", flexDirection: "column", gap: "8px" }}
>
<TermsCheckbox isChecked={isChecked} />
<button
data-test-mixed
onClick$={() => {
isChecked.value = "mixed";
}}
type="button"
>
Make it mixed
</button>
</form>
);
});

export const TermsCheckbox = component$(
({ isChecked }: { isChecked: Signal<boolean | "mixed"> }) => {
return (
<Checkbox.Root bind:checked={isChecked}>
<Checkbox.HiddenNativeInput />
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
<Checkbox.Trigger class="checkbox-trigger">
<Checkbox.Indicator class="checkbox-indicator">
<LuCheck />
</Checkbox.Indicator>
</Checkbox.Trigger>
<Checkbox.Label>I accept the Terms and Conditions</Checkbox.Label>
</div>
</Checkbox.Root>
);
}
);

// example styles
import styles from "./checkbox.css?inline";
43 changes: 43 additions & 0 deletions apps/docs/src/routes/checkbox/examples/form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { $, component$, useSignal, useStyles$, useStylesScoped$ } from "@builder.io/qwik";
import { Checkbox } from "@kunai-consulting/qwik-components";
import { LuCheck } from "@qwikest/icons/lucide";

export default component$(() => {
useStyles$(styles);

const formData = useSignal<Record<string, FormDataEntryValue>>();

return (
<form
preventdefault:submit
onSubmit$={(e) => {
const form = e.target as HTMLFormElement;
formData.value = Object.fromEntries(new FormData(form));
}}
style={{ display: "flex", flexDirection: "column", gap: "8px" }}
>
<TermsCheckbox />
<button type="submit">Submit</button>
{formData.value && <div>Submitted: {JSON.stringify(formData.value, null, 2)}</div>}
</form>
);
});

export const TermsCheckbox = component$(() => {
return (
<Checkbox.Root name="terms">
<Checkbox.HiddenNativeInput />
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
<Checkbox.Trigger class="checkbox-trigger">
<Checkbox.Indicator class="checkbox-indicator">
<LuCheck />
</Checkbox.Indicator>
</Checkbox.Trigger>
<Checkbox.Label>I accept the Terms and Conditions</Checkbox.Label>
</div>
</Checkbox.Root>
);
});

// example styles
import styles from "./checkbox.css?inline";
43 changes: 43 additions & 0 deletions apps/docs/src/routes/checkbox/examples/required.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { $, component$, useSignal, useStyles$, useStylesScoped$ } from "@builder.io/qwik";
import { Checkbox } from "@kunai-consulting/qwik-components";
import { LuCheck } from "@qwikest/icons/lucide";

export default component$(() => {
useStyles$(styles);

const formData = useSignal<Record<string, FormDataEntryValue>>();

return (
<form
preventdefault:submit
onSubmit$={(e) => {
const form = e.target as HTMLFormElement;
formData.value = Object.fromEntries(new FormData(form));
}}
style={{ display: "flex", flexDirection: "column", gap: "8px" }}
>
<TermsCheckbox />
<button type="submit">Submit</button>
{formData.value && <div>Submitted: {JSON.stringify(formData.value, null, 2)}</div>}
</form>
);
});

export const TermsCheckbox = component$(() => {
return (
<Checkbox.Root name="terms" required>
<Checkbox.HiddenNativeInput />
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
<Checkbox.Trigger class="checkbox-trigger">
<Checkbox.Indicator class="checkbox-indicator">
<LuCheck />
</Checkbox.Indicator>
</Checkbox.Trigger>
<Checkbox.Label>I accept the Terms and Conditions</Checkbox.Label>
</div>
</Checkbox.Root>
);
});

// example styles
import styles from "./checkbox.css?inline";
70 changes: 70 additions & 0 deletions apps/docs/src/routes/checkbox/examples/validation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import {
component$,
type Signal,
useComputed$,
useSignal,
useStyles$
} from "@builder.io/qwik";
import { Checkbox } from "@kunai-consulting/qwik-components";
import { LuCheck } from "@qwikest/icons/lucide";

export default component$(() => {
useStyles$(styles);

const formData = useSignal<Record<string, FormDataEntryValue>>();
const isChecked = useSignal(false);
const isSubmitAttempt = useSignal(false);
const isError = useComputed$(() => !isChecked.value && isSubmitAttempt.value);

return (
<form
preventdefault:submit
noValidate
onSubmit$={(e) => {
const form = e.target as HTMLFormElement;
if (!isChecked.value) {
isSubmitAttempt.value = true;
return;
}

formData.value = Object.fromEntries(new FormData(form));
}}
style={{ display: "flex", flexDirection: "column", gap: "8px" }}
>
<TermsCheckbox isChecked={isChecked} isError={isError} />
<button type="submit">Submit</button>
{formData.value && <div>Submitted: {JSON.stringify(formData.value, null, 2)}</div>}
</form>
);
});

type TermsCheckboxProps = {
isChecked: Signal<boolean>;
isError: Signal<boolean>;
};

export const TermsCheckbox = component$(({ isChecked, isError }: TermsCheckboxProps) => {
return (
<Checkbox.Root name="terms" required bind:checked={isChecked}>
<Checkbox.HiddenNativeInput />
<div
style={{ display: "flex", alignItems: "center", gap: "8px", marginBottom: "8px" }}
>
<Checkbox.Trigger class="checkbox-trigger">
<Checkbox.Indicator class="checkbox-indicator">
<LuCheck />
</Checkbox.Indicator>
</Checkbox.Trigger>
<Checkbox.Label>I accept the Terms and Conditions</Checkbox.Label>
</div>
{isError.value && (
<Checkbox.ErrorMessage style={{ color: "red" }}>
Please accept the terms and conditions
</Checkbox.ErrorMessage>
)}
</Checkbox.Root>
);
});

// example styles
import styles from "./checkbox.css?inline";
43 changes: 43 additions & 0 deletions apps/docs/src/routes/checkbox/examples/value.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { $, component$, useSignal, useStyles$, useStylesScoped$ } from "@builder.io/qwik";
import { Checkbox } from "@kunai-consulting/qwik-components";
import { LuCheck } from "@qwikest/icons/lucide";

export default component$(() => {
useStyles$(styles);

const formData = useSignal<Record<string, FormDataEntryValue>>();

return (
<form
preventdefault:submit
onSubmit$={(e) => {
const form = e.target as HTMLFormElement;
formData.value = Object.fromEntries(new FormData(form));
}}
style={{ display: "flex", flexDirection: "column", gap: "8px" }}
>
<TermsCheckbox />
<button type="submit">Submit</button>
{formData.value && <div>Submitted: {JSON.stringify(formData.value, null, 2)}</div>}
</form>
);
});

export const TermsCheckbox = component$(() => {
return (
<Checkbox.Root name="terms" value="checked">
<Checkbox.HiddenNativeInput />
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
<Checkbox.Trigger class="checkbox-trigger">
<Checkbox.Indicator class="checkbox-indicator">
<LuCheck />
</Checkbox.Indicator>
</Checkbox.Trigger>
<Checkbox.Label>I accept the Terms and Conditions</Checkbox.Label>
</div>
</Checkbox.Root>
);
});

// example styles
import styles from "./checkbox.css?inline";
47 changes: 46 additions & 1 deletion apps/docs/src/routes/checkbox/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,16 @@ To associate a label with a checkbox, use the `Checkbox.Label` component.

<Showcase name="label" />

## Adding a description

To add a description to a checkbox, add the `isDescription` prop in the `Checkbox.Root` component.

Then use the `Checkbox.Description` component to decide where to render the description.

<Showcase name="description" />

> **Note:** Due to HTML streaming limitations, the `isDescription` prop must be passed to the root component to maintain consistent accessibility across different environments (especially server rendered content). The component will display a warning if no description is provided when this component is rendered.
## Initially check a checkbox

To set a checkbox to its initial checked state, use the `checked` prop.
Expand Down Expand Up @@ -49,14 +59,49 @@ To disable a checkbox, use the `disabled` prop.

Checkboxes can also be in a third mixed or indeterminate state. This is often considered a "partially checked" state.

To set a checkbox to an initial mixed state, use the `mixed` prop.
To set a mixed state, pass `"mixed"` to the `bind:checked` prop:

<Showcase name="mixed-initial" />

The mixed state can also be set reactively

<Showcase name="mixed-reactive" />

> When a checkbox is in a mixed state and the user clicks it:
> 1. The first click changes the state from "mixed" to "checked"
> 2. The next click changes the state from "checked" to "unchecked"
>
> This follows the standard accessibility pattern where mixed → checked → unchecked → checked → unchecked, and so on.
## Inside a form

To create a form with a checkbox, use the `Checkbox.HiddenNativeInput` component.

<Showcase name="form" />

Then create a name for the checkbox in the `Checkbox.Root` component.

> This will be used to associate the checkbox with the form.
## Making a checkbox required

To make a checkbox required, pass `required` to the `Checkbox.Root` component.

<Showcase name="required" />

## Giving a checkbox a value

By default, the value of a checkbox is `on` when checked. To give a checkbox a distinct value, pass a string to the `value` prop.

<Showcase name="value" />

## Checkbox validation

To validate a checkbox, use the `Checkbox.ErrorMessage` component. Whenever this component is rendered, the checkbox will display a validation error message.

> Screen readers will announce the error message when the component is rendered, along with an indication that the checkbox is invalid.
<Showcase name="validation" />



9 changes: 7 additions & 2 deletions libs/components/src/checkbox/checkbox-context.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import { createContextId, type Signal } from "@builder.io/qwik";
import { createContextId, JSXNode, type Signal } from "@builder.io/qwik";

export const checkboxContextId = createContextId<CheckboxContext>("qds-checkbox-context");

export type CheckboxContext = {
isCheckedSig: Signal<boolean | "mixed">;
isDisabledSig: Signal<boolean | undefined>;
isMixedSig: Signal<boolean | undefined>;
isErrorSig: Signal<boolean | undefined>;
localId: string;
isDescription: boolean | undefined;
name: string | undefined;
required: boolean | undefined;
value: string | undefined;
triggerRef: Signal<HTMLButtonElement | undefined>;
};
Loading

0 comments on commit 6c5d194

Please sign in to comment.