@charcuterie/ui 2.17.0 → 2.18.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.
- package/dist/Checkbox/Checkbox.d.ts +20 -1
- package/dist/Checkbox/Checkbox.d.ts.map +1 -1
- package/dist/Checkbox/Checkbox.js +2 -2
- package/dist/Checkbox/Checkbox.js.map +1 -1
- package/package.json +1 -1
- package/src/Checkbox/Checkbox.mdx +24 -0
- package/src/Checkbox/Checkbox.stories.tsx +42 -0
- package/src/Checkbox/Checkbox.test.tsx +52 -1
- package/src/Checkbox/Checkbox.tsx +21 -0
|
@@ -34,6 +34,25 @@ export type CheckboxProps = {
|
|
|
34
34
|
label: ReactNode;
|
|
35
35
|
onChange?: (isChecked: boolean) => void;
|
|
36
36
|
size?: ControlSize;
|
|
37
|
+
/**
|
|
38
|
+
* The `<input>`'s `value` attribute — which member of a group this
|
|
39
|
+
* box IS, as opposed to whether it is ticked.
|
|
40
|
+
*
|
|
41
|
+
* A lone boolean does not need this. A GROUP does: the browser
|
|
42
|
+
* submits `name=value` for each ticked box in a form, and, more
|
|
43
|
+
* relevantly here, a group is normally read back with one query —
|
|
44
|
+
* `[...root.querySelectorAll("input")].filter(i => i.checked)
|
|
45
|
+
* .map(i => i.value)`. Without a `value` every box in the group
|
|
46
|
+
* reports the string `"on"` and the read is useless, which is why
|
|
47
|
+
* queuepilot's library and ratings pickers were still hand-rolling
|
|
48
|
+
* a raw `<input type="checkbox">` rather than adopting this
|
|
49
|
+
* component.
|
|
50
|
+
*
|
|
51
|
+
* Passed straight through and otherwise inert. It is NOT the
|
|
52
|
+
* checked state and does not become one: `isChecked` still seeds
|
|
53
|
+
* the box, and `onChange` still reports a boolean.
|
|
54
|
+
*/
|
|
55
|
+
value?: string;
|
|
37
56
|
};
|
|
38
57
|
/**
|
|
39
58
|
* A single boolean, on or off, that owns nothing but itself.
|
|
@@ -64,5 +83,5 @@ export type CheckboxProps = {
|
|
|
64
83
|
* `<input>`'s job. `isChecked` seeds the first paint and nothing
|
|
65
84
|
* after.
|
|
66
85
|
*/
|
|
67
|
-
export declare const Checkbox: ({ className, id, isChecked, isDisabled, isReadOnly, label, onChange, size, }: CheckboxProps) => ReactNode;
|
|
86
|
+
export declare const Checkbox: ({ className, id, isChecked, isDisabled, isReadOnly, label, onChange, size, value, }: CheckboxProps) => ReactNode;
|
|
68
87
|
//# 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;
|
|
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,SA2GlB,CAAA"}
|
|
@@ -45,7 +45,7 @@ const TEXT_SIZE_CLASS = {
|
|
|
45
45
|
* `<input>`'s job. `isChecked` seeds the first paint and nothing
|
|
46
46
|
* after.
|
|
47
47
|
*/
|
|
48
|
-
export const Checkbox = ({ className, id, isChecked = false, isDisabled = false, isReadOnly = false, label, onChange, size = "md", }) => (_jsxs("label", { className: toClassName("inline-flex items-center gap-2 text-content-secondary select-none", TEXT_SIZE_CLASS[size],
|
|
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
49
|
// One cursor, chosen by state — two cursor utilities of the same
|
|
50
50
|
// specificity do not resolve by class-list order.
|
|
51
51
|
isDisabled
|
|
@@ -99,7 +99,7 @@ export const Checkbox = ({ className, id, isChecked = false, isDisabled = false,
|
|
|
99
99
|
if (isReadOnly && keyEvent.key === " ") {
|
|
100
100
|
keyEvent.preventDefault();
|
|
101
101
|
}
|
|
102
|
-
}, type: "checkbox" }), _jsx("svg", { "aria-hidden": "true", className: toClassName("pointer-events-none invisible absolute peer-checked:visible",
|
|
102
|
+
}, type: "checkbox", value: value }), _jsx("svg", { "aria-hidden": "true", className: toClassName("pointer-events-none invisible absolute peer-checked:visible",
|
|
103
103
|
// Matches whichever fill the box wears, so the tick keeps its
|
|
104
104
|
// guaranteed on-solid contrast in either intent.
|
|
105
105
|
isReadOnly
|
|
@@ -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;
|
|
1
|
+
{"version":3,"file":"Checkbox.js","sourceRoot":"","sources":["../../src/Checkbox/Checkbox.tsx"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -62,6 +62,30 @@ is a control every consumer has to hold it for.
|
|
|
62
62
|
here and change elsewhere, not an actionable accent. It announces `aria-readonly`, and
|
|
63
63
|
the toggle is blocked on both pointer and `Space`.
|
|
64
64
|
|
|
65
|
+
## A group of boxes needs `value`
|
|
66
|
+
|
|
67
|
+
`value` is the `<input>`'s `value` attribute: **which member of a group** this box is, as
|
|
68
|
+
opposed to whether it is ticked. A lone boolean should omit it.
|
|
69
|
+
|
|
70
|
+
A group needs it, because a group is read back with one query over its container rather
|
|
71
|
+
than by keeping a callback's worth of state per box:
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
const chosen = [...group.querySelectorAll<HTMLInputElement>("input[type=checkbox]")]
|
|
75
|
+
.filter((input) => input.checked)
|
|
76
|
+
.map((input) => input.value)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Without a `value` every box in the group answers the UA default `"on"`, so that read
|
|
80
|
+
returns N copies of one meaningless string. This is why queuepilot's library and ratings
|
|
81
|
+
pickers were still hand-rolling a raw `<input type="checkbox">` instead of using this
|
|
82
|
+
component — the component was correct and simply could not express the case.
|
|
83
|
+
|
|
84
|
+
It is passed straight through and is otherwise inert: it is **not** the checked state and
|
|
85
|
+
does not become one. `isChecked` still seeds the box, `onChange` still reports a boolean.
|
|
86
|
+
|
|
87
|
+
<Canvas of={CheckboxStories.WithValues} />
|
|
88
|
+
|
|
65
89
|
## Checkbox or `Switch`
|
|
66
90
|
|
|
67
91
|
Same state kind, different meaning. A checkbox is a value you **submit** — "delete
|
|
@@ -120,3 +120,45 @@ export const AllStates: Story = {
|
|
|
120
120
|
export const Interactive: Story = {
|
|
121
121
|
args: { label: "Delete originals after import" },
|
|
122
122
|
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* `value` names which member of a group a box IS — not whether it is
|
|
126
|
+
* ticked. It matters as soon as there is more than one box: a group is
|
|
127
|
+
* read back with a single query over its container, and without a
|
|
128
|
+
* `value` every box in it answers `"on"`.
|
|
129
|
+
*
|
|
130
|
+
* ```ts
|
|
131
|
+
* const chosen = [...group.querySelectorAll("input")]
|
|
132
|
+
* .filter((input) => input.checked)
|
|
133
|
+
* .map((input) => input.value)
|
|
134
|
+
* ```
|
|
135
|
+
*
|
|
136
|
+
* A lone boolean needs none of this and should omit the prop.
|
|
137
|
+
*/
|
|
138
|
+
export const WithValues: Story = {
|
|
139
|
+
args: { label: "Anime" },
|
|
140
|
+
render: (controlProps) => (
|
|
141
|
+
<StorySection title="A library picker: each box carries the id the caller will read back, and the checked ones are the answer.">
|
|
142
|
+
<StoryGrid columns={2}>
|
|
143
|
+
{[
|
|
144
|
+
{ id: "11", isChosen: true, title: "Anime" },
|
|
145
|
+
{ id: "1", isChosen: false, title: "Movies" },
|
|
146
|
+
{ id: "15", isChosen: true, title: "Shorts" },
|
|
147
|
+
{ id: "2", isChosen: false, title: "Shows" },
|
|
148
|
+
].map((library) => (
|
|
149
|
+
<StoryCell
|
|
150
|
+
key={library.id}
|
|
151
|
+
label={`value="${library.id}"`}
|
|
152
|
+
>
|
|
153
|
+
<Checkbox
|
|
154
|
+
{...controlProps}
|
|
155
|
+
isChecked={library.isChosen}
|
|
156
|
+
label={library.title}
|
|
157
|
+
value={library.id}
|
|
158
|
+
/>
|
|
159
|
+
</StoryCell>
|
|
160
|
+
))}
|
|
161
|
+
</StoryGrid>
|
|
162
|
+
</StorySection>
|
|
163
|
+
),
|
|
164
|
+
}
|
|
@@ -7,7 +7,7 @@ import { mountStory } from "../mountStory.testHelpers.ts"
|
|
|
7
7
|
import { expectAgentDrivable } from "../testing/index.ts"
|
|
8
8
|
import * as stories from "./Checkbox.stories.tsx"
|
|
9
9
|
|
|
10
|
-
const { AllStates, Default, Interactive } =
|
|
10
|
+
const { AllStates, Default, Interactive, WithValues } =
|
|
11
11
|
composeStories(stories)
|
|
12
12
|
|
|
13
13
|
test("it is a checkbox a screen reader can name", async () => {
|
|
@@ -107,3 +107,54 @@ test("a read-only box is announced and cannot be toggled", async () => {
|
|
|
107
107
|
|
|
108
108
|
await expectNoAxeViolations(canvasElement)
|
|
109
109
|
})
|
|
110
|
+
|
|
111
|
+
test("value names the member, and a group reads back the ticked ones", async () => {
|
|
112
|
+
const { canvas, canvasElement } =
|
|
113
|
+
await mountStory(WithValues)
|
|
114
|
+
|
|
115
|
+
// The prop is the `<input>`'s `value` and nothing else — it must not
|
|
116
|
+
// touch the checked state, which `isChecked` still owns alone.
|
|
117
|
+
await expect(
|
|
118
|
+
expectAgentDrivable(canvas, {
|
|
119
|
+
name: "Anime",
|
|
120
|
+
role: "checkbox",
|
|
121
|
+
}),
|
|
122
|
+
).toHaveAttribute("value", "11")
|
|
123
|
+
|
|
124
|
+
await expect(
|
|
125
|
+
expectAgentDrivable(canvas, {
|
|
126
|
+
name: "Movies",
|
|
127
|
+
role: "checkbox",
|
|
128
|
+
}),
|
|
129
|
+
).not.toBeChecked()
|
|
130
|
+
|
|
131
|
+
// The read this prop exists for. Without a `value` every box here
|
|
132
|
+
// answers the UA default `"on"` and the result is four copies of one
|
|
133
|
+
// meaningless string, which is why a group could not use this
|
|
134
|
+
// component at all before.
|
|
135
|
+
const chosen = [
|
|
136
|
+
...canvasElement.querySelectorAll<HTMLInputElement>(
|
|
137
|
+
"input[type=checkbox]",
|
|
138
|
+
),
|
|
139
|
+
]
|
|
140
|
+
.filter((input) => input.checked)
|
|
141
|
+
.map((input) => input.value)
|
|
142
|
+
|
|
143
|
+
await expect(chosen).toEqual(["11", "15"])
|
|
144
|
+
|
|
145
|
+
await expectNoAxeViolations(canvasElement)
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
test("a box with no value stays out of the way", async () => {
|
|
149
|
+
const { canvas } = await mountStory(Default)
|
|
150
|
+
|
|
151
|
+
// `value` is optional and a lone boolean should omit it. React drops
|
|
152
|
+
// an `undefined` attribute entirely rather than writing an empty
|
|
153
|
+
// string, so the DOM looks exactly as it did before the prop existed.
|
|
154
|
+
await expect(
|
|
155
|
+
expectAgentDrivable(canvas, {
|
|
156
|
+
name: "Delete originals after import",
|
|
157
|
+
role: "checkbox",
|
|
158
|
+
}),
|
|
159
|
+
).not.toHaveAttribute("value")
|
|
160
|
+
})
|
|
@@ -38,6 +38,25 @@ export type CheckboxProps = {
|
|
|
38
38
|
label: ReactNode
|
|
39
39
|
onChange?: (isChecked: boolean) => void
|
|
40
40
|
size?: ControlSize
|
|
41
|
+
/**
|
|
42
|
+
* The `<input>`'s `value` attribute — which member of a group this
|
|
43
|
+
* box IS, as opposed to whether it is ticked.
|
|
44
|
+
*
|
|
45
|
+
* A lone boolean does not need this. A GROUP does: the browser
|
|
46
|
+
* submits `name=value` for each ticked box in a form, and, more
|
|
47
|
+
* relevantly here, a group is normally read back with one query —
|
|
48
|
+
* `[...root.querySelectorAll("input")].filter(i => i.checked)
|
|
49
|
+
* .map(i => i.value)`. Without a `value` every box in the group
|
|
50
|
+
* reports the string `"on"` and the read is useless, which is why
|
|
51
|
+
* queuepilot's library and ratings pickers were still hand-rolling
|
|
52
|
+
* a raw `<input type="checkbox">` rather than adopting this
|
|
53
|
+
* component.
|
|
54
|
+
*
|
|
55
|
+
* Passed straight through and otherwise inert. It is NOT the
|
|
56
|
+
* checked state and does not become one: `isChecked` still seeds
|
|
57
|
+
* the box, and `onChange` still reports a boolean.
|
|
58
|
+
*/
|
|
59
|
+
value?: string
|
|
41
60
|
}
|
|
42
61
|
|
|
43
62
|
const BOX_SIZE_CLASS: Record<ControlSize, string> = {
|
|
@@ -96,6 +115,7 @@ export const Checkbox = ({
|
|
|
96
115
|
label,
|
|
97
116
|
onChange,
|
|
98
117
|
size = "md",
|
|
118
|
+
value,
|
|
99
119
|
}: CheckboxProps): ReactNode => (
|
|
100
120
|
<label
|
|
101
121
|
className={toClassName(
|
|
@@ -176,6 +196,7 @@ export const Checkbox = ({
|
|
|
176
196
|
}
|
|
177
197
|
}}
|
|
178
198
|
type="checkbox"
|
|
199
|
+
value={value}
|
|
179
200
|
/>
|
|
180
201
|
|
|
181
202
|
<svg
|