@wtfalch/design 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -17,16 +17,36 @@
17
17
  * **This is not a `Toggle`.** A switch is the action and applies as it moves; a
18
18
  * checkbox is an answer that applies when something else is pressed. If there
19
19
  * is no Save at the end of it, this is the wrong control — see `Toggle`.
20
+ *
21
+ * **It can post itself.** `checked`/`onChange` were required and there was no
22
+ * `name`, which made this component unusable in a plain `<form action=…>`:
23
+ * with nothing to submit under, a caller wanting one boolean in a Server
24
+ * Action had to render `<input type="checkbox" name="x">` by hand and wrap it
25
+ * in its own `<label>`. That is exactly the native tick box this component
26
+ * exists to replace, and it reappeared the moment the form was uncontrolled —
27
+ * manage's break-glass form carried one, with a comment explaining why it had
28
+ * to. So `name` and `value` are passed through, and `checked` is optional:
29
+ * give it `checked` and `onChange` for a controlled box, `defaultChecked` (or
30
+ * neither) for one the form reads at submit.
20
31
  */
21
- export default function Checkbox({ label, hint, meta, checked, onChange, disabled, className, }: {
32
+ export default function Checkbox({ label, hint, meta, checked, defaultChecked, onChange, name, value, disabled, className, }: {
22
33
  label: React.ReactNode;
23
34
  /** What choosing it means, or what it costs. Under the name. `hint`, the
24
35
  * word `Toggle`, `Slider` and `Field` use for the same line; it was `why`. */
25
36
  hint?: React.ReactNode;
26
37
  /** A quieter third line — a path, a size, an id. */
27
38
  meta?: React.ReactNode;
28
- checked: boolean;
29
- onChange: (on: boolean) => void;
39
+ /** Controlled. Omit it, with `name`, for a box a form reads at submit. */
40
+ checked?: boolean;
41
+ /** The uncontrolled starting state. Ignored when `checked` is given. */
42
+ defaultChecked?: boolean;
43
+ onChange?: (on: boolean) => void;
44
+ /** What the form submits this under. Without it there is nothing to post,
45
+ * which is what sent callers back to a native tick box. */
46
+ name?: string;
47
+ /** What the form submits when it is ticked. The browser's default is `on`,
48
+ * which is rarely the word a Server Action wants to read. */
49
+ value?: string;
30
50
  disabled?: boolean;
31
51
  className?: string;
32
52
  }): import("react").JSX.Element;
@@ -18,14 +18,30 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
18
18
  * **This is not a `Toggle`.** A switch is the action and applies as it moves; a
19
19
  * checkbox is an answer that applies when something else is pressed. If there
20
20
  * is no Save at the end of it, this is the wrong control — see `Toggle`.
21
+ *
22
+ * **It can post itself.** `checked`/`onChange` were required and there was no
23
+ * `name`, which made this component unusable in a plain `<form action=…>`:
24
+ * with nothing to submit under, a caller wanting one boolean in a Server
25
+ * Action had to render `<input type="checkbox" name="x">` by hand and wrap it
26
+ * in its own `<label>`. That is exactly the native tick box this component
27
+ * exists to replace, and it reappeared the moment the form was uncontrolled —
28
+ * manage's break-glass form carried one, with a comment explaining why it had
29
+ * to. So `name` and `value` are passed through, and `checked` is optional:
30
+ * give it `checked` and `onChange` for a controlled box, `defaultChecked` (or
31
+ * neither) for one the form reads at submit.
21
32
  */
22
33
  import { Checkbox as AriaCheckbox } from 'react-aria-components';
23
- export default function Checkbox({ label, hint, meta, checked, onChange, disabled, className, }) {
34
+ export default function Checkbox({ label, hint, meta, checked, defaultChecked, onChange, name, value, disabled, className, }) {
24
35
  /* React Aria's `Checkbox` is the `<label>`. It keeps the real input in the
25
36
  markup and visually hidden -- exactly the arrangement this component
26
37
  already had by hand -- and stamps `data-selected`, `data-focus-visible`
27
38
  and `data-disabled` on the row. The ring and the tick are still drawn by
28
39
  the row's `::after` and `::before`; `checkbox.css` now reads the state off
29
40
  those attributes instead of `:has(input:checked)`. */
30
- return (_jsx(AriaCheckbox, { className: `choice${className ? ` ${className}` : ''}`, isSelected: checked, onChange: onChange, isDisabled: disabled, children: _jsxs("span", { className: "choice-body", children: [_jsx("span", { className: "choice-name", children: label }), hint && _jsx("span", { className: "choice-why", children: hint }), meta && _jsx("span", { className: "choice-meta", children: meta })] }) }));
41
+ return (_jsx(AriaCheckbox, { className: `choice${className ? ` ${className}` : ''}`,
42
+ /* `undefined` is what makes React Aria leave the box uncontrolled, so
43
+ the controlled and uncontrolled cases are the same call: pass both and
44
+ let whichever was given decide. Passing `isSelected={false}` here
45
+ instead would silently pin every uncontrolled box to off. */
46
+ isSelected: checked, defaultSelected: defaultChecked, onChange: onChange, name: name, value: value, isDisabled: disabled, children: _jsxs("span", { className: "choice-body", children: [_jsx("span", { className: "choice-name", children: label }), hint && _jsx("span", { className: "choice-why", children: hint }), meta && _jsx("span", { className: "choice-meta", children: meta })] }) }));
31
47
  }
@@ -33,6 +33,19 @@ export interface FieldWiring {
33
33
  id: string;
34
34
  'aria-describedby': string | undefined;
35
35
  'aria-invalid': boolean | undefined;
36
+ /** The label element's own id, for a control a `<label for>` cannot name.
37
+ *
38
+ * `htmlFor` is enough for an `<input>`, which is what almost every caller
39
+ * wraps. It is not enough for `Select`, or for anything else built on a
40
+ * `<button>`: a button takes its accessible name from its *contents*, and
41
+ * a `<label for>` pointing at one is ignored by the name computation. A
42
+ * caller that put a `Select` in a `Field` got a label on screen and a
43
+ * control announcing only its current value — and the workaround was an
44
+ * `aria-label` repeating the label string, which is two literals and two
45
+ * chances to drift apart. That drift is exactly the bug this component
46
+ * exists to prevent, so: pass this as `aria-labelledby` and there is one
47
+ * string in one place. */
48
+ labelId: string;
36
49
  }
37
50
  export default function Field({ label, hint, error, required, children, labelHidden, layout, className, }: {
38
51
  /** What the field is. Always given -- there is no unlabelled case, only
@@ -35,8 +35,10 @@ export default function Field({ label, hint, error, required, children, labelHid
35
35
  const id = useId();
36
36
  const hintId = `${id}-hint`;
37
37
  const errorId = `${id}-error`;
38
- return (_jsxs("div", { className: `field field-${layout}-layout${error ? ' field-bad' : ''}${className ? ` ${className}` : ''}`, children: [_jsxs("label", { className: labelHidden ? 'sr-only' : 'field-label', htmlFor: id, children: [label, required && (_jsxs("span", { className: "field-required", "aria-label": "required", children: [' ', "*"] }))] }), hint && (_jsx("p", { className: "field-hint", id: hintId, children: hint })), children({
38
+ const labelId = `${id}-label`;
39
+ return (_jsxs("div", { className: `field field-${layout}-layout${error ? ' field-bad' : ''}${className ? ` ${className}` : ''}`, children: [_jsxs("label", { id: labelId, className: labelHidden ? 'sr-only' : 'field-label', htmlFor: id, children: [label, required && (_jsxs("span", { className: "field-required", "aria-label": "required", children: [' ', "*"] }))] }), hint && (_jsx("p", { className: "field-hint", id: hintId, children: hint })), children({
39
40
  id,
41
+ labelId,
40
42
  /* Both, in reading order, when both are there. A field that has a rule
41
43
  and has broken it needs to say the rule too -- "must be a URL" on its
42
44
  own does not tell you what shape of URL. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wtfalch/design",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "wtfalch's design system: themeable components on a fixed token vocabulary.",
5
5
  "keywords": [
6
6
  "design-system",