@arun-dev/headless 1.0.0 → 2.1.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.
@@ -60,14 +60,33 @@ function isSyntheticEvent(event) {
60
60
  return typeof event === "object" && event !== null && "nativeEvent" in event;
61
61
  }
62
62
  function makeEventPreventable(event) {
63
+ if (Object.prototype.hasOwnProperty.call(event, "preventComponentHandler")) return;
63
64
  const target = event;
64
- target.preventComponentHandler = () => {
65
+ const prevent = () => {
65
66
  target.componentHandlerPrevented = true;
66
67
  };
68
+ target.preventComponentHandler = prevent;
69
+ const native = event.nativeEvent;
70
+ const stopImmediate = native?.stopImmediatePropagation?.bind(native);
71
+ if (native && stopImmediate) {
72
+ native.stopImmediatePropagation = () => {
73
+ stopImmediate();
74
+ prevent();
75
+ };
76
+ }
67
77
  }
68
78
  function isComponentHandlerPrevented(event) {
69
79
  return typeof event === "object" && event !== null && event.componentHandlerPrevented === true;
70
80
  }
81
+ function chainHandlers(earlier, value) {
82
+ return (...args) => {
83
+ const event = args[0];
84
+ if (isSyntheticEvent(event)) makeEventPreventable(event);
85
+ value(...args);
86
+ if (isComponentHandlerPrevented(event)) return;
87
+ earlier(...args);
88
+ };
89
+ }
71
90
  function mergeProps(...objects) {
72
91
  const merged = {};
73
92
  for (const props of objects) {
@@ -77,13 +96,7 @@ function mergeProps(...objects) {
77
96
  if (value === void 0) continue;
78
97
  if (isEventHandler(key) && typeof value === "function") {
79
98
  const earlier = merged[key];
80
- merged[key] = typeof earlier === "function" ? (...args) => {
81
- const event = args[0];
82
- if (isSyntheticEvent(event)) makeEventPreventable(event);
83
- value(...args);
84
- if (isComponentHandlerPrevented(event)) return;
85
- earlier(...args);
86
- } : value;
99
+ merged[key] = typeof earlier === "function" ? chainHandlers(earlier, value) : value;
87
100
  } else if (key === "className") {
88
101
  merged.className = merged.className ? `${String(merged.className)} ${String(value)}` : value;
89
102
  } else if (key === "style") {
@@ -123,11 +136,12 @@ function useRender({
123
136
  render,
124
137
  defaultTagName,
125
138
  props,
139
+ consumerProps,
126
140
  state,
127
141
  stateAttributes
128
142
  }) {
129
143
  const attributes = state ? getStateAttributes(state, stateAttributes) : {};
130
- const merged = mergeProps(attributes, ...props);
144
+ const merged = mergeProps(attributes, props, consumerProps);
131
145
  if ((0, import_react.isValidElement)(render)) {
132
146
  return (0, import_react.cloneElement)(render, mergeProps(merged, render.props));
133
147
  }
@@ -239,7 +253,8 @@ function Button({
239
253
  return useRender({
240
254
  render: safeRender,
241
255
  defaultTagName: "button",
242
- props: [{ role, className, children }, elementProps, { ref: buttonRef }]
256
+ props: { role, className, children, ref: buttonRef },
257
+ consumerProps: elementProps
243
258
  });
244
259
  }
245
260
  // Annotate the CommonJS export names for ESM import in node:
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  retractActivationProps,
3
3
  useButton
4
- } from "../chunk-2XSRAJHB.js";
4
+ } from "../chunk-TNMGZWHA.js";
5
5
  import {
6
6
  useRender
7
- } from "../chunk-HWG3GMTO.js";
7
+ } from "../chunk-PYLPZVWQ.js";
8
8
 
9
9
  // src/button/Button.tsx
10
10
  import { cloneElement } from "react";
@@ -24,7 +24,8 @@ function Button({
24
24
  return useRender({
25
25
  render: safeRender,
26
26
  defaultTagName: "button",
27
- props: [{ role, className, children }, elementProps, { ref: buttonRef }]
27
+ props: { role, className, children, ref: buttonRef },
28
+ consumerProps: elementProps
28
29
  });
29
30
  }
30
31
  export {
@@ -34,14 +34,33 @@ function isSyntheticEvent(event) {
34
34
  return typeof event === "object" && event !== null && "nativeEvent" in event;
35
35
  }
36
36
  function makeEventPreventable(event) {
37
+ if (Object.prototype.hasOwnProperty.call(event, "preventComponentHandler")) return;
37
38
  const target = event;
38
- target.preventComponentHandler = () => {
39
+ const prevent = () => {
39
40
  target.componentHandlerPrevented = true;
40
41
  };
42
+ target.preventComponentHandler = prevent;
43
+ const native = event.nativeEvent;
44
+ const stopImmediate = native?.stopImmediatePropagation?.bind(native);
45
+ if (native && stopImmediate) {
46
+ native.stopImmediatePropagation = () => {
47
+ stopImmediate();
48
+ prevent();
49
+ };
50
+ }
41
51
  }
42
52
  function isComponentHandlerPrevented(event) {
43
53
  return typeof event === "object" && event !== null && event.componentHandlerPrevented === true;
44
54
  }
55
+ function chainHandlers(earlier, value) {
56
+ return (...args) => {
57
+ const event = args[0];
58
+ if (isSyntheticEvent(event)) makeEventPreventable(event);
59
+ value(...args);
60
+ if (isComponentHandlerPrevented(event)) return;
61
+ earlier(...args);
62
+ };
63
+ }
45
64
  function mergeProps(...objects) {
46
65
  const merged = {};
47
66
  for (const props of objects) {
@@ -51,13 +70,7 @@ function mergeProps(...objects) {
51
70
  if (value === void 0) continue;
52
71
  if (isEventHandler(key) && typeof value === "function") {
53
72
  const earlier = merged[key];
54
- merged[key] = typeof earlier === "function" ? (...args) => {
55
- const event = args[0];
56
- if (isSyntheticEvent(event)) makeEventPreventable(event);
57
- value(...args);
58
- if (isComponentHandlerPrevented(event)) return;
59
- earlier(...args);
60
- } : value;
73
+ merged[key] = typeof earlier === "function" ? chainHandlers(earlier, value) : value;
61
74
  } else if (key === "className") {
62
75
  merged.className = merged.className ? `${String(merged.className)} ${String(value)}` : value;
63
76
  } else if (key === "style") {
@@ -98,11 +111,12 @@ function useRender({
98
111
  render,
99
112
  defaultTagName,
100
113
  props,
114
+ consumerProps,
101
115
  state,
102
116
  stateAttributes
103
117
  }) {
104
118
  const attributes = state ? getStateAttributes(state, stateAttributes) : {};
105
- const merged = mergeProps(attributes, ...props);
119
+ const merged = mergeProps(attributes, props, consumerProps);
106
120
  if (isValidElement(render)) {
107
121
  return cloneElement(render, mergeProps(merged, render.props));
108
122
  }
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  mergeProps
3
- } from "./chunk-HWG3GMTO.js";
3
+ } from "./chunk-PYLPZVWQ.js";
4
4
 
5
5
  // src/useButton.ts
6
6
  import { useEffect, useRef } from "react";
package/dist/index.cjs CHANGED
@@ -59,14 +59,33 @@ function isSyntheticEvent(event) {
59
59
  return typeof event === "object" && event !== null && "nativeEvent" in event;
60
60
  }
61
61
  function makeEventPreventable(event) {
62
+ if (Object.prototype.hasOwnProperty.call(event, "preventComponentHandler")) return;
62
63
  const target = event;
63
- target.preventComponentHandler = () => {
64
+ const prevent = () => {
64
65
  target.componentHandlerPrevented = true;
65
66
  };
67
+ target.preventComponentHandler = prevent;
68
+ const native = event.nativeEvent;
69
+ const stopImmediate = native?.stopImmediatePropagation?.bind(native);
70
+ if (native && stopImmediate) {
71
+ native.stopImmediatePropagation = () => {
72
+ stopImmediate();
73
+ prevent();
74
+ };
75
+ }
66
76
  }
67
77
  function isComponentHandlerPrevented(event) {
68
78
  return typeof event === "object" && event !== null && event.componentHandlerPrevented === true;
69
79
  }
80
+ function chainHandlers(earlier, value) {
81
+ return (...args) => {
82
+ const event = args[0];
83
+ if (isSyntheticEvent(event)) makeEventPreventable(event);
84
+ value(...args);
85
+ if (isComponentHandlerPrevented(event)) return;
86
+ earlier(...args);
87
+ };
88
+ }
70
89
  function mergeProps(...objects) {
71
90
  const merged = {};
72
91
  for (const props of objects) {
@@ -76,13 +95,7 @@ function mergeProps(...objects) {
76
95
  if (value === void 0) continue;
77
96
  if (isEventHandler(key) && typeof value === "function") {
78
97
  const earlier = merged[key];
79
- merged[key] = typeof earlier === "function" ? (...args) => {
80
- const event = args[0];
81
- if (isSyntheticEvent(event)) makeEventPreventable(event);
82
- value(...args);
83
- if (isComponentHandlerPrevented(event)) return;
84
- earlier(...args);
85
- } : value;
98
+ merged[key] = typeof earlier === "function" ? chainHandlers(earlier, value) : value;
86
99
  } else if (key === "className") {
87
100
  merged.className = merged.className ? `${String(merged.className)} ${String(value)}` : value;
88
101
  } else if (key === "style") {
@@ -125,11 +138,12 @@ function useRender({
125
138
  render,
126
139
  defaultTagName,
127
140
  props,
141
+ consumerProps,
128
142
  state,
129
143
  stateAttributes
130
144
  }) {
131
145
  const attributes = state ? getStateAttributes(state, stateAttributes) : {};
132
- const merged = mergeProps(attributes, ...props);
146
+ const merged = mergeProps(attributes, props, consumerProps);
133
147
  if ((0, import_react.isValidElement)(render)) {
134
148
  return (0, import_react.cloneElement)(render, mergeProps(merged, render.props));
135
149
  }
package/dist/index.d.cts CHANGED
@@ -1,12 +1,6 @@
1
1
  import { ReactElement } from 'react';
2
2
 
3
- /**
4
- * Prop merging for headless components.
5
- *
6
- * A component and its consumer both want to put things on the same element. Naive
7
- * spreading lets whichever runs last silently destroy the other's behaviour, so each
8
- * kind of prop is combined rather than replaced.
9
- */
3
+ /** The shape prop objects are handled as here: unknown keys, unknown values. */
10
4
  type UnknownProps = Record<string, unknown>;
11
5
  /**
12
6
  * An event a consumer's handler can use to stop the component's own handler running.
@@ -66,6 +60,17 @@ declare function mergeProps(...objects: (UnknownProps | undefined)[]): UnknownPr
66
60
  type StateAttributeMapping<State> = {
67
61
  [Key in keyof State]?: (value: State[Key]) => Record<string, string> | null;
68
62
  };
63
+ /**
64
+ * Turns a state object into the `data-*` attributes a mapping declares for it.
65
+ *
66
+ * Walks the state's own keys, so a field the mapping does not cover contributes
67
+ * nothing, and a mapping entry that returns `null` is skipped. Entries may each
68
+ * emit several attributes; later keys win if two entries name the same attribute.
69
+ *
70
+ * @param state - The component's current state.
71
+ * @param mapping - Which fields become attributes. `undefined` yields `{}`.
72
+ * @returns The attributes to spread onto the element.
73
+ */
69
74
  declare function getStateAttributes<State extends Record<string, unknown>>(state: State, mapping: StateAttributeMapping<State> | undefined): UnknownProps;
70
75
  /**
71
76
  * The common case: a boolean that emits one attribute when true and another when
@@ -98,14 +103,22 @@ interface UseRenderParams<State extends Record<string, unknown>> {
98
103
  render?: ReactElement | undefined;
99
104
  /** Tag rendered when `render` is not supplied. */
100
105
  defaultTagName: string;
101
- /** Prop objects to merge, in precedence order (later wins). */
102
- props: (UnknownProps | undefined)[];
106
+ /** The component's own props. */
107
+ props?: UnknownProps;
108
+ /**
109
+ * The consumer's props, spread from `...rest`.
110
+ *
111
+ * A separate slot rather than a position in a list: precedence is then a property of
112
+ * this signature rather than a rule each component has to remember. A component
113
+ * cannot put a consumer's props before its own, because there is nowhere to put them.
114
+ */
115
+ consumerProps?: UnknownProps;
103
116
  /** The part's state, projected onto the DOM through `stateAttributes`. */
104
117
  state?: State;
105
118
  /** How each state field becomes `data-*` attributes. */
106
119
  stateAttributes?: StateAttributeMapping<State>;
107
120
  }
108
- declare function useRender<State extends Record<string, unknown> = Record<string, never>>({ render, defaultTagName, props, state, stateAttributes, }: UseRenderParams<State>): ReactElement;
121
+ declare function useRender<State extends Record<string, unknown> = Record<string, never>>({ render, defaultTagName, props, consumerProps, state, stateAttributes, }: UseRenderParams<State>): ReactElement;
109
122
 
110
123
  /**
111
124
  * Supports both controlled and uncontrolled use of a single value.
package/dist/index.d.ts CHANGED
@@ -1,12 +1,6 @@
1
1
  import { ReactElement } from 'react';
2
2
 
3
- /**
4
- * Prop merging for headless components.
5
- *
6
- * A component and its consumer both want to put things on the same element. Naive
7
- * spreading lets whichever runs last silently destroy the other's behaviour, so each
8
- * kind of prop is combined rather than replaced.
9
- */
3
+ /** The shape prop objects are handled as here: unknown keys, unknown values. */
10
4
  type UnknownProps = Record<string, unknown>;
11
5
  /**
12
6
  * An event a consumer's handler can use to stop the component's own handler running.
@@ -66,6 +60,17 @@ declare function mergeProps(...objects: (UnknownProps | undefined)[]): UnknownPr
66
60
  type StateAttributeMapping<State> = {
67
61
  [Key in keyof State]?: (value: State[Key]) => Record<string, string> | null;
68
62
  };
63
+ /**
64
+ * Turns a state object into the `data-*` attributes a mapping declares for it.
65
+ *
66
+ * Walks the state's own keys, so a field the mapping does not cover contributes
67
+ * nothing, and a mapping entry that returns `null` is skipped. Entries may each
68
+ * emit several attributes; later keys win if two entries name the same attribute.
69
+ *
70
+ * @param state - The component's current state.
71
+ * @param mapping - Which fields become attributes. `undefined` yields `{}`.
72
+ * @returns The attributes to spread onto the element.
73
+ */
69
74
  declare function getStateAttributes<State extends Record<string, unknown>>(state: State, mapping: StateAttributeMapping<State> | undefined): UnknownProps;
70
75
  /**
71
76
  * The common case: a boolean that emits one attribute when true and another when
@@ -98,14 +103,22 @@ interface UseRenderParams<State extends Record<string, unknown>> {
98
103
  render?: ReactElement | undefined;
99
104
  /** Tag rendered when `render` is not supplied. */
100
105
  defaultTagName: string;
101
- /** Prop objects to merge, in precedence order (later wins). */
102
- props: (UnknownProps | undefined)[];
106
+ /** The component's own props. */
107
+ props?: UnknownProps;
108
+ /**
109
+ * The consumer's props, spread from `...rest`.
110
+ *
111
+ * A separate slot rather than a position in a list: precedence is then a property of
112
+ * this signature rather than a rule each component has to remember. A component
113
+ * cannot put a consumer's props before its own, because there is nowhere to put them.
114
+ */
115
+ consumerProps?: UnknownProps;
103
116
  /** The part's state, projected onto the DOM through `stateAttributes`. */
104
117
  state?: State;
105
118
  /** How each state field becomes `data-*` attributes. */
106
119
  stateAttributes?: StateAttributeMapping<State>;
107
120
  }
108
- declare function useRender<State extends Record<string, unknown> = Record<string, never>>({ render, defaultTagName, props, state, stateAttributes, }: UseRenderParams<State>): ReactElement;
121
+ declare function useRender<State extends Record<string, unknown> = Record<string, never>>({ render, defaultTagName, props, consumerProps, state, stateAttributes, }: UseRenderParams<State>): ReactElement;
109
122
 
110
123
  /**
111
124
  * Supports both controlled and uncontrolled use of a single value.
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  getStateAttributes,
8
8
  mergeProps,
9
9
  useRender
10
- } from "./chunk-HWG3GMTO.js";
10
+ } from "./chunk-PYLPZVWQ.js";
11
11
  export {
12
12
  booleanAttribute,
13
13
  disabledAttribute,
@@ -67,14 +67,33 @@ function isSyntheticEvent(event) {
67
67
  return typeof event === "object" && event !== null && "nativeEvent" in event;
68
68
  }
69
69
  function makeEventPreventable(event) {
70
+ if (Object.prototype.hasOwnProperty.call(event, "preventComponentHandler")) return;
70
71
  const target = event;
71
- target.preventComponentHandler = () => {
72
+ const prevent = () => {
72
73
  target.componentHandlerPrevented = true;
73
74
  };
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
+ }
74
84
  }
75
85
  function isComponentHandlerPrevented(event) {
76
86
  return typeof event === "object" && event !== null && event.componentHandlerPrevented === true;
77
87
  }
88
+ function chainHandlers(earlier, value) {
89
+ return (...args) => {
90
+ const event = args[0];
91
+ if (isSyntheticEvent(event)) makeEventPreventable(event);
92
+ value(...args);
93
+ if (isComponentHandlerPrevented(event)) return;
94
+ earlier(...args);
95
+ };
96
+ }
78
97
  function mergeProps(...objects) {
79
98
  const merged = {};
80
99
  for (const props of objects) {
@@ -84,13 +103,7 @@ function mergeProps(...objects) {
84
103
  if (value === void 0) continue;
85
104
  if (isEventHandler(key) && typeof value === "function") {
86
105
  const earlier = merged[key];
87
- merged[key] = typeof earlier === "function" ? (...args) => {
88
- const event = args[0];
89
- if (isSyntheticEvent(event)) makeEventPreventable(event);
90
- value(...args);
91
- if (isComponentHandlerPrevented(event)) return;
92
- earlier(...args);
93
- } : value;
106
+ merged[key] = typeof earlier === "function" ? chainHandlers(earlier, value) : value;
94
107
  } else if (key === "className") {
95
108
  merged.className = merged.className ? `${String(merged.className)} ${String(value)}` : value;
96
109
  } else if (key === "style") {
@@ -253,11 +266,12 @@ function useRender({
253
266
  render,
254
267
  defaultTagName,
255
268
  props,
269
+ consumerProps,
256
270
  state,
257
271
  stateAttributes
258
272
  }) {
259
273
  const attributes = state ? getStateAttributes(state, stateAttributes) : {};
260
- const merged = mergeProps(attributes, ...props);
274
+ const merged = mergeProps(attributes, props, consumerProps);
261
275
  if ((0, import_react3.isValidElement)(render)) {
262
276
  return (0, import_react3.cloneElement)(render, mergeProps(merged, render.props));
263
277
  }
@@ -286,6 +300,7 @@ var import_jsx_runtime = require("react/jsx-runtime");
286
300
  function SwitchRoot({
287
301
  checked: checkedProp,
288
302
  defaultChecked,
303
+ nativeButton,
289
304
  onCheckedChange,
290
305
  disabled = false,
291
306
  name,
@@ -302,7 +317,7 @@ function SwitchRoot({
302
317
  state: "checked"
303
318
  });
304
319
  const state = (0, import_react5.useMemo)(() => ({ checked, disabled }), [checked, disabled]);
305
- const isNativeButton = render === void 0 || render.type === "button";
320
+ const isNativeButton = nativeButton ?? (render === void 0 || render.type === "button");
306
321
  const { props: consumerProps, ref: buttonRef } = useButton({
307
322
  disabled,
308
323
  native: isNativeButton,
@@ -314,22 +329,20 @@ function SwitchRoot({
314
329
  defaultTagName: "button",
315
330
  state,
316
331
  stateAttributes: switchStateAttributes,
317
- props: [
318
- {
319
- role: "switch",
320
- "aria-checked": checked,
321
- className,
322
- children,
323
- onClick() {
324
- if (disabled) return;
325
- const next = !checked;
326
- setChecked(next);
327
- onCheckedChange?.(next);
328
- }
329
- },
330
- consumerProps,
331
- { ref: buttonRef }
332
- ]
332
+ props: {
333
+ role: "switch",
334
+ "aria-checked": checked,
335
+ className,
336
+ children,
337
+ ref: buttonRef,
338
+ onClick() {
339
+ if (disabled) return;
340
+ const next = !checked;
341
+ setChecked(next);
342
+ onCheckedChange?.(next);
343
+ }
344
+ },
345
+ consumerProps
333
346
  });
334
347
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(SwitchRootContext.Provider, { value: state, children: [
335
348
  element,
@@ -350,7 +363,8 @@ function SwitchThumb({
350
363
  defaultTagName: "span",
351
364
  state,
352
365
  stateAttributes: switchStateAttributes,
353
- props: [{ "aria-hidden": true, className, children }, rest]
366
+ props: { "aria-hidden": true, className, children },
367
+ consumerProps: rest
354
368
  });
355
369
  }
356
370
  // Annotate the CommonJS export names for ESM import in node:
@@ -12,6 +12,15 @@ interface SwitchRootProps {
12
12
  checked?: boolean;
13
13
  /** Initial state when uncontrolled. Read once, at mount. */
14
14
  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;
15
24
  onCheckedChange?: (checked: boolean) => void;
16
25
  disabled?: boolean;
17
26
  /**
@@ -21,6 +30,16 @@ interface SwitchRootProps {
21
30
  name?: string;
22
31
  /** Value submitted when checked. Defaults to `"on"`, as a native checkbox does. */
23
32
  value?: string;
33
+ /**
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;
24
43
  className?: string;
25
44
  children?: ReactNode;
26
45
  /**
@@ -38,11 +57,11 @@ interface SwitchRootProps {
38
57
  * activation, and the disabled semantics for free. Per the WAI-ARIA switch pattern
39
58
  * it carries `role="switch"` and `aria-checked`.
40
59
  *
41
- * It has no accessible name of its own — wrap it in a `<label>`, or pass `aria-label`
42
- * or `aria-labelledby`. That is the consumer's decision, not something a headless
60
+ * 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
43
62
  * component should guess.
44
63
  */
45
- declare function SwitchRoot({ checked: checkedProp, defaultChecked, onCheckedChange, disabled, name, value, className, children, render, ...rest }: SwitchRootProps & Record<string, unknown>): react.JSX.Element;
64
+ declare function SwitchRoot({ checked: checkedProp, defaultChecked, nativeButton, onCheckedChange, disabled, name, value, className, children, render, ...rest }: SwitchRootProps & Record<string, unknown>): react.JSX.Element;
46
65
 
47
66
  interface SwitchThumbProps {
48
67
  className?: string;
@@ -12,6 +12,15 @@ interface SwitchRootProps {
12
12
  checked?: boolean;
13
13
  /** Initial state when uncontrolled. Read once, at mount. */
14
14
  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;
15
24
  onCheckedChange?: (checked: boolean) => void;
16
25
  disabled?: boolean;
17
26
  /**
@@ -21,6 +30,16 @@ interface SwitchRootProps {
21
30
  name?: string;
22
31
  /** Value submitted when checked. Defaults to `"on"`, as a native checkbox does. */
23
32
  value?: string;
33
+ /**
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;
24
43
  className?: string;
25
44
  children?: ReactNode;
26
45
  /**
@@ -38,11 +57,11 @@ interface SwitchRootProps {
38
57
  * activation, and the disabled semantics for free. Per the WAI-ARIA switch pattern
39
58
  * it carries `role="switch"` and `aria-checked`.
40
59
  *
41
- * It has no accessible name of its own — wrap it in a `<label>`, or pass `aria-label`
42
- * or `aria-labelledby`. That is the consumer's decision, not something a headless
60
+ * 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
43
62
  * component should guess.
44
63
  */
45
- declare function SwitchRoot({ checked: checkedProp, defaultChecked, onCheckedChange, disabled, name, value, className, children, render, ...rest }: SwitchRootProps & Record<string, unknown>): react.JSX.Element;
64
+ declare function SwitchRoot({ checked: checkedProp, defaultChecked, nativeButton, onCheckedChange, disabled, name, value, className, children, render, ...rest }: SwitchRootProps & Record<string, unknown>): react.JSX.Element;
46
65
 
47
66
  interface SwitchThumbProps {
48
67
  className?: string;
@@ -4,13 +4,13 @@ import {
4
4
  import {
5
5
  retractActivationProps,
6
6
  useButton
7
- } from "../chunk-2XSRAJHB.js";
7
+ } from "../chunk-TNMGZWHA.js";
8
8
  import {
9
9
  __export,
10
10
  booleanAttribute,
11
11
  disabledAttribute,
12
12
  useRender
13
- } from "../chunk-HWG3GMTO.js";
13
+ } from "../chunk-PYLPZVWQ.js";
14
14
 
15
15
  // src/switch/index.parts.ts
16
16
  var index_parts_exports = {};
@@ -44,6 +44,7 @@ import { jsx, jsxs } from "react/jsx-runtime";
44
44
  function SwitchRoot({
45
45
  checked: checkedProp,
46
46
  defaultChecked,
47
+ nativeButton,
47
48
  onCheckedChange,
48
49
  disabled = false,
49
50
  name,
@@ -60,7 +61,7 @@ function SwitchRoot({
60
61
  state: "checked"
61
62
  });
62
63
  const state = useMemo(() => ({ checked, disabled }), [checked, disabled]);
63
- const isNativeButton = render === void 0 || render.type === "button";
64
+ const isNativeButton = nativeButton ?? (render === void 0 || render.type === "button");
64
65
  const { props: consumerProps, ref: buttonRef } = useButton({
65
66
  disabled,
66
67
  native: isNativeButton,
@@ -72,22 +73,20 @@ function SwitchRoot({
72
73
  defaultTagName: "button",
73
74
  state,
74
75
  stateAttributes: switchStateAttributes,
75
- props: [
76
- {
77
- role: "switch",
78
- "aria-checked": checked,
79
- className,
80
- children,
81
- onClick() {
82
- if (disabled) return;
83
- const next = !checked;
84
- setChecked(next);
85
- onCheckedChange?.(next);
86
- }
87
- },
88
- consumerProps,
89
- { ref: buttonRef }
90
- ]
76
+ props: {
77
+ role: "switch",
78
+ "aria-checked": checked,
79
+ className,
80
+ children,
81
+ ref: buttonRef,
82
+ onClick() {
83
+ if (disabled) return;
84
+ const next = !checked;
85
+ setChecked(next);
86
+ onCheckedChange?.(next);
87
+ }
88
+ },
89
+ consumerProps
91
90
  });
92
91
  return /* @__PURE__ */ jsxs(SwitchRootContext.Provider, { value: state, children: [
93
92
  element,
@@ -108,7 +107,8 @@ function SwitchThumb({
108
107
  defaultTagName: "span",
109
108
  state,
110
109
  stateAttributes: switchStateAttributes,
111
- props: [{ "aria-hidden": true, className, children }, rest]
110
+ props: { "aria-hidden": true, className, children },
111
+ consumerProps: rest
112
112
  });
113
113
  }
114
114
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arun-dev/headless",
3
- "version": "1.0.0",
3
+ "version": "2.1.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",