| 1 | import Check from "lucide-solid/icons/check"; |
| 2 | import Minus from "lucide-solid/icons/minus"; |
| 3 | import { createEffect, type JSX } from "solid-js"; |
| 4 | |
| 5 | /** |
| 6 | * A checkbox drawn in the page's style around a real input, so Space, label clicks and forms behave natively. A string |
| 7 | * `disabled` is the reason, shown as the tooltip; `checked` without `onChange` is just the initial state in a form. The |
| 8 | * box flips at once, and once `onChange`'s promise settles it shows `checked` again, so a failed save flips it back. |
| 9 | */ |
| 10 | export function Checkbox(props: { |
| 11 | checked?: boolean; |
| 12 | indeterminate?: boolean; |
| 13 | disabled?: boolean | string; |
| 14 | name?: string; |
| 15 | value?: string; |
| 16 | onChange?: (checked: boolean) => unknown; |
| 17 | children: JSX.Element; |
| 18 | }) { |
| 19 | const reason = () => (typeof props.disabled === "string" ? props.disabled : undefined); |
| 20 | let input!: HTMLInputElement; |
| 21 | createEffect(() => (input.indeterminate = !!props.indeterminate)); |
| 22 | return ( |
| 23 | <label class="checkbox" data-tip={reason()} tabIndex={reason() ? 0 : undefined}> |
| 24 | <input ref={input} type="checkbox" name={props.name} value={props.value} checked={props.checked} disabled={!!props.disabled} |
| 25 | onChange={async (event) => { |
| 26 | if (!props.onChange) return; |
| 27 | try { |
| 28 | await props.onChange(event.currentTarget.checked); |
| 29 | } finally { |
| 30 | input.checked = !!props.checked; |
| 31 | } |
| 32 | }} /> |
| 33 | <span class="box" aria-hidden="true"><Check class="tick" /><Minus class="dash" /></span> |
| 34 | {props.children} |
| 35 | </label> |
| 36 | ); |
| 37 | } |