@charcuterie/ui 2.4.0 → 2.5.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 +59 -0
- package/dist/Checkbox/Checkbox.d.ts.map +1 -0
- package/dist/Checkbox/Checkbox.js +61 -0
- package/dist/Checkbox/Checkbox.js.map +1 -0
- package/dist/Combobox/Combobox.d.ts.map +1 -1
- package/dist/Combobox/Combobox.js +1 -2
- package/dist/Combobox/Combobox.js.map +1 -1
- package/dist/Field/Field.d.ts.map +1 -1
- package/dist/Field/Field.js +12 -1
- package/dist/Field/Field.js.map +1 -1
- package/dist/Menu/Menu.d.ts +60 -2
- package/dist/Menu/Menu.d.ts.map +1 -1
- package/dist/Menu/Menu.js +58 -11
- package/dist/Menu/Menu.js.map +1 -1
- package/dist/RadioGroup/RadioGroup.d.ts +47 -0
- package/dist/RadioGroup/RadioGroup.d.ts.map +1 -0
- package/dist/RadioGroup/RadioGroup.js +114 -0
- package/dist/RadioGroup/RadioGroup.js.map +1 -0
- package/dist/RadioGroup/RadioGroupOption.d.ts +26 -0
- package/dist/RadioGroup/RadioGroupOption.d.ts.map +1 -0
- package/dist/RadioGroup/RadioGroupOption.js +52 -0
- package/dist/RadioGroup/RadioGroupOption.js.map +1 -0
- package/dist/Switch/Switch.d.ts +54 -0
- package/dist/Switch/Switch.d.ts.map +1 -0
- package/dist/Switch/Switch.js +94 -0
- package/dist/Switch/Switch.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/slotWiring.d.ts +9 -7
- package/dist/slotWiring.d.ts.map +1 -1
- package/dist/slotWiring.js +10 -70
- package/dist/slotWiring.js.map +1 -1
- package/package.json +3 -3
- package/src/Checkbox/Checkbox.mdx +58 -0
- package/src/Checkbox/Checkbox.stories.tsx +100 -0
- package/src/Checkbox/Checkbox.test.tsx +80 -0
- package/src/Checkbox/Checkbox.tsx +146 -0
- package/src/Combobox/Combobox.stories.tsx +64 -4
- package/src/Combobox/Combobox.tsx +13 -1
- package/src/Field/Field.stories.tsx +19 -0
- package/src/Field/Field.test.tsx +30 -2
- package/src/Field/Field.tsx +13 -1
- package/src/Menu/Menu.stories.tsx +80 -2
- package/src/Menu/Menu.test.tsx +102 -0
- package/src/Menu/Menu.tsx +156 -17
- package/src/RadioGroup/RadioGroup.mdx +58 -0
- package/src/RadioGroup/RadioGroup.stories.tsx +130 -0
- package/src/RadioGroup/RadioGroup.test.tsx +191 -0
- package/src/RadioGroup/RadioGroup.tsx +204 -0
- package/src/RadioGroup/RadioGroupOption.tsx +126 -0
- package/src/Switch/Switch.mdx +53 -0
- package/src/Switch/Switch.stories.tsx +94 -0
- package/src/Switch/Switch.test.tsx +86 -0
- package/src/Switch/Switch.tsx +162 -0
- package/src/index.ts +9 -0
- package/src/slotWiring.ts +15 -107
- package/src/sourceRules.test.ts +11 -1
- package/src/tailwindCandidates.test.ts +9 -7
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { composeStories } from "@storybook/react"
|
|
2
|
+
import { expect, userEvent, waitFor } from "storybook/test"
|
|
3
|
+
import { test } from "vitest"
|
|
4
|
+
|
|
5
|
+
import { expectNoAxeViolations } from "../expectNoAxeViolations.testHelpers.ts"
|
|
6
|
+
import { mountStory } from "../mountStory.testHelpers.ts"
|
|
7
|
+
import { expectAgentDrivable } from "../testing/index.ts"
|
|
8
|
+
import * as stories from "./Switch.stories.tsx"
|
|
9
|
+
|
|
10
|
+
const { AllStates, Default, Interactive } =
|
|
11
|
+
composeStories(stories)
|
|
12
|
+
|
|
13
|
+
test("it is a switch a screen reader can name and read", async () => {
|
|
14
|
+
const { canvas, canvasElement } =
|
|
15
|
+
await mountStory(Default)
|
|
16
|
+
|
|
17
|
+
const control = expectAgentDrivable(canvas, {
|
|
18
|
+
name: "Automatic imports",
|
|
19
|
+
role: "switch",
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
// `aria-checked`, not `aria-pressed`: a switch announces "off",
|
|
23
|
+
// where a toggle button would announce "not pressed". The role is
|
|
24
|
+
// the whole reason to reach for this over a `Checkbox`.
|
|
25
|
+
await expect(control).toHaveAttribute(
|
|
26
|
+
"aria-checked",
|
|
27
|
+
"false",
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
await expectNoAxeViolations(canvasElement)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
test("Space flips it, and flips it back", async () => {
|
|
34
|
+
const { canvas } = await mountStory(Interactive)
|
|
35
|
+
|
|
36
|
+
const control = expectAgentDrivable(canvas, {
|
|
37
|
+
name: "Automatic imports",
|
|
38
|
+
role: "switch",
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
control.focus()
|
|
42
|
+
|
|
43
|
+
await userEvent.keyboard(" ")
|
|
44
|
+
|
|
45
|
+
await waitFor(() => {
|
|
46
|
+
expect(control).toHaveAttribute("aria-checked", "true")
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
await userEvent.keyboard(" ")
|
|
50
|
+
|
|
51
|
+
await waitFor(() => {
|
|
52
|
+
expect(control).toHaveAttribute("aria-checked", "false")
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test("a disabled switch cannot be flipped", async () => {
|
|
57
|
+
const { canvas, canvasElement } =
|
|
58
|
+
await mountStory(AllStates)
|
|
59
|
+
|
|
60
|
+
const control = expectAgentDrivable(canvas, {
|
|
61
|
+
name: "Disabled",
|
|
62
|
+
role: "switch",
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
await expect(control).toBeDisabled()
|
|
66
|
+
|
|
67
|
+
await userEvent.click(control)
|
|
68
|
+
|
|
69
|
+
await expect(control).toHaveAttribute(
|
|
70
|
+
"aria-checked",
|
|
71
|
+
"false",
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
await expectNoAxeViolations(canvasElement)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
test("isChecked decides the first render", async () => {
|
|
78
|
+
const { canvas } = await mountStory(AllStates)
|
|
79
|
+
|
|
80
|
+
await expect(
|
|
81
|
+
expectAgentDrivable(canvas, {
|
|
82
|
+
name: "On",
|
|
83
|
+
role: "switch",
|
|
84
|
+
}),
|
|
85
|
+
).toHaveAttribute("aria-checked", "true")
|
|
86
|
+
})
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { useUniqueId } from "@charcuterie/logic"
|
|
2
|
+
import type { ControlSize } from "@charcuterie/tokens"
|
|
3
|
+
import type { ReactNode } from "react"
|
|
4
|
+
import { useState } from "react"
|
|
5
|
+
|
|
6
|
+
import { FOCUS_RING_CLASS } from "../intentStyles.ts"
|
|
7
|
+
import { toClassName } from "../toClassName.ts"
|
|
8
|
+
|
|
9
|
+
export type SwitchProps = {
|
|
10
|
+
className?: string
|
|
11
|
+
/**
|
|
12
|
+
* **Initial** only. The `Switch` owns it from then on — the same
|
|
13
|
+
* uncontrolled contract as `Checkbox` and `SegmentedControl`, so
|
|
14
|
+
* `onChange` reports the new state and there is no controlled
|
|
15
|
+
* `checked` prop to keep in step.
|
|
16
|
+
*/
|
|
17
|
+
isChecked?: boolean
|
|
18
|
+
isDisabled?: boolean
|
|
19
|
+
/** The visible text, and the switch's accessible name. */
|
|
20
|
+
label: ReactNode
|
|
21
|
+
onChange?: (isChecked: boolean) => void
|
|
22
|
+
size?: ControlSize
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Track and thumb are sized so `p-1` (4px) breathes evenly on every
|
|
26
|
+
// side and the thumb is centred by arithmetic, not by eye: inner
|
|
27
|
+
// height (track − 2·padding) equals the thumb, and the on-translate is
|
|
28
|
+
// (track width − 2·padding − thumb). Break one of those and the thumb
|
|
29
|
+
// clips an edge, which is what the first cut did.
|
|
30
|
+
const TRACK_SIZE_CLASS: Record<ControlSize, string> = {
|
|
31
|
+
sm: "h-5 w-9",
|
|
32
|
+
md: "h-6 w-11",
|
|
33
|
+
lg: "h-7 w-14",
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const THUMB_SIZE_CLASS: Record<ControlSize, string> = {
|
|
37
|
+
sm: "size-3",
|
|
38
|
+
md: "size-4",
|
|
39
|
+
lg: "size-5",
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const THUMB_ON_TRANSLATE_CLASS: Record<
|
|
43
|
+
ControlSize,
|
|
44
|
+
string
|
|
45
|
+
> = {
|
|
46
|
+
sm: "translate-x-4",
|
|
47
|
+
md: "translate-x-5",
|
|
48
|
+
lg: "translate-x-7",
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const TEXT_SIZE_CLASS: Record<ControlSize, string> = {
|
|
52
|
+
sm: "text-sm",
|
|
53
|
+
md: "text-md",
|
|
54
|
+
lg: "text-lg",
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* A boolean that takes effect the moment it is flipped — a setting,
|
|
59
|
+
* not a form value.
|
|
60
|
+
*
|
|
61
|
+
* A `Checkbox` and a `Switch` are the same state kind and a
|
|
62
|
+
* different affordance, and the split is meaning rather than looks:
|
|
63
|
+
* a checkbox is a value you submit ("I agree"), a switch is a state
|
|
64
|
+
* you toggle and live with immediately ("dark mode on"). ARIA gives
|
|
65
|
+
* the second its own role, and a screen reader announces "on/off"
|
|
66
|
+
* for a `switch` where it announces "checked/unchecked" for a
|
|
67
|
+
* checkbox.
|
|
68
|
+
*
|
|
69
|
+
* ### A `button role="switch"`, not an `input`
|
|
70
|
+
*
|
|
71
|
+
* The sliding track and thumb are the whole point of the affordance,
|
|
72
|
+
* and a `<button>` styles them without fighting a native control's
|
|
73
|
+
* own rendering. It carries `aria-checked`, `aria-labelledby`, and
|
|
74
|
+
* nothing else the browser would try to own. The value lives in
|
|
75
|
+
* React state seeded once from `isChecked` — the switch is the
|
|
76
|
+
* store, the same as the DOM is for `Checkbox`.
|
|
77
|
+
*
|
|
78
|
+
* ### The thumb is one colour, the track carries the state
|
|
79
|
+
*
|
|
80
|
+
* `bg-surface-raised` in both positions — the knob is the same object
|
|
81
|
+
* sliding, not two differently-painted dots, which is what the first
|
|
82
|
+
* cut looked like when off was `content-secondary` and on was
|
|
83
|
+
* `on-solid`. A themed system has no single colour that is "light" in
|
|
84
|
+
* both schemes, so consistency lives *within* a scheme: `surface-raised`
|
|
85
|
+
* is a white knob in a light one and a raised charcoal knob in a dark
|
|
86
|
+
* one, off and on identical either way. The **track** shows the state —
|
|
87
|
+
* `bg-intent-accent-solid` on, `bg-surface-sunken` with a `ring-2`
|
|
88
|
+
* outline off — which is where a switch's state belongs, and the ring
|
|
89
|
+
* gives the off track the defined edge a bare well would miss on a pale
|
|
90
|
+
* theme.
|
|
91
|
+
*/
|
|
92
|
+
export const Switch = ({
|
|
93
|
+
className,
|
|
94
|
+
isChecked = false,
|
|
95
|
+
isDisabled = false,
|
|
96
|
+
label,
|
|
97
|
+
onChange,
|
|
98
|
+
size = "md",
|
|
99
|
+
}: SwitchProps): ReactNode => {
|
|
100
|
+
const [isOn, setIsOn] = useState(isChecked)
|
|
101
|
+
|
|
102
|
+
const labelId = useUniqueId()
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<span
|
|
106
|
+
className={toClassName(
|
|
107
|
+
"inline-flex items-center gap-2 text-content-secondary select-none",
|
|
108
|
+
TEXT_SIZE_CLASS[size],
|
|
109
|
+
isDisabled && "text-content-disabled",
|
|
110
|
+
className,
|
|
111
|
+
)}
|
|
112
|
+
>
|
|
113
|
+
<button
|
|
114
|
+
aria-checked={isOn}
|
|
115
|
+
aria-labelledby={labelId}
|
|
116
|
+
className={toClassName(
|
|
117
|
+
// `p-1` is the breathing room; `ring-inset` draws the off
|
|
118
|
+
// outline without changing the box size, so the centring
|
|
119
|
+
// arithmetic above stays exact.
|
|
120
|
+
"inline-flex shrink-0 cursor-pointer items-center rounded-full p-1 transition-colors duration-(--duration-fast) ease-standard",
|
|
121
|
+
// One state class, chosen by ternary — two `bg-`/`ring-`
|
|
122
|
+
// utilities of the same specificity do not resolve by
|
|
123
|
+
// class-list order, so disabled has to win as the only one
|
|
124
|
+
// written.
|
|
125
|
+
isDisabled
|
|
126
|
+
? "cursor-not-allowed bg-surface-sunken ring-2 ring-border-subtle ring-inset"
|
|
127
|
+
: isOn
|
|
128
|
+
? "bg-intent-accent-solid"
|
|
129
|
+
: "bg-surface-sunken ring-2 ring-border-strong ring-inset",
|
|
130
|
+
TRACK_SIZE_CLASS[size],
|
|
131
|
+
FOCUS_RING_CLASS,
|
|
132
|
+
)}
|
|
133
|
+
disabled={isDisabled}
|
|
134
|
+
onClick={() => {
|
|
135
|
+
const isNextOn = !isOn
|
|
136
|
+
|
|
137
|
+
setIsOn(isNextOn)
|
|
138
|
+
|
|
139
|
+
onChange?.(isNextOn)
|
|
140
|
+
}}
|
|
141
|
+
role="switch"
|
|
142
|
+
type="button"
|
|
143
|
+
>
|
|
144
|
+
<span
|
|
145
|
+
className={toClassName(
|
|
146
|
+
"rounded-full transition-transform duration-(--duration-fast) ease-standard",
|
|
147
|
+
// One knob, same colour off and on; disabled mutes it.
|
|
148
|
+
isDisabled
|
|
149
|
+
? "bg-content-disabled"
|
|
150
|
+
: "bg-surface-raised",
|
|
151
|
+
isOn
|
|
152
|
+
? THUMB_ON_TRANSLATE_CLASS[size]
|
|
153
|
+
: "translate-x-0",
|
|
154
|
+
THUMB_SIZE_CLASS[size],
|
|
155
|
+
)}
|
|
156
|
+
/>
|
|
157
|
+
</button>
|
|
158
|
+
|
|
159
|
+
<span id={labelId}>{label}</span>
|
|
160
|
+
</span>
|
|
161
|
+
)
|
|
162
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -52,6 +52,8 @@ export type {
|
|
|
52
52
|
CardSurface,
|
|
53
53
|
} from "./Card/Card.tsx"
|
|
54
54
|
export { Card } from "./Card/Card.tsx"
|
|
55
|
+
export type { CheckboxProps } from "./Checkbox/Checkbox.tsx"
|
|
56
|
+
export { Checkbox } from "./Checkbox/Checkbox.tsx"
|
|
55
57
|
export type { ColorSchemeSwitcherProps } from "./ColorSchemeSwitcher/ColorSchemeSwitcher.tsx"
|
|
56
58
|
export { ColorSchemeSwitcher } from "./ColorSchemeSwitcher/ColorSchemeSwitcher.tsx"
|
|
57
59
|
export type {
|
|
@@ -141,6 +143,11 @@ export {
|
|
|
141
143
|
toProgressPercent,
|
|
142
144
|
toProgressValue,
|
|
143
145
|
} from "./ProgressBar/progressValue.ts"
|
|
146
|
+
export type {
|
|
147
|
+
RadioGroupProps,
|
|
148
|
+
RadioItem,
|
|
149
|
+
} from "./RadioGroup/RadioGroup.tsx"
|
|
150
|
+
export { RadioGroup } from "./RadioGroup/RadioGroup.tsx"
|
|
144
151
|
export type {
|
|
145
152
|
SegmentedControlProps,
|
|
146
153
|
SegmentedItem,
|
|
@@ -171,6 +178,8 @@ export type {
|
|
|
171
178
|
SwatchSize,
|
|
172
179
|
} from "./Swatch/Swatch.tsx"
|
|
173
180
|
export { Swatch } from "./Swatch/Swatch.tsx"
|
|
181
|
+
export type { SwitchProps } from "./Switch/Switch.tsx"
|
|
182
|
+
export { Switch } from "./Switch/Switch.tsx"
|
|
174
183
|
export type { SlotProps } from "./slotProps.ts"
|
|
175
184
|
export { mergeSlotProps } from "./slotProps.ts"
|
|
176
185
|
export {
|
package/src/slotWiring.ts
CHANGED
|
@@ -15,116 +15,24 @@
|
|
|
15
15
|
* a CSS bug and is not one.
|
|
16
16
|
* - **`on*`** is a listener, and listeners are plural.
|
|
17
17
|
*
|
|
18
|
-
* ###
|
|
18
|
+
* ### Shared with `@charcuterie/logic`
|
|
19
19
|
*
|
|
20
|
-
* `useClonedChild`'s `mergeClonedProps` applies the
|
|
21
|
-
* level down — between a slot and the element a caller wrote —
|
|
22
|
-
* the
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
20
|
+
* `useClonedChild`'s `mergeClonedProps` applies the identical rule
|
|
21
|
+
* one level down — between a slot and the element a caller wrote —
|
|
22
|
+
* so the `ref`/`on*` primitives (`mergeRefs`, `chainHandlers`, and
|
|
23
|
+
* the two guards) are imported from `@charcuterie/logic` rather than
|
|
24
|
+
* copied here, where they used to be a byte-for-byte duplicate. Only
|
|
25
|
+
* `mergeSlotWiring` stays local: it merges the *other* direction
|
|
26
|
+
* (own over received) and returns just the wiring keys, which is a
|
|
27
|
+
* different function from `mergeClonedProps`.
|
|
26
28
|
*/
|
|
27
29
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const isMergeableRef = (
|
|
36
|
-
value: unknown,
|
|
37
|
-
): value is MergeableRef =>
|
|
38
|
-
typeof value === "function" ||
|
|
39
|
-
(typeof value === "object" &&
|
|
40
|
-
value !== null &&
|
|
41
|
-
"current" in value)
|
|
42
|
-
|
|
43
|
-
const setRef = (ref: MergeableRef, node: unknown) => {
|
|
44
|
-
if (typeof ref === "function") {
|
|
45
|
-
return ref(node)
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
ref.current = node
|
|
49
|
-
|
|
50
|
-
return undefined
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Every pair of refs ever merged, so the same two always come back
|
|
55
|
-
* as the **same function** — which is load-bearing rather than
|
|
56
|
-
* frugal.
|
|
57
|
-
*
|
|
58
|
-
* React re-runs a callback ref whenever its identity changes between
|
|
59
|
-
* renders, detaching with the old one and attaching with the new.
|
|
60
|
-
* Both refs here are floating-ui `setReference`s, and calling one
|
|
61
|
-
* sets state; a merged ref rebuilt every render would call both
|
|
62
|
-
* twice per render, each call scheduling the render that rebuilds
|
|
63
|
-
* it. `mergeSlotProps` is a plain function called during render, so
|
|
64
|
-
* it cannot reach for `useMemo` — the memo has to be in the identity
|
|
65
|
-
* of the inputs, which is what a `WeakMap` of `WeakMap`s is.
|
|
66
|
-
*/
|
|
67
|
-
const mergedRefs = new WeakMap<
|
|
68
|
-
MergeableRef,
|
|
69
|
-
WeakMap<MergeableRef, (node: unknown) => () => void>
|
|
70
|
-
>()
|
|
71
|
-
|
|
72
|
-
const mergeRefs = (
|
|
73
|
-
first: MergeableRef,
|
|
74
|
-
second: MergeableRef,
|
|
75
|
-
) => {
|
|
76
|
-
const bySecond =
|
|
77
|
-
mergedRefs.get(first) ??
|
|
78
|
-
new WeakMap<
|
|
79
|
-
MergeableRef,
|
|
80
|
-
(node: unknown) => () => void
|
|
81
|
-
>()
|
|
82
|
-
|
|
83
|
-
mergedRefs.set(first, bySecond)
|
|
84
|
-
|
|
85
|
-
const cached = bySecond.get(second)
|
|
86
|
-
|
|
87
|
-
if (cached) {
|
|
88
|
-
return cached
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const merged = (node: unknown) => {
|
|
92
|
-
const cleanups = [
|
|
93
|
-
setRef(first, node),
|
|
94
|
-
setRef(second, node),
|
|
95
|
-
]
|
|
96
|
-
|
|
97
|
-
// Per ref: one that returned a cleanup has opted out of being
|
|
98
|
-
// called with `null`, and one that did not still expects it.
|
|
99
|
-
return () => {
|
|
100
|
-
for (const [index, cleanup] of cleanups.entries()) {
|
|
101
|
-
if (typeof cleanup === "function") {
|
|
102
|
-
cleanup()
|
|
103
|
-
} else {
|
|
104
|
-
setRef(index === 0 ? first : second, null)
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
bySecond.set(second, merged)
|
|
111
|
-
|
|
112
|
-
return merged
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const chainHandlers =
|
|
116
|
-
(
|
|
117
|
-
first: (...args: unknown[]) => unknown,
|
|
118
|
-
second: (...args: unknown[]) => unknown,
|
|
119
|
-
) =>
|
|
120
|
-
(...args: unknown[]) => {
|
|
121
|
-
// Outer first, the same order `aria-describedby` is joined in:
|
|
122
|
-
// the merge order is structural — whoever wrapped whom — and the
|
|
123
|
-
// author of the nesting is the one who chose it.
|
|
124
|
-
first(...args)
|
|
125
|
-
|
|
126
|
-
second(...args)
|
|
127
|
-
}
|
|
30
|
+
import {
|
|
31
|
+
chainHandlers,
|
|
32
|
+
isEventHandlerName,
|
|
33
|
+
isMergeableRef,
|
|
34
|
+
mergeRefs,
|
|
35
|
+
} from "@charcuterie/logic"
|
|
128
36
|
|
|
129
37
|
/**
|
|
130
38
|
* The keys of `ownProps` that must not simply replace the ones
|
package/src/sourceRules.test.ts
CHANGED
|
@@ -303,7 +303,17 @@ test("the barrel is the only place components are re-exported", async () => {
|
|
|
303
303
|
// `Swatch` — a colour as content with a required name, the one
|
|
304
304
|
// colour the system does not own — comes out of `portly-controllers`,
|
|
305
305
|
// the fleet's newest consumer: +1 -> 32.
|
|
306
|
-
|
|
306
|
+
//
|
|
307
|
+
// The boolean-input family fills the gap `mux-magic`'s `BooleanField`
|
|
308
|
+
// hand-rolled around: `Checkbox` and `Switch` (the same state kind,
|
|
309
|
+
// a submitted value versus a setting that takes effect on flip) and
|
|
310
|
+
// `RadioGroup` (the stacked sibling of `SegmentedControl`, same
|
|
311
|
+
// `SinglePicker` + `RovingFocus` composition): +3 -> 35.
|
|
312
|
+
// `RadioGroupOption` is `RadioGroup`'s member — its own file only
|
|
313
|
+
// because both registrations are effects — and stays out of this
|
|
314
|
+
// count and the barrel by the `<Name>/<Name>.tsx` rule, exactly as
|
|
315
|
+
// `SegmentedOption` does.
|
|
316
|
+
expect(componentNames.length).toBe(35)
|
|
307
317
|
|
|
308
318
|
for (const name of componentNames) {
|
|
309
319
|
expect(barrel).toContain(`export { ${name} }`)
|
|
@@ -256,16 +256,18 @@ test("this package has class names to check at all", () => {
|
|
|
256
256
|
})
|
|
257
257
|
|
|
258
258
|
/**
|
|
259
|
-
* The
|
|
259
|
+
* The class names in this package that are deliberately not
|
|
260
260
|
* utilities.
|
|
261
261
|
*
|
|
262
|
-
* `group`
|
|
263
|
-
*
|
|
264
|
-
*
|
|
265
|
-
*
|
|
266
|
-
*
|
|
262
|
+
* `group` and `peer` are Tailwind's own markers — they generate no
|
|
263
|
+
* CSS by design, they are what `group-hover:` and `peer-checked:`
|
|
264
|
+
* read (`Checkbox` overlays its tick with `peer-checked:visible`).
|
|
265
|
+
* The `charcuterie-*` four are the looping affordances, which cannot
|
|
266
|
+
* be utilities because Tailwind's own `animate-spin` hardcodes 1s
|
|
267
|
+
* past `prefers-reduced-motion`; they live in `styles.css` and are
|
|
268
|
+
* checked against it below.
|
|
267
269
|
*/
|
|
268
|
-
const TAILWIND_MARKER_CLASSES = ["group"]
|
|
270
|
+
const TAILWIND_MARKER_CLASSES = ["group", "peer"]
|
|
269
271
|
|
|
270
272
|
test("every `charcuterie-` class a component uses is defined in styles.css", async () => {
|
|
271
273
|
// The other half of the "no silently-unstyled component" gate. A
|