@arun-dev/headless 3.0.0 → 4.0.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/README.md +13 -9
- package/dist/button/index.cjs +3 -13
- package/dist/button/index.d.cts +11 -22
- package/dist/button/index.d.ts +11 -22
- package/dist/button/index.js +3 -13
- package/dist/switch/index.cjs +46 -16
- package/dist/switch/index.d.cts +11 -10
- package/dist/switch/index.d.ts +11 -10
- package/dist/switch/index.js +47 -17
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -28,19 +28,19 @@ import { Switch } from '@arun-dev/headless/switch';
|
|
|
28
28
|
|
|
29
29
|
| Component | Parts | Props |
|
|
30
30
|
| --------- | ----------------------- | --------------------------------------------------------------------------- |
|
|
31
|
-
| `Button` | — | `disabled`, `
|
|
31
|
+
| `Button` | — | `disabled`, `type` |
|
|
32
32
|
| `Switch` | `Switch.Root`, `.Thumb` | `checked`, `defaultChecked`, `onCheckedChange`, `disabled`, `name`, `value` |
|
|
33
33
|
|
|
34
|
-
`Button` renders a `<button
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
disabled
|
|
34
|
+
`Button` renders a `<button>` and defaults `type="button"` so it never submits a form by accident.
|
|
35
|
+
It does not navigate — a link is a separate `Link` component. A `render` component cannot be
|
|
36
|
+
inspected, so a disabled Button gives it `aria-disabled` and `data-disabled` rather than
|
|
37
|
+
`disabled` — actually disabling itself is up to the component.
|
|
38
38
|
|
|
39
39
|
```tsx
|
|
40
40
|
import { Button } from '@arun-dev/headless/button';
|
|
41
41
|
|
|
42
|
-
<Button
|
|
43
|
-
|
|
42
|
+
<Button type="submit" disabled={saving}>
|
|
43
|
+
Save
|
|
44
44
|
</Button>;
|
|
45
45
|
```
|
|
46
46
|
|
|
@@ -48,6 +48,10 @@ import { Button } from '@arun-dev/headless/button';
|
|
|
48
48
|
from the platform. It carries `role="switch"` and `aria-checked`, but **no accessible name** —
|
|
49
49
|
wrap it in a `<label>` or pass `aria-label`. A headless component should not guess at your copy.
|
|
50
50
|
|
|
51
|
+
In a form it behaves like a native checkbox: with a `name` it submits its `value` only when checked
|
|
52
|
+
and enabled, and `form.reset()` returns it to the state it mounted with, reported through
|
|
53
|
+
`onCheckedChange`.
|
|
54
|
+
|
|
51
55
|
## State reaches CSS through `data-*`
|
|
52
56
|
|
|
53
57
|
A headless component owns no class names, so state is projected onto the DOM instead. That
|
|
@@ -94,8 +98,8 @@ come last and win. An element that declares no children inherits yours, one that
|
|
|
94
98
|
keeps them.
|
|
95
99
|
|
|
96
100
|
```tsx
|
|
97
|
-
<Button render={<
|
|
98
|
-
<Button render={<
|
|
101
|
+
<Button render={<span />}>Docs</Button> // renders "Docs"
|
|
102
|
+
<Button render={<span>Read the docs</span>}>Docs</Button> // renders "Read the docs"
|
|
99
103
|
```
|
|
100
104
|
|
|
101
105
|
Event handlers are chained rather than replaced, and **your handler runs before the component's**,
|
package/dist/button/index.cjs
CHANGED
|
@@ -116,27 +116,17 @@ function useRender({
|
|
|
116
116
|
// src/button/Button.tsx
|
|
117
117
|
function Button({
|
|
118
118
|
disabled = false,
|
|
119
|
-
href,
|
|
120
119
|
type = "button",
|
|
121
120
|
className,
|
|
122
121
|
children,
|
|
123
122
|
render,
|
|
124
123
|
...rest
|
|
125
124
|
}) {
|
|
126
|
-
const
|
|
127
|
-
const
|
|
128
|
-
if (process.env.NODE_ENV !== "production" && disabled && render !== void 0) {
|
|
129
|
-
const renderProps = render.props;
|
|
130
|
-
if (renderProps?.href !== void 0) {
|
|
131
|
-
console.error(
|
|
132
|
-
"Button: `disabled` cannot remove the `href` on a `render` element \u2014 its own props outrank the component. Pass the URL as `href` on Button instead."
|
|
133
|
-
);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
const elementProps = rendersButton ? { type, disabled: disabled || void 0 } : { href: linksOut ? href : void 0, "aria-disabled": disabled || void 0 };
|
|
125
|
+
const rendersButton = render === void 0 || render.type === "button";
|
|
126
|
+
const elementProps = rendersButton ? { type, disabled: disabled || void 0 } : { "aria-disabled": disabled || void 0 };
|
|
137
127
|
return useRender({
|
|
138
128
|
render,
|
|
139
|
-
defaultTagName:
|
|
129
|
+
defaultTagName: "button",
|
|
140
130
|
props: {
|
|
141
131
|
...elementProps,
|
|
142
132
|
"data-disabled": disabled ? "" : void 0,
|
package/dist/button/index.d.cts
CHANGED
|
@@ -8,45 +8,34 @@ import { ReactElement, Ref, ComponentPropsWithRef } from 'react';
|
|
|
8
8
|
*/
|
|
9
9
|
type ButtonOwnProps = {
|
|
10
10
|
/**
|
|
11
|
-
* Prevents activation. On a `<button>` the platform does it
|
|
12
|
-
* `href` renders, dropping the `href` does it — the DOM leaves an anchor without one
|
|
13
|
-
* unfocusable, unactivatable and no longer a link.
|
|
14
|
-
*/
|
|
15
|
-
disabled?: boolean;
|
|
16
|
-
/**
|
|
17
|
-
* Renders an `<a href>` instead of a `<button>`.
|
|
11
|
+
* Prevents activation. On a `<button>` the platform does it.
|
|
18
12
|
*
|
|
19
|
-
* A
|
|
20
|
-
*
|
|
21
|
-
* something to write on a `render` element because `disabled` has to be able to take
|
|
22
|
-
* it away, and a `render` element's own props outrank everything.
|
|
13
|
+
* A `render` component cannot be inspected, so it gets `aria-disabled` and
|
|
14
|
+
* `data-disabled` rather than `disabled`: actually disabling itself is up to it.
|
|
23
15
|
*/
|
|
24
|
-
|
|
16
|
+
disabled?: boolean;
|
|
25
17
|
/**
|
|
26
18
|
* Element or component to render instead of the default. Props, className, event
|
|
27
19
|
* handlers and ref are merged onto it.
|
|
28
20
|
*
|
|
29
|
-
* @example <Button render={<
|
|
21
|
+
* @example <Button render={<motion.button />}>Save</Button>
|
|
30
22
|
*/
|
|
31
23
|
render?: ReactElement;
|
|
32
24
|
/** Ref to the rendered element. Merged with any ref on the `render` element. */
|
|
33
25
|
ref?: Ref<HTMLElement>;
|
|
34
26
|
};
|
|
35
|
-
type ButtonProps = ButtonOwnProps & Omit<ComponentPropsWithRef<'button'>, keyof ButtonOwnProps
|
|
27
|
+
type ButtonProps = ButtonOwnProps & Omit<ComponentPropsWithRef<'button'>, keyof ButtonOwnProps>;
|
|
36
28
|
/**
|
|
37
29
|
* A button — behaviour only, no styling.
|
|
38
30
|
*
|
|
39
|
-
* Renders a `<button>`,
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* and expresses `disabled` the way that element already understands.
|
|
31
|
+
* Renders a `<button>`, which is natively focusable and keyboard-activatable, so
|
|
32
|
+
* nothing here is synthesised: the component defaults `type` so it does not submit by
|
|
33
|
+
* accident, and expresses `disabled` the way the element already understands.
|
|
43
34
|
*
|
|
44
|
-
*
|
|
45
|
-
* platform then removes it from the tab order and suppresses activation, with no
|
|
46
|
-
* handler stripping or `tabindex` bookkeeping of our own.
|
|
35
|
+
* Navigation is not a button's job — a link belongs to a separate `Link` component.
|
|
47
36
|
*
|
|
48
37
|
* Emits `data-disabled` so one selector styles a disabled control either way.
|
|
49
38
|
*/
|
|
50
|
-
declare function Button({ disabled,
|
|
39
|
+
declare function Button({ disabled, type, className, children, render, ...rest }: ButtonProps): ReactElement<unknown, string | react.JSXElementConstructor<any>>;
|
|
51
40
|
|
|
52
41
|
export { Button, type ButtonProps };
|
package/dist/button/index.d.ts
CHANGED
|
@@ -8,45 +8,34 @@ import { ReactElement, Ref, ComponentPropsWithRef } from 'react';
|
|
|
8
8
|
*/
|
|
9
9
|
type ButtonOwnProps = {
|
|
10
10
|
/**
|
|
11
|
-
* Prevents activation. On a `<button>` the platform does it
|
|
12
|
-
* `href` renders, dropping the `href` does it — the DOM leaves an anchor without one
|
|
13
|
-
* unfocusable, unactivatable and no longer a link.
|
|
14
|
-
*/
|
|
15
|
-
disabled?: boolean;
|
|
16
|
-
/**
|
|
17
|
-
* Renders an `<a href>` instead of a `<button>`.
|
|
11
|
+
* Prevents activation. On a `<button>` the platform does it.
|
|
18
12
|
*
|
|
19
|
-
* A
|
|
20
|
-
*
|
|
21
|
-
* something to write on a `render` element because `disabled` has to be able to take
|
|
22
|
-
* it away, and a `render` element's own props outrank everything.
|
|
13
|
+
* A `render` component cannot be inspected, so it gets `aria-disabled` and
|
|
14
|
+
* `data-disabled` rather than `disabled`: actually disabling itself is up to it.
|
|
23
15
|
*/
|
|
24
|
-
|
|
16
|
+
disabled?: boolean;
|
|
25
17
|
/**
|
|
26
18
|
* Element or component to render instead of the default. Props, className, event
|
|
27
19
|
* handlers and ref are merged onto it.
|
|
28
20
|
*
|
|
29
|
-
* @example <Button render={<
|
|
21
|
+
* @example <Button render={<motion.button />}>Save</Button>
|
|
30
22
|
*/
|
|
31
23
|
render?: ReactElement;
|
|
32
24
|
/** Ref to the rendered element. Merged with any ref on the `render` element. */
|
|
33
25
|
ref?: Ref<HTMLElement>;
|
|
34
26
|
};
|
|
35
|
-
type ButtonProps = ButtonOwnProps & Omit<ComponentPropsWithRef<'button'>, keyof ButtonOwnProps
|
|
27
|
+
type ButtonProps = ButtonOwnProps & Omit<ComponentPropsWithRef<'button'>, keyof ButtonOwnProps>;
|
|
36
28
|
/**
|
|
37
29
|
* A button — behaviour only, no styling.
|
|
38
30
|
*
|
|
39
|
-
* Renders a `<button>`,
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* and expresses `disabled` the way that element already understands.
|
|
31
|
+
* Renders a `<button>`, which is natively focusable and keyboard-activatable, so
|
|
32
|
+
* nothing here is synthesised: the component defaults `type` so it does not submit by
|
|
33
|
+
* accident, and expresses `disabled` the way the element already understands.
|
|
43
34
|
*
|
|
44
|
-
*
|
|
45
|
-
* platform then removes it from the tab order and suppresses activation, with no
|
|
46
|
-
* handler stripping or `tabindex` bookkeeping of our own.
|
|
35
|
+
* Navigation is not a button's job — a link belongs to a separate `Link` component.
|
|
47
36
|
*
|
|
48
37
|
* Emits `data-disabled` so one selector styles a disabled control either way.
|
|
49
38
|
*/
|
|
50
|
-
declare function Button({ disabled,
|
|
39
|
+
declare function Button({ disabled, type, className, children, render, ...rest }: ButtonProps): ReactElement<unknown, string | react.JSXElementConstructor<any>>;
|
|
51
40
|
|
|
52
41
|
export { Button, type ButtonProps };
|
package/dist/button/index.js
CHANGED
|
@@ -5,27 +5,17 @@ import {
|
|
|
5
5
|
// src/button/Button.tsx
|
|
6
6
|
function Button({
|
|
7
7
|
disabled = false,
|
|
8
|
-
href,
|
|
9
8
|
type = "button",
|
|
10
9
|
className,
|
|
11
10
|
children,
|
|
12
11
|
render,
|
|
13
12
|
...rest
|
|
14
13
|
}) {
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
if (process.env.NODE_ENV !== "production" && disabled && render !== void 0) {
|
|
18
|
-
const renderProps = render.props;
|
|
19
|
-
if (renderProps?.href !== void 0) {
|
|
20
|
-
console.error(
|
|
21
|
-
"Button: `disabled` cannot remove the `href` on a `render` element \u2014 its own props outrank the component. Pass the URL as `href` on Button instead."
|
|
22
|
-
);
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
const elementProps = rendersButton ? { type, disabled: disabled || void 0 } : { href: linksOut ? href : void 0, "aria-disabled": disabled || void 0 };
|
|
14
|
+
const rendersButton = render === void 0 || render.type === "button";
|
|
15
|
+
const elementProps = rendersButton ? { type, disabled: disabled || void 0 } : { "aria-disabled": disabled || void 0 };
|
|
26
16
|
return useRender({
|
|
27
17
|
render,
|
|
28
|
-
defaultTagName:
|
|
18
|
+
defaultTagName: "button",
|
|
29
19
|
props: {
|
|
30
20
|
...elementProps,
|
|
31
21
|
"data-disabled": disabled ? "" : void 0,
|
package/dist/switch/index.cjs
CHANGED
|
@@ -196,8 +196,17 @@ function SwitchRoot({
|
|
|
196
196
|
name: "Switch.Root",
|
|
197
197
|
state: "checked"
|
|
198
198
|
});
|
|
199
|
+
const commitChecked = (0, import_react4.useCallback)(
|
|
200
|
+
(next) => {
|
|
201
|
+
setChecked(next);
|
|
202
|
+
onCheckedChange?.(next);
|
|
203
|
+
},
|
|
204
|
+
[setChecked, onCheckedChange]
|
|
205
|
+
);
|
|
199
206
|
const state = (0, import_react4.useMemo)(() => ({ checked, disabled }), [checked, disabled]);
|
|
200
|
-
const elementRef =
|
|
207
|
+
const elementRef = (0, import_react4.useRef)(null);
|
|
208
|
+
const inputRef = (0, import_react4.useRef)(null);
|
|
209
|
+
useFormReset({ elementRef, inputRef, checked, commitChecked });
|
|
201
210
|
const element = useRender({
|
|
202
211
|
render,
|
|
203
212
|
defaultTagName: "button",
|
|
@@ -206,36 +215,57 @@ function SwitchRoot({
|
|
|
206
215
|
type: "button",
|
|
207
216
|
role: "switch",
|
|
208
217
|
"aria-checked": checked,
|
|
209
|
-
// The platform suppresses activation
|
|
218
|
+
// The platform suppresses activation and focus on a native button.
|
|
210
219
|
disabled: disabled || void 0,
|
|
211
220
|
...switchDataAttributes(state),
|
|
212
221
|
className,
|
|
213
222
|
children,
|
|
214
223
|
ref: elementRef,
|
|
215
224
|
onClick() {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
onCheckedChange?.(next);
|
|
225
|
+
if (disabled) return;
|
|
226
|
+
commitChecked(!checked);
|
|
219
227
|
}
|
|
220
228
|
},
|
|
221
229
|
consumerProps: rest
|
|
222
230
|
});
|
|
223
231
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(SwitchRootContext.Provider, { value: state, children: [
|
|
224
232
|
element,
|
|
225
|
-
name !== void 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
233
|
+
name !== void 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
234
|
+
"input",
|
|
235
|
+
{
|
|
236
|
+
ref: inputRef,
|
|
237
|
+
type: "checkbox",
|
|
238
|
+
hidden: true,
|
|
239
|
+
readOnly: true,
|
|
240
|
+
name,
|
|
241
|
+
value,
|
|
242
|
+
checked,
|
|
243
|
+
disabled
|
|
244
|
+
}
|
|
245
|
+
) : null
|
|
226
246
|
] });
|
|
227
247
|
}
|
|
228
|
-
function
|
|
229
|
-
|
|
248
|
+
function useFormReset({
|
|
249
|
+
elementRef,
|
|
250
|
+
inputRef,
|
|
251
|
+
checked,
|
|
252
|
+
commitChecked
|
|
253
|
+
}) {
|
|
254
|
+
const { current: initialChecked } = (0, import_react4.useRef)(checked);
|
|
230
255
|
(0, import_react4.useEffect)(() => {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
256
|
+
const form = elementRef.current?.form;
|
|
257
|
+
if (!form) return;
|
|
258
|
+
function onReset(event) {
|
|
259
|
+
queueMicrotask(() => {
|
|
260
|
+
if (event.defaultPrevented) return;
|
|
261
|
+
if (inputRef.current) inputRef.current.checked = checked;
|
|
262
|
+
if (checked === initialChecked) return;
|
|
263
|
+
commitChecked(initialChecked);
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
form.addEventListener("reset", onReset);
|
|
267
|
+
return () => form.removeEventListener("reset", onReset);
|
|
268
|
+
}, [elementRef, inputRef, checked, commitChecked, initialChecked]);
|
|
239
269
|
}
|
|
240
270
|
|
|
241
271
|
// src/switch/SwitchThumb.tsx
|
package/dist/switch/index.d.cts
CHANGED
|
@@ -7,8 +7,9 @@ import { ReactElement, Ref, ComponentPropsWithRef } from 'react';
|
|
|
7
7
|
* and checked without being declared here.
|
|
8
8
|
*
|
|
9
9
|
* `id` in particular is how a switch gets an accessible name, paired with a
|
|
10
|
-
* `<label htmlFor
|
|
11
|
-
*
|
|
10
|
+
* `<label htmlFor>`. A wrapping `<label>` names the rendered `<button>` too, but
|
|
11
|
+
* `jsx-a11y/label-has-associated-control` rejects a button as a nested control, so the
|
|
12
|
+
* explicit pairing is the one that passes lint.
|
|
12
13
|
*/
|
|
13
14
|
type SwitchRootOwnProps = {
|
|
14
15
|
/**
|
|
@@ -25,7 +26,7 @@ type SwitchRootOwnProps = {
|
|
|
25
26
|
disabled?: boolean;
|
|
26
27
|
/**
|
|
27
28
|
* Submits with the enclosing form when checked, mirroring a native checkbox:
|
|
28
|
-
* an unchecked control contributes nothing.
|
|
29
|
+
* an unchecked or disabled control contributes nothing.
|
|
29
30
|
*/
|
|
30
31
|
name?: string;
|
|
31
32
|
/** Value submitted when checked. Defaults to `"on"`, as a native checkbox does. */
|
|
@@ -34,8 +35,8 @@ type SwitchRootOwnProps = {
|
|
|
34
35
|
* Component to render instead of the default `<button>`. Props, className, event
|
|
35
36
|
* handlers and ref are merged onto it.
|
|
36
37
|
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
38
|
+
* The default `<button>` is what supplies focus, Space and Enter activation and
|
|
39
|
+
* `disabled`; what `render` produces instead is the consumer's choice.
|
|
39
40
|
*/
|
|
40
41
|
render?: ReactElement;
|
|
41
42
|
/** Ref to the rendered element. Merged with any ref on the `render` element. */
|
|
@@ -51,6 +52,10 @@ type SwitchRootProps = SwitchRootOwnProps & Omit<ComponentPropsWithRef<'button'>
|
|
|
51
52
|
* of it is synthesised here. Per the WAI-ARIA switch pattern it carries `role="switch"`
|
|
52
53
|
* and `aria-checked`.
|
|
53
54
|
*
|
|
55
|
+
* Inside a form it behaves as a checkbox would there: `form.reset()` returns it to the
|
|
56
|
+
* state it mounted with. The change is reported through `onCheckedChange`, so a
|
|
57
|
+
* controlled switch moves only if its parent accepts it.
|
|
58
|
+
*
|
|
54
59
|
* It has no accessible name of its own — pair it with a `<label htmlFor>` by `id`, or
|
|
55
60
|
* pass `aria-label` or `aria-labelledby`. That is the consumer's decision, not one a
|
|
56
61
|
* headless component should guess.
|
|
@@ -87,11 +92,7 @@ declare namespace index_parts {
|
|
|
87
92
|
export { SwitchRoot as Root, SwitchThumb as Thumb };
|
|
88
93
|
}
|
|
89
94
|
|
|
90
|
-
/**
|
|
91
|
-
* A type alias rather than an interface: only aliases get an implicit index
|
|
92
|
-
* signature, which is what lets this satisfy the `Record<string, unknown>` the
|
|
93
|
-
* state-attribute mapper is generic over.
|
|
94
|
-
*/
|
|
95
|
+
/** The state Switch.Root shares with its parts, and projects as `data-*` attributes. */
|
|
95
96
|
type SwitchState = {
|
|
96
97
|
checked: boolean;
|
|
97
98
|
disabled: boolean;
|
package/dist/switch/index.d.ts
CHANGED
|
@@ -7,8 +7,9 @@ import { ReactElement, Ref, ComponentPropsWithRef } from 'react';
|
|
|
7
7
|
* and checked without being declared here.
|
|
8
8
|
*
|
|
9
9
|
* `id` in particular is how a switch gets an accessible name, paired with a
|
|
10
|
-
* `<label htmlFor
|
|
11
|
-
*
|
|
10
|
+
* `<label htmlFor>`. A wrapping `<label>` names the rendered `<button>` too, but
|
|
11
|
+
* `jsx-a11y/label-has-associated-control` rejects a button as a nested control, so the
|
|
12
|
+
* explicit pairing is the one that passes lint.
|
|
12
13
|
*/
|
|
13
14
|
type SwitchRootOwnProps = {
|
|
14
15
|
/**
|
|
@@ -25,7 +26,7 @@ type SwitchRootOwnProps = {
|
|
|
25
26
|
disabled?: boolean;
|
|
26
27
|
/**
|
|
27
28
|
* Submits with the enclosing form when checked, mirroring a native checkbox:
|
|
28
|
-
* an unchecked control contributes nothing.
|
|
29
|
+
* an unchecked or disabled control contributes nothing.
|
|
29
30
|
*/
|
|
30
31
|
name?: string;
|
|
31
32
|
/** Value submitted when checked. Defaults to `"on"`, as a native checkbox does. */
|
|
@@ -34,8 +35,8 @@ type SwitchRootOwnProps = {
|
|
|
34
35
|
* Component to render instead of the default `<button>`. Props, className, event
|
|
35
36
|
* handlers and ref are merged onto it.
|
|
36
37
|
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
38
|
+
* The default `<button>` is what supplies focus, Space and Enter activation and
|
|
39
|
+
* `disabled`; what `render` produces instead is the consumer's choice.
|
|
39
40
|
*/
|
|
40
41
|
render?: ReactElement;
|
|
41
42
|
/** Ref to the rendered element. Merged with any ref on the `render` element. */
|
|
@@ -51,6 +52,10 @@ type SwitchRootProps = SwitchRootOwnProps & Omit<ComponentPropsWithRef<'button'>
|
|
|
51
52
|
* of it is synthesised here. Per the WAI-ARIA switch pattern it carries `role="switch"`
|
|
52
53
|
* and `aria-checked`.
|
|
53
54
|
*
|
|
55
|
+
* Inside a form it behaves as a checkbox would there: `form.reset()` returns it to the
|
|
56
|
+
* state it mounted with. The change is reported through `onCheckedChange`, so a
|
|
57
|
+
* controlled switch moves only if its parent accepts it.
|
|
58
|
+
*
|
|
54
59
|
* It has no accessible name of its own — pair it with a `<label htmlFor>` by `id`, or
|
|
55
60
|
* pass `aria-label` or `aria-labelledby`. That is the consumer's decision, not one a
|
|
56
61
|
* headless component should guess.
|
|
@@ -87,11 +92,7 @@ declare namespace index_parts {
|
|
|
87
92
|
export { SwitchRoot as Root, SwitchThumb as Thumb };
|
|
88
93
|
}
|
|
89
94
|
|
|
90
|
-
/**
|
|
91
|
-
* A type alias rather than an interface: only aliases get an implicit index
|
|
92
|
-
* signature, which is what lets this satisfy the `Record<string, unknown>` the
|
|
93
|
-
* state-attribute mapper is generic over.
|
|
94
|
-
*/
|
|
95
|
+
/** The state Switch.Root shares with its parts, and projects as `data-*` attributes. */
|
|
95
96
|
type SwitchState = {
|
|
96
97
|
checked: boolean;
|
|
97
98
|
disabled: boolean;
|
package/dist/switch/index.js
CHANGED
|
@@ -14,7 +14,7 @@ __export(index_parts_exports, {
|
|
|
14
14
|
});
|
|
15
15
|
|
|
16
16
|
// src/switch/SwitchRoot.tsx
|
|
17
|
-
import { useEffect, useMemo, useRef } from "react";
|
|
17
|
+
import { useCallback, useEffect, useMemo, useRef } from "react";
|
|
18
18
|
|
|
19
19
|
// src/switch/SwitchRootContext.ts
|
|
20
20
|
import { createContext, useContext } from "react";
|
|
@@ -56,8 +56,17 @@ function SwitchRoot({
|
|
|
56
56
|
name: "Switch.Root",
|
|
57
57
|
state: "checked"
|
|
58
58
|
});
|
|
59
|
+
const commitChecked = useCallback(
|
|
60
|
+
(next) => {
|
|
61
|
+
setChecked(next);
|
|
62
|
+
onCheckedChange?.(next);
|
|
63
|
+
},
|
|
64
|
+
[setChecked, onCheckedChange]
|
|
65
|
+
);
|
|
59
66
|
const state = useMemo(() => ({ checked, disabled }), [checked, disabled]);
|
|
60
|
-
const elementRef =
|
|
67
|
+
const elementRef = useRef(null);
|
|
68
|
+
const inputRef = useRef(null);
|
|
69
|
+
useFormReset({ elementRef, inputRef, checked, commitChecked });
|
|
61
70
|
const element = useRender({
|
|
62
71
|
render,
|
|
63
72
|
defaultTagName: "button",
|
|
@@ -66,36 +75,57 @@ function SwitchRoot({
|
|
|
66
75
|
type: "button",
|
|
67
76
|
role: "switch",
|
|
68
77
|
"aria-checked": checked,
|
|
69
|
-
// The platform suppresses activation
|
|
78
|
+
// The platform suppresses activation and focus on a native button.
|
|
70
79
|
disabled: disabled || void 0,
|
|
71
80
|
...switchDataAttributes(state),
|
|
72
81
|
className,
|
|
73
82
|
children,
|
|
74
83
|
ref: elementRef,
|
|
75
84
|
onClick() {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
onCheckedChange?.(next);
|
|
85
|
+
if (disabled) return;
|
|
86
|
+
commitChecked(!checked);
|
|
79
87
|
}
|
|
80
88
|
},
|
|
81
89
|
consumerProps: rest
|
|
82
90
|
});
|
|
83
91
|
return /* @__PURE__ */ jsxs(SwitchRootContext.Provider, { value: state, children: [
|
|
84
92
|
element,
|
|
85
|
-
name !== void 0 ? /* @__PURE__ */ jsx(
|
|
93
|
+
name !== void 0 ? /* @__PURE__ */ jsx(
|
|
94
|
+
"input",
|
|
95
|
+
{
|
|
96
|
+
ref: inputRef,
|
|
97
|
+
type: "checkbox",
|
|
98
|
+
hidden: true,
|
|
99
|
+
readOnly: true,
|
|
100
|
+
name,
|
|
101
|
+
value,
|
|
102
|
+
checked,
|
|
103
|
+
disabled
|
|
104
|
+
}
|
|
105
|
+
) : null
|
|
86
106
|
] });
|
|
87
107
|
}
|
|
88
|
-
function
|
|
89
|
-
|
|
108
|
+
function useFormReset({
|
|
109
|
+
elementRef,
|
|
110
|
+
inputRef,
|
|
111
|
+
checked,
|
|
112
|
+
commitChecked
|
|
113
|
+
}) {
|
|
114
|
+
const { current: initialChecked } = useRef(checked);
|
|
90
115
|
useEffect(() => {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
116
|
+
const form = elementRef.current?.form;
|
|
117
|
+
if (!form) return;
|
|
118
|
+
function onReset(event) {
|
|
119
|
+
queueMicrotask(() => {
|
|
120
|
+
if (event.defaultPrevented) return;
|
|
121
|
+
if (inputRef.current) inputRef.current.checked = checked;
|
|
122
|
+
if (checked === initialChecked) return;
|
|
123
|
+
commitChecked(initialChecked);
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
form.addEventListener("reset", onReset);
|
|
127
|
+
return () => form.removeEventListener("reset", onReset);
|
|
128
|
+
}, [elementRef, inputRef, checked, commitChecked, initialChecked]);
|
|
99
129
|
}
|
|
100
130
|
|
|
101
131
|
// src/switch/SwitchThumb.tsx
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arun-dev/headless",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"description": "Unstyled React behaviour primitives — render engine, controlled/uncontrolled state, and data-* state projection. Ships no CSS and no class names.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
"react-dom": "19.2.8",
|
|
80
80
|
"tsup": "8.5.1",
|
|
81
81
|
"typescript": "5.9.3",
|
|
82
|
-
"vitest": "
|
|
82
|
+
"vitest": "4.1.11",
|
|
83
83
|
"@arun-dev/config": "0.0.0"
|
|
84
84
|
},
|
|
85
85
|
"scripts": {
|