@arun-dev/headless 2.1.0 → 3.0.1

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.
@@ -31,11 +31,44 @@ __export(index_parts_exports, {
31
31
  Thumb: () => SwitchThumb
32
32
  });
33
33
 
34
- // src/switch/root/SwitchRoot.tsx
35
- var import_react5 = require("react");
34
+ // src/switch/SwitchRoot.tsx
35
+ var import_react4 = require("react");
36
36
 
37
- // src/useButton.ts
37
+ // src/core/useControlled.ts
38
38
  var import_react = require("react");
39
+ function useControlled({
40
+ controlled,
41
+ default: defaultValue,
42
+ name = "Component",
43
+ state = "value"
44
+ }) {
45
+ const { current: isControlled } = (0, import_react.useRef)(controlled !== void 0);
46
+ const [uncontrolled, setUncontrolled] = (0, import_react.useState)(defaultValue);
47
+ const defaultRef = (0, import_react.useRef)(defaultValue);
48
+ const value = isControlled ? controlled : uncontrolled;
49
+ if (process.env.NODE_ENV !== "production") {
50
+ if (isControlled !== (controlled !== void 0)) {
51
+ console.error(
52
+ `${name}: cannot switch between controlled and uncontrolled \`${state}\`. Decide which one this component is for the whole of its life.`
53
+ );
54
+ }
55
+ if (!isControlled && defaultRef.current !== defaultValue) {
56
+ console.error(
57
+ `${name}: cannot change the default \`${state}\` after mount. It is only read once, so later changes are silently ignored.`
58
+ );
59
+ }
60
+ }
61
+ const setValue = (0, import_react.useCallback)(
62
+ (next) => {
63
+ if (!isControlled) setUncontrolled(next);
64
+ },
65
+ [isControlled]
66
+ );
67
+ return [value, setValue];
68
+ }
69
+
70
+ // src/core/useRender.ts
71
+ var import_react2 = require("react");
39
72
 
40
73
  // src/core/mergeProps.ts
