@charcuterie/ui 3.6.0 → 3.7.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.
@@ -2,6 +2,20 @@ import type { ControlSize } from "@charcuterie/tokens";
2
2
  import type { ReactNode } from "react";
3
3
  export type CheckboxProps = {
4
4
  className?: string;
5
+ /**
6
+ * Standing help under the box — units, a consequence, a warning
7
+ * about what ticking it does. Same slot, same words and same
8
+ * announcement order as `Field`'s: bound with `aria-describedby`,
9
+ * so a screen reader reads it **with** the control rather than
10
+ * announcing it from nowhere.
11
+ *
12
+ * It renders **outside** the `<label>` on purpose. A `<label>`'s
13
+ * text content *is* the control's accessible name, so a hint put
14
+ * inside it would be read twice — once as part of the name, once as
15
+ * the description — and a pointer press anywhere on the hint would
16
+ * toggle the box.
17
+ */
18
+ description?: ReactNode;
5
19
  /**
6
20
  * The caller's own id, when it has one — a stable target for an
7
21
  * autofill hint or a deep link. Left to React's `useId` on the
@@ -74,6 +88,30 @@ export type CheckboxProps = {
74
88
  * also the one label association that needs no `for` at all, so there
75
89
  * is no minted-then-dropped id to get wrong.
76
90
  *
91
+ * ### A description is the one thing that cannot go in the `<label>`
92
+ *
93
+ * `Field` has had a `description` since it shipped and this component
94
+ * had none, so a hint could not follow its checkbox. Downstream that
95
+ * meant one hint out of three in a group stayed an app-styled
96
+ * paragraph and the box carried two hint typographies.
97
+ *
98
+ * The slot is here now, and it renders **outside** the `<label>`.
99
+ * A `<label>`'s text content is the control's accessible name, so a
100
+ * hint inside it is announced twice — once inside the name, once
101
+ * again as the description — and a pointer press on the hint toggles
102
+ * the box, which is not what a sentence of explanation should do. So
103
+ * the `<label>` is no longer the outermost element: a `<div>` holds
104
+ * the row and the hint, `className` moves out to it (`className` is
105
+ * the outermost box a component renders, and that rule does not bend
106
+ * for this one), and `aria-describedby` binds the two back together.
107
+ *
108
+ * There is no `error` here to go with it. `Field` derives
109
+ * `aria-invalid` from its error, and a lone boolean is almost never
110
+ * the invalid thing — a **group** of them is, and a group's error
111
+ * belongs on the `FieldGroup` around it, described rather than
112
+ * asserted. Adding one per box would put `aria-invalid` on a control
113
+ * that is not itself wrong.
114
+ *
77
115
  * ### The `<input>` is the store
78
116
  *
79
117
  * `defaultChecked`, not `checked`. The DOM owns a checkbox's state
@@ -83,5 +121,5 @@ export type CheckboxProps = {
83
121
  * `<input>`'s job. `isChecked` seeds the first paint and nothing
84
122
  * after.
85
123
  */
86
- export declare const Checkbox: ({ className, id, isChecked, isDisabled, isReadOnly, label, onChange, size, value, }: CheckboxProps) => ReactNode;
124
+ export declare const Checkbox: ({ className, description, id, isChecked, isDisabled, isReadOnly, label, onChange, size, value, }: CheckboxProps) => ReactNode;
87
125
  //# sourceMappingURL=Checkbox.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Checkbox.d.ts","sourceRoot":"","sources":["../../src/Checkbox/Checkbox.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAA;AACtD,OAAO,KAAK,EAAe,SAAS,EAAE,MAAM,OAAO,CAAA;AAKnD,MAAM,MAAM,aAAa,GAAG;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;;OAIG;IACH,EAAE,CAAC,EAAE,MAAM,CAAA;IACX;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB;;;;;OAKG;IACH,KAAK,EAAE,SAAS,CAAA;IAChB,QAAQ,CAAC,EAAE,CAAC,SAAS,EAAE,OAAO,KAAK,IAAI,CAAA;IACvC,IAAI,CAAC,EAAE,WAAW,CAAA;IAClB;;;;;;;;;;;;;;;;;OAiBG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;CACf,CAAA;AAoBD;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,eAAO,MAAM,QAAQ,GAAI,qFAUtB,aAAa,KAAG,SA0IlB,CAAA"}
1
+ {"version":3,"file":"Checkbox.d.ts","sourceRoot":"","sources":["../../src/Checkbox/Checkbox.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAA;AACtD,OAAO,KAAK,EAAe,SAAS,EAAE,MAAM,OAAO,CAAA;AAKnD,MAAM,MAAM,aAAa,GAAG;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;;;;;;;;;;OAYG;IACH,WAAW,CAAC,EAAE,SAAS,CAAA;IACvB;;;;OAIG;IACH,EAAE,CAAC,EAAE,MAAM,CAAA;IACX;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB;;;;;OAKG;IACH,KAAK,EAAE,SAAS,CAAA;IAChB,QAAQ,CAAC,EAAE,CAAC,SAAS,EAAE,OAAO,KAAK,IAAI,CAAA;IACvC,IAAI,CAAC,EAAE,WAAW,CAAA;IAClB;;;;;;;;;;;;;;;;;OAiBG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;CACf,CAAA;AAuCD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoDG;AACH,eAAO,MAAM,QAAQ,GAAI,kGAWtB,aAAa,KAAG,SA4LlB,CAAA"}
@@ -1,4 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useUniqueId } from "@charcuterie/logic";
2
3
  import { FOCUS_RING_CLASS } from "../intentStyles.js";
3
4
  import { toClassName } from "../toClassName.js";
4
5
  const BOX_SIZE_CLASS = {
@@ -16,6 +17,21 @@ const TEXT_SIZE_CLASS = {
16
17
  md: "text-md",
17
18
  lg: "text-lg",
18
19
  };
20
+ /**
21
+ * The description hangs under the **label text**, not under the box,
22
+ * so the hint and the words it explains share a left edge.
23
+ *
24
+ * Written out per size rather than computed, because the offset is
25
+ * `BOX_SIZE_CLASS` plus the label row's `gap-2` and a class name is
26
+ * never interpolated — `` `ps-${…}` `` generates nothing and fails
27
+ * silently. sm is 0.875rem + 0.5rem, md is 1rem + 0.5rem, lg is
28
+ * 1.25rem + 0.5rem.
29
+ */
30
+ const DESCRIPTION_INDENT_CLASS = {
31
+ sm: "ps-5.5",
32
+ md: "ps-6",
33
+ lg: "ps-7",
34
+ };
19
35
  /**
20
36
  * A single boolean, on or off, that owns nothing but itself.
21
37
  *
@@ -36,6 +52,30 @@ const TEXT_SIZE_CLASS = {
36
52
  * also the one label association that needs no `for` at all, so there
37
53
  * is no minted-then-dropped id to get wrong.
38
54
  *
55
+ * ### A description is the one thing that cannot go in the `<label>`
56
+ *
57
+ * `Field` has had a `description` since it shipped and this component
58
+ * had none, so a hint could not follow its checkbox. Downstream that
59
+ * meant one hint out of three in a group stayed an app-styled
60
+ * paragraph and the box carried two hint typographies.
61
+ *
62
+ * The slot is here now, and it renders **outside** the `<label>`.
63
+ * A `<label>`'s text content is the control's accessible name, so a
64
+ * hint inside it is announced twice — once inside the name, once
65
+ * again as the description — and a pointer press on the hint toggles
66
+ * the box, which is not what a sentence of explanation should do. So
67
+ * the `<label>` is no longer the outermost element: a `<div>` holds
68
+ * the row and the hint, `className` moves out to it (`className` is
69
+ * the outermost box a component renders, and that rule does not bend
70
+ * for this one), and `aria-describedby` binds the two back together.
71
+ *
72
+ * There is no `error` here to go with it. `Field` derives
73
+ * `aria-invalid` from its error, and a lone boolean is almost never
74
+ * the invalid thing — a **group** of them is, and a group's error
75
+ * belongs on the `FieldGroup` around it, described rather than
76
+ * asserted. Adding one per box would put `aria-invalid` on a control
77
+ * that is not itself wrong.
78
+ *
39
79
  * ### The `<input>` is the store
40
80
  *
41
81
  * `defaultChecked`, not `checked`. The DOM owns a checkbox's state
@@ -45,75 +85,95 @@ const TEXT_SIZE_CLASS = {
45
85
  * `<input>`'s job. `isChecked` seeds the first paint and nothing
46
86
  * after.
47
87
  */
48
- export const Checkbox = ({ className, id, isChecked = false, isDisabled = false, isReadOnly = false, label, onChange, size = "md", value, }) => (_jsxs("label", { className: toClassName("inline-flex items-center gap-2 text-content-secondary select-none", TEXT_SIZE_CLASS[size],
49
- // One cursor, chosen by state — two cursor utilities of the same
50
- // specificity do not resolve by class-list order.
51
- isDisabled
52
- ? "cursor-not-allowed"
53
- : isReadOnly
54
- ? "cursor-default"
55
- : "cursor-pointer",
56
- // Disabled dims the whole control with opacity rather than fading
57
- // the box to a quieter token. The token scale has no step between
58
- // `border-default` and `border-strong`, so a muted outline was
59
- // either invisible or indistinguishable from enabled; `opacity-60`
60
- // keeps the full shape and colour and just turns them down, which
61
- // is the "present but inactive" the box actually is. Read-only is
62
- // *not* dimmed — its whole point is that the value stays readable.
63
- // The same treatment is on `Switch` and `RadioGroup`.
64
- isDisabled && "opacity-60", className), children: [_jsxs("span", { className: "relative grid shrink-0 place-items-center", children: [_jsx("input", { "aria-readonly": isReadOnly || undefined, className: toClassName(
65
- // `border-2` is the family's shared weight — the `Switch`
66
- // track's `ring-2` and the `RadioGroup` ring match it, so the
67
- // three read as one set — and a 2px edge is also what keeps a
68
- // disabled box visible where a 1px one washed out.
69
- "peer appearance-none rounded-sm border-2 bg-surface-sunken transition-colors duration-(--duration-fast) ease-standard",
70
- // Read-only wears the **neutral** intent, not the accent: full
71
- // contrast (so the value reads) but plainly not an actionable
72
- // accent control, with a softer `border-default` resting edge.
73
- // Three distinct states result — enabled (accent, crisp),
74
- // disabled (accent, dimmed by the wrapper), read-only (neutral).
75
- // No `disabled:` colour override; the wrapper's `opacity-60`
76
- // does the dimming, and doubling it on the box is what made a
77
- // disabled checkbox vanish.
78
- // `indeterminate:` wears the same fill as `checked:`. It is
79
- // the state a *group* box is in when some of its members are
80
- // ticked, and it cannot be written in markup at all —
81
- // `indeterminate` is a DOM property with no HTML attribute —
82
- // so the only way to reach it is a consumer writing to the
83
- // input. `DataTable`'s select-all box does exactly that, and
84
- // before this line it painted **nothing**: the property was
85
- // set, screen readers announced "mixed", and the box on
86
- // screen was indistinguishable from empty.
87
- isReadOnly
88
- ? "border-border-default checked:border-intent-neutral-solid checked:bg-intent-neutral-solid indeterminate:border-intent-neutral-solid indeterminate:bg-intent-neutral-solid"
89
- : "border-border-strong checked:border-intent-accent-solid checked:bg-intent-accent-solid indeterminate:border-intent-accent-solid indeterminate:bg-intent-accent-solid", isDisabled
90
- ? "cursor-not-allowed"
91
- : isReadOnly
92
- ? "cursor-default"
93
- : "cursor-pointer", BOX_SIZE_CLASS[size], FOCUS_RING_CLASS), defaultChecked: isChecked, disabled: isDisabled, id: id, onChange: (event) => {
94
- if (isReadOnly) {
95
- return;
96
- }
97
- onChange?.(event.target.checked);
98
- }, onClick: (clickEvent) => {
99
- // Native checkboxes have no `readonly`, so the toggle is
100
- // blocked here — `preventDefault` on the click stops the
101
- // checked flip a pointer would make.
102
- if (isReadOnly) {
103
- clickEvent.preventDefault();
104
- }
105
- }, onKeyDown: (keyEvent) => {
106
- // …and Space, the keyboard toggle a click block does not
107
- // cover.
108
- if (isReadOnly && keyEvent.key === " ") {
109
- keyEvent.preventDefault();
110
- }
111
- }, type: "checkbox", value: value }), _jsx("svg", { "aria-hidden": "true", className: toClassName("pointer-events-none invisible absolute peer-checked:visible peer-indeterminate:invisible",
112
- // Matches whichever fill the box wears, so the tick keeps its
113
- // guaranteed on-solid contrast in either intent.
114
- isReadOnly
115
- ? "text-intent-neutral-on-solid"
116
- : "text-intent-accent-on-solid", CHECK_SIZE_CLASS[size]), fill: "none", stroke: "currentColor", strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 3, viewBox: "0 0 24 24", children: _jsx("path", { d: "M20 6 9 17l-5-5" }) }), _jsx("svg", { "aria-hidden": "true", className: toClassName("pointer-events-none invisible absolute peer-indeterminate:visible", isReadOnly
117
- ? "text-intent-neutral-on-solid"
118
- : "text-intent-accent-on-solid", CHECK_SIZE_CLASS[size]), fill: "none", stroke: "currentColor", strokeLinecap: "round", strokeWidth: 3, viewBox: "0 0 24 24", children: _jsx("path", { d: "M6 12h12" }) })] }), _jsx("span", { children: label })] }));
88
+ export const Checkbox = ({ className, description, id, isChecked = false, isDisabled = false, isReadOnly = false, label, onChange, size = "md", value, }) => {
89
+ const baseId = useUniqueId();
90
+ const descriptionId = `${baseId}-description`;
91
+ return (_jsxs("div", { className: toClassName(
92
+ // The row and its hint stack, and the box stays inline-level so
93
+ // a `Checkbox` still sits in a sentence, a table cell or a flex
94
+ // row exactly where the bare `<label>` used to.
95
+ "inline-flex flex-col gap-1", className), children: [_jsxs("label", { className: toClassName("inline-flex items-center gap-2 text-content-secondary select-none", TEXT_SIZE_CLASS[size],
96
+ // One cursor, chosen by state — two cursor utilities of the same
97
+ // specificity do not resolve by class-list order.
98
+ isDisabled
99
+ ? "cursor-not-allowed"
100
+ : isReadOnly
101
+ ? "cursor-default"
102
+ : "cursor-pointer",
103
+ // Disabled dims the whole control with opacity rather than fading
104
+ // the box to a quieter token. The token scale has no step between
105
+ // `border-default` and `border-strong`, so a muted outline was
106
+ // either invisible or indistinguishable from enabled; `opacity-60`
107
+ // keeps the full shape and colour and just turns them down, which
108
+ // is the "present but inactive" the box actually is. Read-only is
109
+ // *not* dimmed — its whole point is that the value stays readable.
110
+ // The same treatment is on `Switch` and `RadioGroup`.
111
+ //
112
+ // It stays on the `<label>` and does NOT reach the description
113
+ // below, which is a contrast decision rather than an oversight.
114
+ // axe exempts a disabled control and its own label from the
115
+ // colour-contrast rule; a sibling paragraph gets no such
116
+ // exemption, and `content-secondary` at 60% measures **3.77:1**
117
+ // on `midnight` against a 4.5 threshold. A hint nobody can read
118
+ // is worse than a hint that does not dim with its control, and
119
+ // the hint is the part a user reads precisely when they are
120
+ // asking why the box is turned off.
121
+ isDisabled && "opacity-60"), children: [_jsxs("span", { className: "relative grid shrink-0 place-items-center", children: [_jsx("input", { "aria-describedby": description === undefined
122
+ ? undefined
123
+ : descriptionId, "aria-readonly": isReadOnly || undefined, className: toClassName(
124
+ // `border-2` is the family's shared weight — the `Switch`
125
+ // track's `ring-2` and the `RadioGroup` ring match it, so the
126
+ // three read as one set — and a 2px edge is also what keeps a
127
+ // disabled box visible where a 1px one washed out.
128
+ "peer appearance-none rounded-sm border-2 bg-surface-sunken transition-colors duration-(--duration-fast) ease-standard",
129
+ // Read-only wears the **neutral** intent, not the accent: full
130
+ // contrast (so the value reads) but plainly not an actionable
131
+ // accent control, with a softer `border-default` resting edge.
132
+ // Three distinct states result — enabled (accent, crisp),
133
+ // disabled (accent, dimmed by the wrapper), read-only (neutral).
134
+ // No `disabled:` colour override; the wrapper's `opacity-60`
135
+ // does the dimming, and doubling it on the box is what made a
136
+ // disabled checkbox vanish.
137
+ // `indeterminate:` wears the same fill as `checked:`. It is
138
+ // the state a *group* box is in when some of its members are
139
+ // ticked, and it cannot be written in markup at all —
140
+ // `indeterminate` is a DOM property with no HTML attribute —
141
+ // so the only way to reach it is a consumer writing to the
142
+ // input. `DataTable`'s select-all box does exactly that, and
143
+ // before this line it painted **nothing**: the property was
144
+ // set, screen readers announced "mixed", and the box on
145
+ // screen was indistinguishable from empty.
146
+ isReadOnly
147
+ ? "border-border-default checked:border-intent-neutral-solid checked:bg-intent-neutral-solid indeterminate:border-intent-neutral-solid indeterminate:bg-intent-neutral-solid"
148
+ : "border-border-strong checked:border-intent-accent-solid checked:bg-intent-accent-solid indeterminate:border-intent-accent-solid indeterminate:bg-intent-accent-solid", isDisabled
149
+ ? "cursor-not-allowed"
150
+ : isReadOnly
151
+ ? "cursor-default"
152
+ : "cursor-pointer", BOX_SIZE_CLASS[size], FOCUS_RING_CLASS), defaultChecked: isChecked, disabled: isDisabled, id: id, onChange: (event) => {
153
+ if (isReadOnly) {
154
+ return;
155
+ }
156
+ onChange?.(event.target.checked);
157
+ }, onClick: (clickEvent) => {
158
+ // Native checkboxes have no `readonly`, so the toggle is
159
+ // blocked here — `preventDefault` on the click stops the
160
+ // checked flip a pointer would make.
161
+ if (isReadOnly) {
162
+ clickEvent.preventDefault();
163
+ }
164
+ }, onKeyDown: (keyEvent) => {
165
+ // …and Space, the keyboard toggle a click block does not
166
+ // cover.
167
+ if (isReadOnly && keyEvent.key === " ") {
168
+ keyEvent.preventDefault();
169
+ }
170
+ }, type: "checkbox", value: value }), _jsx("svg", { "aria-hidden": "true", className: toClassName("pointer-events-none invisible absolute peer-checked:visible peer-indeterminate:invisible",
171
+ // Matches whichever fill the box wears, so the tick keeps its
172
+ // guaranteed on-solid contrast in either intent.
173
+ isReadOnly
174
+ ? "text-intent-neutral-on-solid"
175
+ : "text-intent-accent-on-solid", CHECK_SIZE_CLASS[size]), fill: "none", stroke: "currentColor", strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 3, viewBox: "0 0 24 24", children: _jsx("path", { d: "M20 6 9 17l-5-5" }) }), _jsx("svg", { "aria-hidden": "true", className: toClassName("pointer-events-none invisible absolute peer-indeterminate:visible", isReadOnly
176
+ ? "text-intent-neutral-on-solid"
177
+ : "text-intent-accent-on-solid", CHECK_SIZE_CLASS[size]), fill: "none", stroke: "currentColor", strokeLinecap: "round", strokeWidth: 3, viewBox: "0 0 24 24", children: _jsx("path", { d: "M6 12h12" }) })] }), _jsx("span", { children: label })] }), description === undefined ? null : (_jsx("p", { className: toClassName("text-content-secondary text-sm", DESCRIPTION_INDENT_CLASS[size]), id: descriptionId, children: description }))] }));
178
+ };
119
179
  //# sourceMappingURL=Checkbox.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"Checkbox.js","sourceRoot":"","sources":["../../src/Checkbox/Checkbox.tsx"],"names":[],"mappings":";AAGA,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AACrD,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAyD/C,MAAM,cAAc,GAAgC;IAClD,EAAE,EAAE,UAAU;IACd,EAAE,EAAE,QAAQ;IACZ,EAAE,EAAE,QAAQ;CACb,CAAA;AAED,MAAM,gBAAgB,GAAgC;IACpD,EAAE,EAAE,UAAU;IACd,EAAE,EAAE,QAAQ;IACZ,EAAE,EAAE,UAAU;CACf,CAAA;AAED,MAAM,eAAe,GAAgC;IACnD,EAAE,EAAE,SAAS;IACb,EAAE,EAAE,SAAS;IACb,EAAE,EAAE,SAAS;CACd,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,EACvB,SAAS,EACT,EAAE,EACF,SAAS,GAAG,KAAK,EACjB,UAAU,GAAG,KAAK,EAClB,UAAU,GAAG,KAAK,EAClB,KAAK,EACL,QAAQ,EACR,IAAI,GAAG,IAAI,EACX,KAAK,GACS,EAAa,EAAE,CAAC,CAC9B,iBACE,SAAS,EAAE,WAAW,CACpB,mEAAmE,EACnE,eAAe,CAAC,IAAI,CAAC;IACrB,iEAAiE;IACjE,kDAAkD;IAClD,UAAU;QACR,CAAC,CAAC,oBAAoB;QACtB,CAAC,CAAC,UAAU;YACV,CAAC,CAAC,gBAAgB;YAClB,CAAC,CAAC,gBAAgB;IACtB,kEAAkE;IAClE,kEAAkE;IAClE,+DAA+D;IAC/D,mEAAmE;IACnE,kEAAkE;IAClE,kEAAkE;IAClE,mEAAmE;IACnE,sDAAsD;IACtD,UAAU,IAAI,YAAY,EAC1B,SAAS,CACV,aAED,gBAAM,SAAS,EAAC,2CAA2C,aACzD,iCACiB,UAAU,IAAI,SAAS,EACtC,SAAS,EAAE,WAAW;oBACpB,0DAA0D;oBAC1D,8DAA8D;oBAC9D,8DAA8D;oBAC9D,mDAAmD;oBACnD,uHAAuH;oBACvH,+DAA+D;oBAC/D,8DAA8D;oBAC9D,+DAA+D;oBAC/D,0DAA0D;oBAC1D,iEAAiE;oBACjE,6DAA6D;oBAC7D,8DAA8D;oBAC9D,4BAA4B;oBAC5B,4DAA4D;oBAC5D,6DAA6D;oBAC7D,sDAAsD;oBACtD,6DAA6D;oBAC7D,2DAA2D;oBAC3D,6DAA6D;oBAC7D,4DAA4D;oBAC5D,wDAAwD;oBACxD,2CAA2C;oBAC3C,UAAU;wBACR,CAAC,CAAC,2KAA2K;wBAC7K,CAAC,CAAC,sKAAsK,EAC1K,UAAU;wBACR,CAAC,CAAC,oBAAoB;wBACtB,CAAC,CAAC,UAAU;4BACV,CAAC,CAAC,gBAAgB;4BAClB,CAAC,CAAC,gBAAgB,EACtB,cAAc,CAAC,IAAI,CAAC,EACpB,gBAAgB,CACjB,EACD,cAAc,EAAE,SAAS,EACzB,QAAQ,EAAE,UAAU,EACpB,EAAE,EAAE,EAAE,EACN,QAAQ,EAAE,CACR,KAAoC,EACpC,EAAE;wBACF,IAAI,UAAU,EAAE,CAAC;4BACf,OAAM;wBACR,CAAC;wBAED,QAAQ,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;oBAClC,CAAC,EACD,OAAO,EAAE,CAAC,UAAU,EAAE,EAAE;wBACtB,yDAAyD;wBACzD,yDAAyD;wBACzD,qCAAqC;wBACrC,IAAI,UAAU,EAAE,CAAC;4BACf,UAAU,CAAC,cAAc,EAAE,CAAA;wBAC7B,CAAC;oBACH,CAAC,EACD,SAAS,EAAE,CAAC,QAAQ,EAAE,EAAE;wBACtB,yDAAyD;wBACzD,SAAS;wBACT,IAAI,UAAU,IAAI,QAAQ,CAAC,GAAG,KAAK,GAAG,EAAE,CAAC;4BACvC,QAAQ,CAAC,cAAc,EAAE,CAAA;wBAC3B,CAAC;oBACH,CAAC,EACD,IAAI,EAAC,UAAU,EACf,KAAK,EAAE,KAAK,GACZ,EAEF,6BACc,MAAM,EAClB,SAAS,EAAE,WAAW,CACpB,0FAA0F;oBAC1F,8DAA8D;oBAC9D,iDAAiD;oBACjD,UAAU;wBACR,CAAC,CAAC,8BAA8B;wBAChC,CAAC,CAAC,6BAA6B,EACjC,gBAAgB,CAAC,IAAI,CAAC,CACvB,EACD,IAAI,EAAC,MAAM,EACX,MAAM,EAAC,cAAc,EACrB,aAAa,EAAC,OAAO,EACrB,cAAc,EAAC,OAAO,EACtB,WAAW,EAAE,CAAC,EACd,OAAO,EAAC,WAAW,YAEnB,eAAM,CAAC,EAAC,iBAAiB,GAAG,GACxB,EAMN,6BACc,MAAM,EAClB,SAAS,EAAE,WAAW,CACpB,mEAAmE,EACnE,UAAU;wBACR,CAAC,CAAC,8BAA8B;wBAChC,CAAC,CAAC,6BAA6B,EACjC,gBAAgB,CAAC,IAAI,CAAC,CACvB,EACD,IAAI,EAAC,MAAM,EACX,MAAM,EAAC,cAAc,EACrB,aAAa,EAAC,OAAO,EACrB,WAAW,EAAE,CAAC,EACd,OAAO,EAAC,WAAW,YAEnB,eAAM,CAAC,EAAC,UAAU,GAAG,GACjB,IACD,EAEP,yBAAO,KAAK,GAAQ,IACd,CACT,CAAA"}
1
+ {"version":3,"file":"Checkbox.js","sourceRoot":"","sources":["../../src/Checkbox/Checkbox.tsx"],"names":[],"mappings":"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"}
@@ -1,6 +1,39 @@
1
- import type { ReactElement, ReactNode } from "react";
1
+ import type { ComponentPropsWithRef, ReactElement, ReactNode, Ref } from "react";
2
2
  import type { SlotProps } from "../slotProps.ts";
3
- export type FieldProps = SlotProps & {
3
+ /**
4
+ * Everything else a caller writes on a `Field` reaches **the control
5
+ * it clones onto**, not the `<div>` around it.
6
+ *
7
+ * That is not a new destination, it is the existing one finally
8
+ * declared. `SlotProps` was a *closed* five-key type over a runtime
9
+ * that already spread `...receivedSlotProps` into the clone, so
10
+ * `name`, `placeholder`, `autoFocus`, `onBlur` and every `aria-*` all
11
+ * arrived at the `<input>` and none of them type-checked. The only
12
+ * prop a consumer could legally pass was `className` — which is the
13
+ * one escape hatch the fleet's "configured by props, not a borrowed
14
+ * class" rule is trying to close, so the closed type forced the very
15
+ * thing the rule forbids
16
+ * ([decision](../../../../docs/decisions/2026-08-21-a-slot-components-rest-props-are-the-controls-props.md)).
17
+ *
18
+ * `className` stays on the `<div>`, alone, because a class is a
19
+ * statement about the box a **parent** lays out
20
+ * ([decision](../../../../docs/decisions/2026-08-19-classname-is-the-outermost-box-a-component-renders.md)).
21
+ * Every other prop is a statement about the control, and `id` is the
22
+ * proof: it has named the control since the component shipped, and a
23
+ * dated record settles its precedence
24
+ * ([decision](../../../../docs/decisions/2026-08-05-field-adopts-the-childs-own-id.md)).
25
+ * Routing the rest to the `<div>` would have moved it, silently, in
26
+ * every consumer.
27
+ *
28
+ * `"input"` because `SlotProps` already picks from
29
+ * `InputHTMLAttributes` and this is the same set widened, not a
30
+ * second opinion about what a control is. `ref` is re-declared over
31
+ * `HTMLElement`: the child may be a `Picker`'s `<button>` or a
32
+ * `contenteditable` `<div>`, so `Ref<HTMLInputElement>` would be a
33
+ * narrower claim than the slot makes.
34
+ */
35
+ type ForwardedControlProps = Omit<ComponentPropsWithRef<"input">, "children" | "className" | "ref">;
36
+ export type FieldProps = ForwardedControlProps & SlotProps & {
4
37
  /**
5
38
  * The control. **Cloned, not wrapped** — the same slot contract
6
39
  * as `Popover`'s trigger, so a `Field` can go around an `<input>`,
@@ -8,6 +41,10 @@ export type FieldProps = SlotProps & {
8
41
  * changing that control's layout.
9
42
  */
10
43
  children: ReactElement;
44
+ /**
45
+ * The `<div>` around the label, the control and the messages —
46
+ * the one prop that does **not** reach the control.
47
+ */
11
48
  className?: string;
12
49
  /**
13
50
  * Standing help — units, a format, a warning about what the value
@@ -23,6 +60,11 @@ export type FieldProps = SlotProps & {
23
60
  error?: ReactNode;
24
61
  isRequired?: boolean;
25
62
  label: ReactNode;
63
+ /**
64
+ * Merged onto the control's own, never replacing it — the wiring
65
+ * rule `mergeSlotWiring` states for every slot.
66
+ */
67
+ ref?: Ref<HTMLElement>;
26
68
  };
27
69
  /**
28
70
  * Sixteen files in the fleet spell a label, a control, and a hint,
@@ -99,6 +141,29 @@ export type FieldProps = SlotProps & {
99
141
  * `SlotProps` arriving from an outer slot is forwarded down, and
100
142
  * `aria-describedby` is merged rather than replaced.
101
143
  * `slotProps.ts` has the reasoning.
144
+ *
145
+ * ### …and a pass-through cannot tell who wrote the prop
146
+ *
147
+ * Which is why the rest props are the **control's** props and not the
148
+ * `<div>`'s. `receivedSlotProps` below is one object holding two
149
+ * things that are indistinguishable at runtime: what a cloning
150
+ * ancestor injected, and what the author typed on the `Field` itself.
151
+ * A `Tooltip` above hands down `getReferenceProps()` — hover, focus
152
+ * and dismiss handlers, `refs.setReference`, and an
153
+ * `aria-describedby` naming the tip — and every bit of it has to
154
+ * reach the `<input>` at the bottom. Sending the rest to the wrapper
155
+ * would need a closed runtime allow-list to hold that case back, and
156
+ * an allow-list is `FieldTooltip`'s silent drop rebuilt inside the
157
+ * fix for it: the first slot that writes an `aria-expanded` is broken
158
+ * and nothing says so.
159
+ *
160
+ * So there is one rule, and it has no exceptions to remember:
161
+ * `className` is the box, everything else is the control. A caller
162
+ * that needs a handle on the **box** — an `id` for a screenshot, a
163
+ * `hidden` that takes the label with it — wants `FieldGroup`, which
164
+ * wraps rather than clones and whose rest props land on its own
165
+ * `<fieldset>`.
102
166
  */
103
167
  export declare const Field: ({ children, className, description, error, isRequired, label, ...receivedSlotProps }: FieldProps) => ReactNode;
168
+ export {};
104
169
  //# sourceMappingURL=Field.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Field.d.ts","sourceRoot":"","sources":["../../src/Field/Field.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAEpD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAIhD,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG;IACnC;;;;;OAKG;IACH,QAAQ,EAAE,YAAY,CAAA;IACtB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;OAGG;IACH,WAAW,CAAC,EAAE,SAAS,CAAA;IACvB;;;;;OAKG;IACH,KAAK,CAAC,EAAE,SAAS,CAAA;IACjB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,KAAK,EAAE,SAAS,CAAA;CACjB,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2EG;AACH,eAAO,MAAM,KAAK,GAAI,sFAQnB,UAAU,KAAG,SAkIf,CAAA"}
1
+ {"version":3,"file":"Field.d.ts","sourceRoot":"","sources":["../../src/Field/Field.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,qBAAqB,EACrB,YAAY,EACZ,SAAS,EACT,GAAG,EACJ,MAAM,OAAO,CAAA;AAEd,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAIhD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,KAAK,qBAAqB,GAAG,IAAI,CAC/B,qBAAqB,CAAC,OAAO,CAAC,EAC9B,UAAU,GAAG,WAAW,GAAG,KAAK,CACjC,CAAA;AAED,MAAM,MAAM,UAAU,GAAG,qBAAqB,GAC5C,SAAS,GAAG;IACV;;;;;OAKG;IACH,QAAQ,EAAE,YAAY,CAAA;IACtB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;OAGG;IACH,WAAW,CAAC,EAAE,SAAS,CAAA;IACvB;;;;;OAKG;IACH,KAAK,CAAC,EAAE,SAAS,CAAA;IACjB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,KAAK,EAAE,SAAS,CAAA;IAChB;;;OAGG;IACH,GAAG,CAAC,EAAE,GAAG,CAAC,WAAW,CAAC,CAAA;CACvB,CAAA;AAEH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiGG;AACH,eAAO,MAAM,KAAK,GAAI,sFAQnB,UAAU,KAAG,SAkIf,CAAA"}
@@ -77,6 +77,28 @@ import { toClassName } from "../toClassName.js";
77
77
  * `SlotProps` arriving from an outer slot is forwarded down, and
78
78
  * `aria-describedby` is merged rather than replaced.
79
79
  * `slotProps.ts` has the reasoning.
80
+ *
81
+ * ### …and a pass-through cannot tell who wrote the prop
82
+ *
83
+ * Which is why the rest props are the **control's** props and not the
84
+ * `<div>`'s. `receivedSlotProps` below is one object holding two
85
+ * things that are indistinguishable at runtime: what a cloning
86
+ * ancestor injected, and what the author typed on the `Field` itself.
87
+ * A `Tooltip` above hands down `getReferenceProps()` — hover, focus
88
+ * and dismiss handlers, `refs.setReference`, and an
89
+ * `aria-describedby` naming the tip — and every bit of it has to
90
+ * reach the `<input>` at the bottom. Sending the rest to the wrapper
91
+ * would need a closed runtime allow-list to hold that case back, and
92
+ * an allow-list is `FieldTooltip`'s silent drop rebuilt inside the
93
+ * fix for it: the first slot that writes an `aria-expanded` is broken
94
+ * and nothing says so.
95
+ *
96
+ * So there is one rule, and it has no exceptions to remember:
97
+ * `className` is the box, everything else is the control. A caller
98
+ * that needs a handle on the **box** — an `id` for a screenshot, a
99
+ * `hidden` that takes the label with it — wants `FieldGroup`, which
100
+ * wraps rather than clones and whose rest props land on its own
101
+ * `<fieldset>`.
80
102
  */
81
103
  export const Field = ({ children, className, description, error, isRequired = false, label, ...receivedSlotProps }) => {
82
104
  const baseId = useUniqueId();
@@ -1 +1 @@
1
- {"version":3,"file":"Field.js","sourceRoot":"","sources":["../../src/Field/Field.tsx"],"names":[],"mappings":";AAAA,OAAO,EACL,cAAc,EACd,WAAW,GACZ,MAAM,oBAAoB,CAAA;AAI3B,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAChD,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AA2B/C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2EG;AACH,MAAM,CAAC,MAAM,KAAK,GAAG,CAAC,EACpB,QAAQ,EACR,SAAS,EACT,WAAW,EACX,KAAK,EACL,UAAU,GAAG,KAAK,EAClB,KAAK,EACL,GAAG,iBAAiB,EACT,EAAa,EAAE;IAC1B,MAAM,MAAM,GAAG,WAAW,EAAE,CAAA;IAE5B;;;;;;;;;;;;;;;;OAgBG;IACH,MAAM,OAAO,GAAI,QAAQ,CAAC,KAAyB,CAAC,EAAE,CAAA;IAEtD,MAAM,SAAS,GACb,iBAAiB,CAAC,EAAE,IAAI,OAAO,IAAI,GAAG,MAAM,UAAU,CAAA;IAExD,MAAM,aAAa,GAAG,GAAG,MAAM,cAAc,CAAA;IAE7C,MAAM,OAAO,GAAG,GAAG,MAAM,QAAQ,CAAA;IAEjC,MAAM,WAAW,GAAG;QAClB,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,aAAa;QAChD,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO;KACrC;SACE,MAAM,CAAC,OAAO,CAAC;SACf,IAAI,CAAC,GAAG,CAAC,CAAA;IAEZ,MAAM,aAAa,GAAG,cAAc,CAClC,QAAQ,EACR,cAAc,CAAC,iBAAiB,EAAE;QAChC,kBAAkB,EAChB,WAAW,KAAK,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,WAAW;QAC9C,6DAA6D;QAC7D,+DAA+D;QAC/D,iDAAiD;QACjD,cAAc,EACZ,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI;QACxC,sDAAsD;QACtD,8DAA8D;QAC9D,8DAA8D;QAC9D,gDAAgD;QAChD,eAAe,EAAE,UAAU,IAAI,SAAS;QACxC,EAAE,EAAE,SAAS;QACb,QAAQ,EAAE,UAAU,IAAI,SAAS;KAClC,CAAC,CACH,CAAA;IAED,OAAO,CACL,eACE,SAAS,EAAE,WAAW,CACpB,uBAAuB,EACvB,SAAS,CACV,aAED,iBACE,SAAS,EAAC,0CAA0C,EACpD,OAAO,EAAE,SAAS;gBAClB;;;;;;;;;;;;;;;;mBAgBG;gBACH,EAAE,EAAE,GAAG,SAAS,QAAQ,aAEvB,KAAK,EAEL,UAAU,CAAC,CAAC,CAAC,CACZ,8BAKc,MAAM,EAClB,SAAS,EAAC,iCAAiC,kBAGtC,CACR,CAAC,CAAC,CAAC,IAAI,IACF,EAEP,aAAa,EAUb,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAClC,YACE,SAAS,EAAC,gCAAgC,EAC1C,EAAE,EAAE,aAAa,YAEhB,WAAW,GACV,CACL,EAEA,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAC5B,YACE,SAAS,EAAC,oCAAoC,EAC9C,EAAE,EAAE,OAAO,YAEV,KAAK,GACJ,CACL,IACG,CACP,CAAA;AACH,CAAC,CAAA"}
1
+ {"version":3,"file":"Field.js","sourceRoot":"","sources":["../../src/Field/Field.tsx"],"names":[],"mappings":";AAAA,OAAO,EACL,cAAc,EACd,WAAW,GACZ,MAAM,oBAAoB,CAAA;AAS3B,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAChD,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AA0E/C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiGG;AACH,MAAM,CAAC,MAAM,KAAK,GAAG,CAAC,EACpB,QAAQ,EACR,SAAS,EACT,WAAW,EACX,KAAK,EACL,UAAU,GAAG,KAAK,EAClB,KAAK,EACL,GAAG,iBAAiB,EACT,EAAa,EAAE;IAC1B,MAAM,MAAM,GAAG,WAAW,EAAE,CAAA;IAE5B;;;;;;;;;;;;;;;;OAgBG;IACH,MAAM,OAAO,GAAI,QAAQ,CAAC,KAAyB,CAAC,EAAE,CAAA;IAEtD,MAAM,SAAS,GACb,iBAAiB,CAAC,EAAE,IAAI,OAAO,IAAI,GAAG,MAAM,UAAU,CAAA;IAExD,MAAM,aAAa,GAAG,GAAG,MAAM,cAAc,CAAA;IAE7C,MAAM,OAAO,GAAG,GAAG,MAAM,QAAQ,CAAA;IAEjC,MAAM,WAAW,GAAG;QAClB,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,aAAa;QAChD,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO;KACrC;SACE,MAAM,CAAC,OAAO,CAAC;SACf,IAAI,CAAC,GAAG,CAAC,CAAA;IAEZ,MAAM,aAAa,GAAG,cAAc,CAClC,QAAQ,EACR,cAAc,CAAC,iBAAiB,EAAE;QAChC,kBAAkB,EAChB,WAAW,KAAK,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,WAAW;QAC9C,6DAA6D;QAC7D,+DAA+D;QAC/D,iDAAiD;QACjD,cAAc,EACZ,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI;QACxC,sDAAsD;QACtD,8DAA8D;QAC9D,8DAA8D;QAC9D,gDAAgD;QAChD,eAAe,EAAE,UAAU,IAAI,SAAS;QACxC,EAAE,EAAE,SAAS;QACb,QAAQ,EAAE,UAAU,IAAI,SAAS;KAClC,CAAC,CACH,CAAA;IAED,OAAO,CACL,eACE,SAAS,EAAE,WAAW,CACpB,uBAAuB,EACvB,SAAS,CACV,aAED,iBACE,SAAS,EAAC,0CAA0C,EACpD,OAAO,EAAE,SAAS;gBAClB;;;;;;;;;;;;;;;;mBAgBG;gBACH,EAAE,EAAE,GAAG,SAAS,QAAQ,aAEvB,KAAK,EAEL,UAAU,CAAC,CAAC,CAAC,CACZ,8BAKc,MAAM,EAClB,SAAS,EAAC,iCAAiC,kBAGtC,CACR,CAAC,CAAC,CAAC,IAAI,IACF,EAEP,aAAa,EAUb,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAClC,YACE,SAAS,EAAC,gCAAgC,EAC1C,EAAE,EAAE,aAAa,YAEhB,WAAW,GACV,CACL,EAEA,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAC5B,YACE,SAAS,EAAC,oCAAoC,EAC9C,EAAE,EAAE,OAAO,YAEV,KAAK,GACJ,CACL,IACG,CACP,CAAA;AACH,CAAC,CAAA"}
@@ -1,11 +1,36 @@
1
- import type { ReactNode } from "react";
2
- export type FieldGroupProps = {
1
+ import type { ComponentPropsWithRef, ReactNode } from "react";
2
+ /**
3
+ * Everything a caller writes on a `FieldGroup` reaches the
4
+ * `<fieldset>` — `id`, `hidden`, `data-*`, `aria-*`, `ref`, and the
5
+ * three attributes that are the element's own: `disabled`, `form`
6
+ * and `name`.
7
+ *
8
+ * This is the **opposite** destination from `Field`'s, and the
9
+ * difference is not a preference. `Field` clones onto its child, so
10
+ * its props are that control's props; `FieldGroup` renders its
11
+ * children as-is, so the only element it owns is the `<fieldset>` and
12
+ * a prop has nowhere else to go. One rule reads both:
13
+ * ***a component's props land on the element it owns***
14
+ * ([decision](../../../../docs/decisions/2026-08-21-a-slot-components-rest-props-are-the-controls-props.md)).
15
+ *
16
+ * `disabled` is worth knowing about before reaching for it. On a
17
+ * `<fieldset>` it is not decoration — the platform disables every
18
+ * form control inside the element, not just the box itself. That is
19
+ * usually what a caller wants and occasionally a surprise, and it is
20
+ * a thing a `<div>`-based group could not have offered at all.
21
+ */
22
+ export type FieldGroupProps = ComponentPropsWithRef<"fieldset"> & {
3
23
  /**
4
24
  * The controls. **Rendered as-is, not cloned** — that is the whole
5
25
  * difference from `Field`, and it is why this is a separate
6
26
  * component rather than a boolean on that one.
7
27
  */
8
28
  children: ReactNode;
29
+ /**
30
+ * The `<fieldset>`, which is also the outermost element this
31
+ * component renders — so `className` is not the exception here that
32
+ * it is on `Field`.
33
+ */
9
34
  className?: string;
10
35
  /** Standing help for the group as a whole. */
11
36
  description?: ReactNode;
@@ -54,5 +79,5 @@ export type FieldGroupProps = {
54
79
  * over — the alternative was to clone `aria-invalid` onto every
55
80
  * child, which marks the valid ones invalid.
56
81
  */
57
- export declare const FieldGroup: ({ children, className, description, error, isRequired, label, }: FieldGroupProps) => ReactNode;
82
+ export declare const FieldGroup: ({ children, className, description, error, isRequired, label, ...receivedFieldsetProps }: FieldGroupProps) => ReactNode;
58
83
  //# sourceMappingURL=FieldGroup.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"FieldGroup.d.ts","sourceRoot":"","sources":["../../src/Field/FieldGroup.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAItC,MAAM,MAAM,eAAe,GAAG;IAC5B;;;;OAIG;IACH,QAAQ,EAAE,SAAS,CAAA;IACnB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,8CAA8C;IAC9C,WAAW,CAAC,EAAE,SAAS,CAAA;IACvB,0CAA0C;IAC1C,KAAK,CAAC,EAAE,SAAS,CAAA;IACjB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,KAAK,EAAE,SAAS,CAAA;CACjB,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,eAAO,MAAM,UAAU,GAAI,iEAOxB,eAAe,KAAG,SAuEpB,CAAA"}
1
+ {"version":3,"file":"FieldGroup.d.ts","sourceRoot":"","sources":["../../src/Field/FieldGroup.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,qBAAqB,EACrB,SAAS,EACV,MAAM,OAAO,CAAA;AAKd;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,MAAM,eAAe,GACzB,qBAAqB,CAAC,UAAU,CAAC,GAAG;IAClC;;;;OAIG;IACH,QAAQ,EAAE,SAAS,CAAA;IACnB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,8CAA8C;IAC9C,WAAW,CAAC,EAAE,SAAS,CAAA;IACvB,0CAA0C;IAC1C,KAAK,CAAC,EAAE,SAAS,CAAA;IACjB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,KAAK,EAAE,SAAS,CAAA;CACjB,CAAA;AAEH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,eAAO,MAAM,UAAU,GAAI,0FAQxB,eAAe,KAAG,SAiFpB,CAAA"}
@@ -1,5 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useUniqueId } from "@charcuterie/logic";
3
+ import { mergeSlotProps } from "../slotProps.js";
3
4
  import { toClassName } from "../toClassName.js";
4
5
  /**
5
6
  * A label over **several** controls.
@@ -41,7 +42,7 @@ import { toClassName } from "../toClassName.js";
41
42
  * over — the alternative was to clone `aria-invalid` onto every
42
43
  * child, which marks the valid ones invalid.
43
44
  */
44
- export const FieldGroup = ({ children, className, description, error, isRequired = false, label, }) => {
45
+ export const FieldGroup = ({ children, className, description, error, isRequired = false, label, ...receivedFieldsetProps }) => {
45
46
  const baseId = useUniqueId();
46
47
  const descriptionId = `${baseId}-description`;
47
48
  const errorId = `${baseId}-error`;
@@ -54,7 +55,9 @@ export const FieldGroup = ({ children, className, description, error, isRequired
54
55
  ]
55
56
  .filter(Boolean)
56
57
  .join(" ");
57
- return (_jsxs("fieldset", { "aria-describedby": describedBy === "" ? undefined : describedBy, className: toClassName(
58
+ return (_jsxs("fieldset", { ...mergeSlotProps(receivedFieldsetProps, {
59
+ "aria-describedby": describedBy === "" ? undefined : describedBy,
60
+ }), className: toClassName(
58
61
  // A `<fieldset>` arrives with a border, an inline margin, a
59
62
  // block padding, and `min-inline-size: min-content` — the
60
63
  // last of which makes it refuse to shrink inside a flex or
@@ -1 +1 @@
1
- {"version":3,"file":"FieldGroup.js","sourceRoot":"","sources":["../../src/Field/FieldGroup.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAGhD,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAkB/C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,EACzB,QAAQ,EACR,SAAS,EACT,WAAW,EACX,KAAK,EACL,UAAU,GAAG,KAAK,EAClB,KAAK,GACW,EAAa,EAAE;IAC/B,MAAM,MAAM,GAAG,WAAW,EAAE,CAAA;IAE5B,MAAM,aAAa,GAAG,GAAG,MAAM,cAAc,CAAA;IAE7C,MAAM,OAAO,GAAG,GAAG,MAAM,QAAQ,CAAA;IAEjC,iEAAiE;IACjE,gEAAgE;IAChE,2BAA2B;IAC3B,MAAM,WAAW,GAAG;QAClB,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,aAAa;QAChD,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO;KACrC;SACE,MAAM,CAAC,OAAO,CAAC;SACf,IAAI,CAAC,GAAG,CAAC,CAAA;IAEZ,OAAO,CACL,wCAEI,WAAW,KAAK,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,WAAW,EAE9C,SAAS,EAAE,WAAW;QACpB,4DAA4D;QAC5D,0DAA0D;QAC1D,2DAA2D;QAC3D,4DAA4D;QAC5D,4CAA4C;QAC5C,gDAAgD,EAChD,SAAS,CACV,aAED,kBAAQ,SAAS,EAAC,qDAAqD,aACpE,KAAK,EAEL,UAAU,CAAC,CAAC,CAAC,CACZ,8BAIc,MAAM,EAClB,SAAS,EAAC,iCAAiC,kBAGtC,CACR,CAAC,CAAC,CAAC,IAAI,IACD,EAER,QAAQ,EAIR,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAClC,YACE,SAAS,EAAC,gCAAgC,EAC1C,EAAE,EAAE,aAAa,YAEhB,WAAW,GACV,CACL,EAEA,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAC5B,YACE,SAAS,EAAC,oCAAoC,EAC9C,EAAE,EAAE,OAAO,YAEV,KAAK,GACJ,CACL,IACQ,CACZ,CAAA;AACH,CAAC,CAAA"}
1
+ {"version":3,"file":"FieldGroup.js","sourceRoot":"","sources":["../../src/Field/FieldGroup.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAMhD,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAChD,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AA4C/C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,EACzB,QAAQ,EACR,SAAS,EACT,WAAW,EACX,KAAK,EACL,UAAU,GAAG,KAAK,EAClB,KAAK,EACL,GAAG,qBAAqB,EACR,EAAa,EAAE;IAC/B,MAAM,MAAM,GAAG,WAAW,EAAE,CAAA;IAE5B,MAAM,aAAa,GAAG,GAAG,MAAM,cAAc,CAAA;IAE7C,MAAM,OAAO,GAAG,GAAG,MAAM,QAAQ,CAAA;IAEjC,iEAAiE;IACjE,gEAAgE;IAChE,2BAA2B;IAC3B,MAAM,WAAW,GAAG;QAClB,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,aAAa;QAChD,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO;KACrC;SACE,MAAM,CAAC,OAAO,CAAC;SACf,IAAI,CAAC,GAAG,CAAC,CAAA;IAEZ,OAAO,CACL,uBAUM,cAAc,CAAC,qBAAqB,EAAE;YACxC,kBAAkB,EAChB,WAAW,KAAK,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,WAAW;SAC/C,CAAC,EACF,SAAS,EAAE,WAAW;QACpB,4DAA4D;QAC5D,0DAA0D;QAC1D,2DAA2D;QAC3D,4DAA4D;QAC5D,4CAA4C;QAC5C,gDAAgD,EAChD,SAAS,CACV,aAED,kBAAQ,SAAS,EAAC,qDAAqD,aACpE,KAAK,EAEL,UAAU,CAAC,CAAC,CAAC,CACZ,8BAIc,MAAM,EAClB,SAAS,EAAC,iCAAiC,kBAGtC,CACR,CAAC,CAAC,CAAC,IAAI,IACD,EAER,QAAQ,EAIR,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAClC,YACE,SAAS,EAAC,gCAAgC,EAC1C,EAAE,EAAE,aAAa,YAEhB,WAAW,GACV,CACL,EAEA,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAC5B,YACE,SAAS,EAAC,oCAAoC,EAC9C,EAAE,EAAE,OAAO,YAEV,KAAK,GACJ,CACL,IACQ,CACZ,CAAA;AACH,CAAC,CAAA"}
@@ -116,6 +116,19 @@ export type SlotProps = Pick<InputHTMLAttributes<HTMLElement>, "aria-describedby
116
116
  * inner one won. The outer panel had no anchor and rendered in the
117
117
  * corner of the viewport — no error, no failing test, and it looks
118
118
  * like a CSS bug.
119
+ *
120
+ * ### A wrapping component uses it too, and keeps its own prop type
121
+ *
122
+ * `receivedProps` is generic in `ReceivedProps extends SlotProps`
123
+ * rather than plain `SlotProps`, so the return type carries whatever
124
+ * the caller actually holds instead of narrowing it to the five keys.
125
+ * `FieldGroup` is the reason: it does not clone, its rest props land
126
+ * on its own `<fieldset>`, and it writes an `aria-describedby` that
127
+ * has to **join** a caller's rather than replace it. Narrowing to
128
+ * `SlotProps` on the way out would have dropped `disabled`, `form`,
129
+ * `name` and every `data-*` from the type of the object it spreads
130
+ * onto that element — the props would still arrive at runtime, and
131
+ * only the type would have lied.
119
132
  */
120
- export declare const mergeSlotProps: <OwnProps extends Record<string, unknown>>(receivedProps: SlotProps, ownProps: OwnProps) => OwnProps & SlotProps;
133
+ export declare const mergeSlotProps: <ReceivedProps extends SlotProps, OwnProps extends Record<string, unknown>>(receivedProps: ReceivedProps, ownProps: OwnProps) => OwnProps & ReceivedProps;
121
134
  //# sourceMappingURL=slotProps.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"slotProps.d.ts","sourceRoot":"","sources":["../src/slotProps.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,OAAO,CAAA;AAIhD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiFG;AACH,MAAM,MAAM,SAAS,GAAG,IAAI,CAC1B,mBAAmB,CAAC,WAAW,CAAC,EAC9B,kBAAkB,GAClB,cAAc,GACd,eAAe,GACf,IAAI,GACJ,UAAU,CACb,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,eAAO,MAAM,cAAc,GACzB,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAExC,eAAe,SAAS,EACxB,UAAU,QAAQ,KACjB,QAAQ,GAAG,SA2Bb,CAAA"}
1
+ {"version":3,"file":"slotProps.d.ts","sourceRoot":"","sources":["../src/slotProps.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,OAAO,CAAA;AAIhD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiFG;AACH,MAAM,MAAM,SAAS,GAAG,IAAI,CAC1B,mBAAmB,CAAC,WAAW,CAAC,EAC9B,kBAAkB,GAClB,cAAc,GACd,eAAe,GACf,IAAI,GACJ,UAAU,CACb,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AACH,eAAO,MAAM,cAAc,GACzB,aAAa,SAAS,SAAS,EAC/B,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAExC,eAAe,aAAa,EAC5B,UAAU,QAAQ,KACjB,QAAQ,GAAG,aA2Bb,CAAA"}
package/dist/slotProps.js CHANGED
@@ -33,6 +33,19 @@ import { mergeSlotWiring } from "./slotWiring.js";
33
33
  * inner one won. The outer panel had no anchor and rendered in the
34
34
  * corner of the viewport — no error, no failing test, and it looks
35
35
  * like a CSS bug.
36
+ *
37
+ * ### A wrapping component uses it too, and keeps its own prop type
38
+ *
39
+ * `receivedProps` is generic in `ReceivedProps extends SlotProps`
40
+ * rather than plain `SlotProps`, so the return type carries whatever
41
+ * the caller actually holds instead of narrowing it to the five keys.
42
+ * `FieldGroup` is the reason: it does not clone, its rest props land
43
+ * on its own `<fieldset>`, and it writes an `aria-describedby` that
44
+ * has to **join** a caller's rather than replace it. Narrowing to
45
+ * `SlotProps` on the way out would have dropped `disabled`, `form`,
46
+ * `name` and every `data-*` from the type of the object it spreads
47
+ * onto that element — the props would still arrive at runtime, and
48
+ * only the type would have lied.
36
49
  */
37
50
  export const mergeSlotProps = (receivedProps, ownProps) => {
38
51
  const definedOwnProps = Object.fromEntries(Object.entries(ownProps).filter(([, value]) => value !== undefined));