41
74
  function isEventHandler(key) {
@@ -69,18 +102,9 @@ function isSyntheticEvent(event) {
69
102
  function makeEventPreventable(event) {
70
103
  if (Object.prototype.hasOwnProperty.call(event, "preventComponentHandler")) return;
71
104
  const target = event;
72
- const prevent = () => {
105
+ target.preventComponentHandler = () => {
73
106
  target.componentHandlerPrevented = true;
74
107
  };
75
- target.preventComponentHandler = prevent;
76
- const native = event.nativeEvent;
77
- const stopImmediate = native?.stopImmediatePropagation?.bind(native);
78
- if (native && stopImmediate) {
79
- native.stopImmediatePropagation = () => {
80
- stopImmediate();
81
- prevent();
82
- };
83
- }
84
108
  }
85
109
  function isComponentHandlerPrevented(event) {
86
110
  return typeof event === "object" && event !== null && event.componentHandlerPrevented === true;
@@ -118,189 +142,45 @@ function mergeProps(...objects) {
118
142
  return merged;
119
143
  }
120
144
 
121
- // src/useButton.ts
122
- var ACTIVATION_HANDLERS = [
123
- "onClick",
124
- "onDoubleClick",
125
- "onMouseDown",
126
- "onMouseUp",
127
- "onPointerDown",
128
- "onPointerUp",
129
- "onTouchStart",
130
- "onTouchEnd",
131
- "onKeyDown",
132
- "onKeyUp",
133
- "onKeyPress"
134
- ];
135
- function activationHandlers() {
136
- return {
137
- onKeyDown(event) {
138
- if (event.target !== event.currentTarget || event.defaultPrevented) return;
139
- if (event.key === " ") {
140
- event.preventDefault();
141
- return;
142
- }
143
- if (event.key === "Enter") {
144
- event.preventDefault();
145
- click(event.currentTarget);
146
- }
147
- },
148
- onKeyUp(event) {
149
- if (event.target !== event.currentTarget || event.defaultPrevented) return;
150
- if (event.key === " ") click(event.currentTarget);
151
- }
152
- };
153
- }
154
- function click(element) {
155
- if (element && typeof element.click === "function") {
156
- element.click();
157
- }
158
- }
159
- function useButton({
160
- disabled = false,
161
- native,
162
- props = {}
163
- }) {
164
- const elementRef = (0, import_react.useRef)(null);
165
- (0, import_react.useEffect)(() => {
166
- if (process.env.NODE_ENV === "production") return;
167
- const element = elementRef.current;
168
- if (!element) return;
169
- const isButtonTag = element.tagName === "BUTTON";
170
- if (native && !isButtonTag) {
171
- console.error(
172
- `A component expected a native <button> but rendered <${element.tagName.toLowerCase()}>. Focus, keyboard activation and \`disabled\` will not behave natively.`
173
- );
174
- } else if (!native && isButtonTag) {
175
- console.error(
176
- "A component rendered a native <button> while treating it as a non-native element, so it carries synthesised attributes it does not need."
177
- );
178
- }
179
- }, [native]);
180
- return { props: buttonProps({ disabled, native, props }), ref: elementRef };
181
- }
182
- function buttonProps({ disabled, native, props }) {
183
- if (!disabled) {
184
- return native ? mergeProps({ type: "button" }, props) : mergeProps({ tabIndex: 0 }, activationHandlers(), props);
185
- }
186
- if (native) return mergeProps({ type: "button" }, props, { disabled: true, "data-disabled": "" });
187
- const sanitised = {};
188
- for (const key of Object.keys(props)) {
189
- if (key === "href" || ACTIVATION_HANDLERS.includes(key)) continue;
190
- sanitised[key] = props[key];
191
- }
192
- return {
193
- ...sanitised,
194
- "aria-disabled": true,
195
- "data-disabled": "",
196
- // Mirrors a native disabled button, which is not focusable.
197
- tabIndex: -1
198
- };
199
- }
200
- function retractActivationProps(props) {
201
- const overrides = {};
202
- for (const key of Object.keys(props)) {
203
- if (key === "href" || ACTIVATION_HANDLERS.includes(key)) overrides[key] = void 0;
204
- }
205
- return overrides;
206
- }
207
-
208
- // src/core/useControlled.ts
209
- var import_react2 = require("react");
210
- function useControlled({
211
- controlled,
212
- default: defaultValue,
213
- name = "Component",
214
- state = "value"
215
- }) {
216
- const { current: isControlled } = (0, import_react2.useRef)(controlled !== void 0);
217
- const [uncontrolled, setUncontrolled] = (0, import_react2.useState)(defaultValue);
218
- const defaultRef = (0, import_react2.useRef)(defaultValue);
219
- const value = isControlled ? controlled : uncontrolled;
220
- if (process.env.NODE_ENV !== "production") {
221
- if (isControlled !== (controlled !== void 0)) {
222
- console.error(
223
- `${name}: cannot switch between controlled and uncontrolled \`${state}\`. Decide which one this component is for the whole of its life.`
224
- );
225
- }
226
- if (!isControlled && defaultRef.current !== defaultValue) {
227
- console.error(
228
- `${name}: cannot change the default \`${state}\` after mount. It is only read once, so later changes are silently ignored.`
229
- );
230
- }
231
- }
232
- const setValue = (0, import_react2.useCallback)(
233
- (next) => {
234
- if (!isControlled) setUncontrolled(next);
235
- },
236
- [isControlled]
237
- );
238
- return [value, setValue];
239
- }
240
-
241
- // src/core/useRender.ts
242
- var import_react3 = require("react");
243
-
244
- // src/core/stateAttributes.ts
245
- function getStateAttributes(state, mapping) {
246
- if (!mapping) return {};
247
- const attributes = {};
248
- for (const key of Object.keys(state)) {
249
- const toAttributes = mapping[key];
250
- if (!toAttributes) continue;
251
- const result = toAttributes(state[key]);
252
- if (result) Object.assign(attributes, result);
253
- }
254
- return attributes;
255
- }
256
- function booleanAttribute(whenTrue, whenFalse) {
257
- return (value) => {
258
- if (value) return { [whenTrue]: "" };
259
- return whenFalse ? { [whenFalse]: "" } : null;
260
- };
261
- }
262
- var disabledAttribute = booleanAttribute("data-disabled");
263
-
264
145
  // src/core/useRender.ts
265
146
  function useRender({
266
147
  render,
267
148
  defaultTagName,
268
149
  props,
269
- consumerProps,
270
- state,
271
- stateAttributes
150
+ consumerProps
272
151
  }) {
273
- const attributes = state ? getStateAttributes(state, stateAttributes) : {};
274
- const merged = mergeProps(attributes, props, consumerProps);
275
- if ((0, import_react3.isValidElement)(render)) {
276
- return (0, import_react3.cloneElement)(render, mergeProps(merged, render.props));
152
+ const merged = mergeProps(props, consumerProps);
153
+ if ((0, import_react2.isValidElement)(render)) {
154
+ return (0, import_react2.cloneElement)(render, mergeProps(merged, render.props));
277
155
  }
278
- return (0, import_react3.createElement)(defaultTagName, merged);
156
+ return (0, import_react2.createElement)(defaultTagName, merged);
279
157
  }
280
158
 
281
159
  // src/switch/SwitchRootContext.ts
282
- var import_react4 = require("react");
283
- var SwitchRootContext = (0, import_react4.createContext)(null);
160
+ var import_react3 = require("react");
161
+ var SwitchRootContext = (0, import_react3.createContext)(null);
284
162
  function useSwitchRootContext() {
285
- const context = (0, import_react4.useContext)(SwitchRootContext);
163
+ const context = (0, import_react3.useContext)(SwitchRootContext);
286
164
  if (context === null) {
287
165
  throw new Error("<Switch.Thumb> must be rendered inside <Switch.Root>.");
288
166
  }
289
167
  return context;
290
168
  }
291
169
 
292
- // src/switch/stateAttributes.ts
293
- var switchStateAttributes = {
294
- checked: booleanAttribute("data-checked", "data-unchecked"),
295
- disabled: disabledAttribute
296
- };
170
+ // src/switch/switchDataAttributes.ts
171
+ function switchDataAttributes({ checked, disabled }) {
172
+ return {
173
+ "data-checked": checked ? "" : void 0,
174
+ "data-unchecked": checked ? void 0 : "",
175
+ "data-disabled": disabled ? "" : void 0
176
+ };
177
+ }
297
178
 
298
- // src/switch/root/SwitchRoot.tsx
179
+ // src/switch/SwitchRoot.tsx
299
180
  var import_jsx_runtime = require("react/jsx-runtime");
300
181
  function SwitchRoot({
301
182
  checked: checkedProp,
302
183
  defaultChecked,
303
- nativeButton,
304
184
  onCheckedChange,
305
185
  disabled = false,
306
186
  name,
@@ -316,54 +196,92 @@ function SwitchRoot({
316
196
  name: "Switch.Root",
317
197
  state: "checked"
318
198
  });
319
- const state = (0, import_react5.useMemo)(() => ({ checked, disabled }), [checked, disabled]);
320
- const isNativeButton = nativeButton ?? (render === void 0 || render.type === "button");
321
- const { props: consumerProps, ref: buttonRef } = useButton({
322
- disabled,
323
- native: isNativeButton,
324
- props: rest
325
- });
326
- const safeRender = disabled && !isNativeButton && render !== void 0 ? (0, import_react5.cloneElement)(render, retractActivationProps(render.props)) : render;
199
+ const state = (0, import_react4.useMemo)(() => ({ checked, disabled }), [checked, disabled]);
200
+ const elementRef = (0, import_react4.useRef)(null);
201
+ const inputRef = (0, import_react4.useRef)(null);
202
+ useNativeButtonWarning(elementRef);
203
+ useFormReset({ elementRef, inputRef, checked, setChecked, onCheckedChange });
327
204
  const element = useRender({
328
- render: safeRender,
205
+ render,
329
206
  defaultTagName: "button",
330
- state,
331
- stateAttributes: switchStateAttributes,
332
207
  props: {
208
+ // Without this a switch inside a form would submit it on every toggle.
209
+ type: "button",
333
210
  role: "switch",
334
211
  "aria-checked": checked,
212
+ // The platform suppresses activation, focus and the click handler below.
213
+ disabled: disabled || void 0,
214
+ ...switchDataAttributes(state),
335
215
  className,
336
216
  children,
337
- ref: buttonRef,
217
+ ref: elementRef,
338
218
  onClick() {
339
- if (disabled) return;
340
219
  const next = !checked;
341
220
  setChecked(next);
342
221
  onCheckedChange?.(next);
343
222
  }
344
223
  },
345
- consumerProps
224
+ consumerProps: rest
346
225
  });
347
226
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(SwitchRootContext.Provider, { value: state, children: [
348
227
  element,
349
- name !== void 0 && checked ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { type: "hidden", name, value }) : null
228
+ name !== void 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
229
+ "input",
230
+ {
231
+ ref: inputRef,
232
+ type: "checkbox",
233
+ hidden: true,
234
+ readOnly: true,
235
+ name,
236
+ value,
237
+ checked,
238
+ disabled
239
+ }
240
+ ) : null
350
241
  ] });
351
242
  }
352
-
353
- // src/switch/thumb/SwitchThumb.tsx
354
- function SwitchThumb({
355
- className,
356
- children,
357
- render,
358
- ...rest
243
+ function useFormReset({
244
+ elementRef,
245
+ inputRef,
246
+ checked,
247
+ setChecked,
248
+ onCheckedChange
359
249
  }) {
250
+ const { current: initialChecked } = (0, import_react4.useRef)(checked);
251
+ (0, import_react4.useEffect)(() => {
252
+ const form = elementRef.current?.form;
253
+ if (!form) return;
254
+ function onReset(event) {
255
+ queueMicrotask(() => {
256
+ if (event.defaultPrevented) return;
257
+ if (inputRef.current) inputRef.current.checked = checked;
258
+ if (checked === initialChecked) return;
259
+ setChecked(initialChecked);
260
+ onCheckedChange?.(initialChecked);
261
+ });
262
+ }
263
+ form.addEventListener("reset", onReset);
264
+ return () => form.removeEventListener("reset", onReset);
265
+ }, [elementRef, inputRef, checked, setChecked, onCheckedChange, initialChecked]);
266
+ }
267
+ function useNativeButtonWarning(elementRef) {
268
+ (0, import_react4.useEffect)(() => {
269
+ if (process.env.NODE_ENV === "production") return;
270
+ const element = elementRef.current;
271
+ if (!element || element.tagName === "BUTTON") return;
272
+ console.error(
273
+ `Switch.Root rendered <${element.tagName.toLowerCase()}> instead of <button>. Focus, Space and Enter activation and \`disabled\` all come from the button element; pass a \`render\` component that forwards its props to one.`
274
+ );
275
+ }, [elementRef]);
276
+ }
277
+
278
+ // src/switch/SwitchThumb.tsx
279
+ function SwitchThumb({ className, children, render, ...rest }) {
360
280
  const state = useSwitchRootContext();
361
281
  return useRender({
362
282
  render,
363
283
  defaultTagName: "span",
364
- state,
365
- stateAttributes: switchStateAttributes,
366
- props: { "aria-hidden": true, className, children },
284
+ props: { "aria-hidden": true, ...switchDataAttributes(state), className, children },
367
285
  consumerProps: rest
368
286
  });
369
287
  }
@@ -1,7 +1,17 @@
1
1
  import * as react from 'react';
2
- import { ReactNode, ReactElement, Ref } from 'react';
2
+ import { ReactElement, Ref, ComponentPropsWithRef } from 'react';
3
3
 
4
- interface SwitchRootProps {
4
+ /**
5
+ * Switch.Root's own props. Everything else — `id`, `className`, `children`, `aria-*`,
6
+ * `data-*`, event handlers — comes from React's own `<button>` props, so it is typed
7
+ * and checked without being declared here.
8
+ *
9
+ * `id` in particular is how a switch gets an accessible name, paired with a
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.
13
+ */
14
+ type SwitchRootOwnProps = {
5
15
  /**
6
16
  * Controlled state. Provide `onCheckedChange` alongside it.
7
17
  *
@@ -12,64 +22,57 @@ interface SwitchRootProps {
12
22
  checked?: boolean;
13
23
  /** Initial state when uncontrolled. Read once, at mount. */
14
24
  defaultChecked?: boolean;
15
- /**
16
- * Whether the rendered element is a native `<button>`.
17
- *
18
- * Inferred from `render`, which is right for an element literal. Set it explicitly
19
- * when rendering a *component* — `render={<Tooltip.Trigger />}` cannot be inspected,
20
- * so it is treated as non-native and picks up attributes it does not need. A mismatch
21
- * logs a development warning.
22
- */
23
- nativeButton?: boolean;
24
25
  onCheckedChange?: (checked: boolean) => void;
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. */
32
33
  value?: string;
33
34
  /**
34
- * Names the switch when paired with a `<label htmlFor>`.
35
- *
36
- * Declared rather than left to the prop spread because it is how a switch gets an
37
- * accessible name: `Switch.Root` renders a `<button>`, which a wrapping `<label>`
38
- * would name implicitly, but `jsx-a11y/label-has-associated-control` rejects a
39
- * `<button>` as a nested control. The explicit association keeps the rule quiet
40
- * without a disable at the call site.
41
- */
42
- id?: string;
43
- className?: string;
44
- children?: ReactNode;
45
- /**
46
- * Element to render instead of the default `<button>`. Props, className, event
35
+ * Component to render instead of the default `<button>`. Props, className, event
47
36
  * handlers and ref are merged onto it.
37
+ *
38
+ * It must render a native `<button>` — a wrapper such as `<Tooltip.Trigger />` that
39
+ * forwards its props to one. Anything else is reported in development.
48
40
  */
49
41
  render?: ReactElement;
42
+ /** Ref to the rendered element. Merged with any ref on the `render` element. */
50
43
  ref?: Ref<HTMLElement>;
51
- }
44
+ };
45
+ type SwitchRootProps = SwitchRootOwnProps & Omit<ComponentPropsWithRef<'button'>, keyof SwitchRootOwnProps>;
52
46
  /**
53
47
  * A switch — an immediate on/off control, distinct from a checkbox in that it takes
54
48
  * effect at once rather than on submit.
55
49
  *
56
- * Renders a native `<button>`, which supplies focusability, Space and Enter
57
- * activation, and the disabled semantics for free. Per the WAI-ARIA switch pattern
58
- * it carries `role="switch"` and `aria-checked`.
50
+ * Always a native `<button>`, which is the whole reason this component is short: the
51
+ * platform supplies focusability, Space and Enter activation, and `disabled`, so none
52
+ * of it is synthesised here. Per the WAI-ARIA switch pattern it carries `role="switch"`
53
+ * and `aria-checked`.
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.
59
58
  *
60
59
  * It has no accessible name of its own — pair it with a `<label htmlFor>` by `id`, or
61
- * pass `aria-label` or `aria-labelledby`. That is the consumer's decision, not a headless
62
- * component should guess.
60
+ * pass `aria-label` or `aria-labelledby`. That is the consumer's decision, not one a
61
+ * headless component should guess.
63
62
  */
64
- declare function SwitchRoot({ checked: checkedProp, defaultChecked, nativeButton, onCheckedChange, disabled, name, value, className, children, render, ...rest }: SwitchRootProps & Record<string, unknown>): react.JSX.Element;
63
+ declare function SwitchRoot({ checked: checkedProp, defaultChecked, onCheckedChange, disabled, name, value, className, children, render, ...rest }: SwitchRootProps): react.JSX.Element;
65
64
 
66
- interface SwitchThumbProps {
67
- className?: string;
68
- children?: ReactNode;
65
+ /**
66
+ * Switch.Thumb's own props. Everything else — `id`, `className`, `children`, `aria-*`,
67
+ * `data-*` — comes from React's own `<span>` props.
68
+ */
69
+ type SwitchThumbOwnProps = {
69
70
  /** Element to render instead of the default `<span>`. */
70
71
  render?: ReactElement;
72
+ /** Ref to the rendered element, whatever `render` makes it. */
71
73
  ref?: Ref<HTMLElement>;
72
- }
74
+ };
75
+ type SwitchThumbProps = SwitchThumbOwnProps & Omit<ComponentPropsWithRef<'span'>, keyof SwitchThumbOwnProps>;
73
76
  /**
74
77
  * The moving part of the switch.
75
78
  *
@@ -83,17 +86,13 @@ interface SwitchThumbProps {
83
86
  * Purely presentational — hidden from assistive technology, since the Root already
84
87
  * announces the state.
85
88
  */
86
- declare function SwitchThumb({ className, children, render, ...rest }: SwitchThumbProps & Record<string, unknown>): ReactElement<unknown, string | react.JSXElementConstructor<any>>;
89
+ declare function SwitchThumb({ className, children, render, ...rest }: SwitchThumbProps): ReactElement<unknown, string | react.JSXElementConstructor<any>>;
87
90
 
88
91
  declare namespace index_parts {
89
92
  export { SwitchRoot as Root, SwitchThumb as Thumb };
90
93
  }
91
94
 
92
- /**
93
- * A type alias rather than an interface: only aliases get an implicit index
94
- * signature, which is what lets this satisfy the `Record<string, unknown>` the
95
- * state-attribute mapper is generic over.
96
- */
95
+ /** The state Switch.Root shares with its parts, and projects as `data-*` attributes. */
97
96
  type SwitchState = {
98
97
  checked: boolean;
99
98
  disabled: boolean;
@@ -1,7 +1,17 @@
1
1
  import * as react from 'react';
2
- import { ReactNode, ReactElement, Ref } from 'react';
2
+ import { ReactElement, Ref, ComponentPropsWithRef } from 'react';
3
3
 
4
- interface SwitchRootProps {
4
+ /**
5
+ * Switch.Root's own props. Everything else — `id`, `className`, `children`, `aria-*`,
6
+ * `data-*`, event handlers — comes from React's own `<button>` props, so it is typed
7
+ * and checked without being declared here.
8
+ *
9
+ * `id` in particular is how a switch gets an accessible name, paired with a
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.
13
+ */
14
+ type SwitchRootOwnProps = {
5
15
  /**
6
16
  * Controlled state. Provide `onCheckedChange` alongside it.
7
17
  *
@@ -12,64 +22,57 @@ interface SwitchRootProps {
12
22
  checked?: boolean;
13
23
  /** Initial state when uncontrolled. Read once, at mount. */
14
24
  defaultChecked?: boolean;
15
- /**
16
- * Whether the rendered element is a native `<button>`.
17
- *
18
- * Inferred from `render`, which is right for an element literal. Set it explicitly
19
- * when rendering a *component* — `render={<Tooltip.Trigger />}` cannot be inspected,
20
- * so it is treated as non-native and picks up attributes it does not need. A mismatch
21
- * logs a development warning.
22
- */
23
- nativeButton?: boolean;
24
25
  onCheckedChange?: (checked: boolean) => void;
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. */
32
33
  value?: string;
33
34
  /**
34
- * Names the switch when paired with a `<label htmlFor>`.
35
- *
36
- * Declared rather than left to the prop spread because it is how a switch gets an
37
- * accessible name: `Switch.Root` renders a `<button>`, which a wrapping `<label>`
38
- * would name implicitly, but `jsx-a11y/label-has-associated-control` rejects a
39
- * `<button>` as a nested control. The explicit association keeps the rule quiet
40
- * without a disable at the call site.
41
- */
42
- id?: string;
43
- className?: string;
44
- children?: ReactNode;
45
- /**
46
- * Element to render instead of the default `<button>`. Props, className, event
35
+ * Component to render instead of the default `<button>`. Props, className, event
47
36
  * handlers and ref are merged onto it.
37
+ *
38
+ * It must render a native `<button>` — a wrapper such as `<Tooltip.Trigger />` that
39
+ * forwards its props to one. Anything else is reported in development.
48
40
  */
49
41
  render?: ReactElement;
42
+ /** Ref to the rendered element. Merged with any ref on the `render` element. */
50
43
  ref?: Ref<HTMLElement>;
51
- }
44
+ };
45
+ type SwitchRootProps = SwitchRootOwnProps & Omit<ComponentPropsWithRef<'button'>, keyof SwitchRootOwnProps>;
52
46
  /**
53
47
  * A switch — an immediate on/off control, distinct from a checkbox in that it takes
54
48
  * effect at once rather than on submit.
55
49
  *
56
- * Renders a native `<button>`, which supplies focusability, Space and Enter
57
- * activation, and the disabled semantics for free. Per the WAI-ARIA switch pattern
58
- * it carries `role="switch"` and `aria-checked`.
50
+ * Always a native `<button>`, which is the whole reason this component is short: the
51
+ * platform supplies focusability, Space and Enter activation, and `disabled`, so none
52
+ * of it is synthesised here. Per the WAI-ARIA switch pattern it carries `role="switch"`
53
+ * and `aria-checked`.
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.
59
58
  *
60
59
  * It has no accessible name of its own — pair it with a `<label htmlFor>` by `id`, or
61
- * pass `aria-label` or `aria-labelledby`. That is the consumer's decision, not a headless
62
- * component should guess.
60
+ * pass `aria-label` or `aria-labelledby`. That is the consumer's decision, not one a
61
+ * headless component should guess.
63
62
  */
64
- declare function SwitchRoot({ checked: checkedProp, defaultChecked, nativeButton, onCheckedChange, disabled, name, value, className, children, render, ...rest }: SwitchRootProps & Record<string, unknown>): react.JSX.Element;
63
+ declare function SwitchRoot({ checked: checkedProp, defaultChecked, onCheckedChange, disabled, name, value, className, children, render, ...rest }: SwitchRootProps): react.JSX.Element;
65
64
 
66
- interface SwitchThumbProps {
67
- className?: string;
68
- children?: ReactNode;
65
+ /**
66
+ * Switch.Thumb's own props. Everything else — `id`, `className`, `children`, `aria-*`,
67
+ * `data-*` — comes from React's own `<span>` props.
68
+ */
69
+ type SwitchThumbOwnProps = {
69
70
  /** Element to render instead of the default `<span>`. */
70
71
  render?: ReactElement;
72
+ /** Ref to the rendered element, whatever `render` makes it. */
71
73
  ref?: Ref<HTMLElement>;
72
- }
74
+ };
75
+ type SwitchThumbProps = SwitchThumbOwnProps & Omit<ComponentPropsWithRef<'span'>, keyof SwitchThumbOwnProps>;
73
76
  /**
74
77
  * The moving part of the switch.
75
78
  *
@@ -83,17 +86,13 @@ interface SwitchThumbProps {
83
86
  * Purely presentational — hidden from assistive technology, since the Root already
84
87
  * announces the state.
85
88
  */
86
- declare function SwitchThumb({ className, children, render, ...rest }: SwitchThumbProps & Record<string, unknown>): ReactElement<unknown, string | react.JSXElementConstructor<any>>;
89
+ declare function SwitchThumb({ className, children, render, ...rest }: SwitchThumbProps): ReactElement<unknown, string | react.JSXElementConstructor<any>>;
87
90
 
88
91
  declare namespace index_parts {
89
92
  export { SwitchRoot as Root, SwitchThumb as Thumb };
90
93
  }
91
94
 
92
- /**
93
- * A type alias rather than an interface: only aliases get an implicit index
94
- * signature, which is what lets this satisfy the `Record<string, unknown>` the
95
- * state-attribute mapper is generic over.
96
- */
95
+ /** The state Switch.Root shares with its parts, and projects as `data-*` attributes. */
97
96
  type SwitchState = {
98
97
  checked: boolean;
99
98
  disabled: boolean;