@arun-dev/headless 2.1.0 → 3.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/dist/index.d.cts CHANGED
@@ -24,6 +24,11 @@ type ComponentEvent<E = unknown> = E & {
24
24
  * `undefined` values are skipped, so an absent key on a later object never
25
25
  * clobbers a value set by an earlier one.
26
26
  *
27
+ * `children` is deliberately not special-cased: it is a plain value, so the last
28
+ * object to declare it wins. That is what lets a `render` element either inherit the
29
+ * component's children (it declares none, so the key is absent and skipped) or supply
30
+ * its own (it declares them, and they win like any other value).
31
+ *
27
32
  * **Event handlers run right to left** — the last object's handler first, the first
28
33
  * object's last. Since a component passes its own props first and the consumer's last,
29
34
  * the consumer's handler runs before the component's and can stop it:
@@ -37,73 +42,29 @@ type ComponentEvent<E = unknown> = E & {
37
42
  */
38
43
  declare function mergeProps(...objects: (UnknownProps | undefined)[]): UnknownProps;
39
44
 
40
- /**
41
- * Projects a component's state onto the DOM as `data-*` attributes.
42
- *
43
- * This is the contract between behaviour and styling. A headless component owns no
44
- * class names, so the only way CSS can react to `checked` or `disabled` is for the
45
- * state to be visible in the DOM:
46
- *
47
- * .switch[data-checked] { background: var(--switch-track-bg-checked); }
48
- *
49
- * Doing it through a declared mapping rather than by hand keeps the attributes
50
- * consistent across components — Base UI's Checkbox emits eleven of them, which is
51
- * not something anyone maintains by hand — and makes the state a component exposes
52
- * a single readable object rather than scattered JSX.
53
- */
54
- /**
55
- * Maps one state value to the attributes it should produce. Return `null` for
56
- * "no attribute". A single value may produce several, and mutually exclusive
57
- * attributes are how a third state stays addressable: with only `data-checked`,
58
- * `:not([data-checked])` would match both unchecked *and* indeterminate.
59
- */
60
- type StateAttributeMapping<State> = {
61
- [Key in keyof State]?: (value: State[Key]) => Record<string, string> | null;
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
- */
74
- declare function getStateAttributes<State extends Record<string, unknown>>(state: State, mapping: StateAttributeMapping<State> | undefined): UnknownProps;
75
- /**
76
- * The common case: a boolean that emits one attribute when true and another when
77
- * false, so both sides are addressable at equal specificity.
78
- *
79
- * checked: booleanAttribute('data-checked', 'data-unchecked')
80
- */
81
- declare function booleanAttribute(whenTrue: string, whenFalse?: string): (value: unknown) => Record<string, string> | null;
82
- /**
83
- * The shared spelling of the disabled state. Every component uses this rather than
84
- * writing the string again, so `[data-disabled]` means the same thing system-wide and
85
- * a typo cannot silently split the CSS contract.
86
- */
87
- declare const disabledAttribute: (value: unknown) => Record<string, string> | null;
88
-
89
45
  /**
90
46
  * Resolves what a component part actually renders.
91
47
  *
92
48
  * Every part goes through here, which is what makes the whole library consistent:
93
- * one place decides how props merge, how state reaches the DOM, and what element
94
- * comes out. A part supplies its default element, its state, and the props it wants;
95
- * the consumer can replace the element entirely via `render` without losing any of it.
49
+ * one place decides how props merge and what element comes out. A part supplies its
50
+ * default element and the props it wants; the consumer can replace the element
51
+ * entirely via `render` without losing any of it.
96
52
  *
97
53
  * Currently uses no hooks. The `use` prefix matches the API it will grow into
98
54
  * (memoised merging, so parts stop re-rendering their children needlessly) and keeps
99
55
  * call sites stable when that lands.
100
56
  */
101
- interface UseRenderParams<State extends Record<string, unknown>> {
102
- /** Element to render instead of the default. Props, className and ref are merged onto it. */
57
+ interface UseRenderParams {
58
+ /**
59
+ * Element to render instead of the default. Props, className and ref are merged onto it.
60
+ *
61
+ * Its own props are the last word, `children` included: an element with no children of
62
+ * its own takes the component's, and one with children keeps them.
63
+ */
103
64
  render?: ReactElement | undefined;
104
65
  /** Tag rendered when `render` is not supplied. */
105
66
  defaultTagName: string;
106
- /** The component's own props. */
67
+ /** The component's own props, including any `data-*` state attributes it emits. */
107
68
  props?: UnknownProps;
108
69
  /**
109
70
  * The consumer's props, spread from `...rest`.
@@ -113,12 +74,8 @@ interface UseRenderParams<State extends Record<string, unknown>> {
113
74
  * cannot put a consumer's props before its own, because there is nowhere to put them.
114
75
  */
115
76
  consumerProps?: UnknownProps;
116
- /** The part's state, projected onto the DOM through `stateAttributes`. */
117
- state?: State;
118
- /** How each state field becomes `data-*` attributes. */
119
- stateAttributes?: StateAttributeMapping<State>;
120
77
  }
121
- declare function useRender<State extends Record<string, unknown> = Record<string, never>>({ render, defaultTagName, props, consumerProps, state, stateAttributes, }: UseRenderParams<State>): ReactElement;
78
+ declare function useRender({ render, defaultTagName, props, consumerProps, }: UseRenderParams): ReactElement;
122
79
 
123
80
  /**
124
81
  * Supports both controlled and uncontrolled use of a single value.
@@ -138,4 +95,4 @@ declare function useControlled<T>({ controlled, default: defaultValue, name, sta
138
95
  state?: string;
139
96
  }): [T, (next: T) => void];
140
97
 
141
- export { type ComponentEvent, type StateAttributeMapping, type UnknownProps, type UseRenderParams, booleanAttribute, disabledAttribute, getStateAttributes, mergeProps, useControlled, useRender };
98
+ export { type ComponentEvent, type UnknownProps, type UseRenderParams, mergeProps, useControlled, useRender };
package/dist/index.d.ts CHANGED
@@ -24,6 +24,11 @@ type ComponentEvent<E = unknown> = E & {
24
24
  * `undefined` values are skipped, so an absent key on a later object never
25
25
  * clobbers a value set by an earlier one.
26
26
  *
27
+ * `children` is deliberately not special-cased: it is a plain value, so the last
28
+ * object to declare it wins. That is what lets a `render` element either inherit the
29
+ * component's children (it declares none, so the key is absent and skipped) or supply
30
+ * its own (it declares them, and they win like any other value).
31
+ *
27
32
  * **Event handlers run right to left** — the last object's handler first, the first
28
33
  * object's last. Since a component passes its own props first and the consumer's last,
29
34
  * the consumer's handler runs before the component's and can stop it:
@@ -37,73 +42,29 @@ type ComponentEvent<E = unknown> = E & {
37
42
  */
38
43
  declare function mergeProps(...objects: (UnknownProps | undefined)[]): UnknownProps;
39
44
 
40
- /**
41
- * Projects a component's state onto the DOM as `data-*` attributes.
42
- *
43
- * This is the contract between behaviour and styling. A headless component owns no
44
- * class names, so the only way CSS can react to `checked` or `disabled` is for the
45
- * state to be visible in the DOM:
46
- *
47
- * .switch[data-checked] { background: var(--switch-track-bg-checked); }
48
- *
49
- * Doing it through a declared mapping rather than by hand keeps the attributes
50
- * consistent across components — Base UI's Checkbox emits eleven of them, which is
51
- * not something anyone maintains by hand — and makes the state a component exposes
52
- * a single readable object rather than scattered JSX.
53
- */
54
- /**
55
- * Maps one state value to the attributes it should produce. Return `null` for
56
- * "no attribute". A single value may produce several, and mutually exclusive
57
- * attributes are how a third state stays addressable: with only `data-checked`,
58
- * `:not([data-checked])` would match both unchecked *and* indeterminate.
59
- */
60
- type StateAttributeMapping<State> = {
61
- [Key in keyof State]?: (value: State[Key]) => Record<string, string> | null;
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
- */
74
- declare function getStateAttributes<State extends Record<string, unknown>>(state: State, mapping: StateAttributeMapping<State> | undefined): UnknownProps;
75
- /**
76
- * The common case: a boolean that emits one attribute when true and another when
77
- * false, so both sides are addressable at equal specificity.
78
- *
79
- * checked: booleanAttribute('data-checked', 'data-unchecked')
80
- */
81
- declare function booleanAttribute(whenTrue: string, whenFalse?: string): (value: unknown) => Record<string, string> | null;
82
- /**
83
- * The shared spelling of the disabled state. Every component uses this rather than
84
- * writing the string again, so `[data-disabled]` means the same thing system-wide and
85
- * a typo cannot silently split the CSS contract.
86
- */
87
- declare const disabledAttribute: (value: unknown) => Record<string, string> | null;
88
-
89
45
  /**
90
46
  * Resolves what a component part actually renders.
91
47
  *
92
48
  * Every part goes through here, which is what makes the whole library consistent:
93
- * one place decides how props merge, how state reaches the DOM, and what element
94
- * comes out. A part supplies its default element, its state, and the props it wants;
95
- * the consumer can replace the element entirely via `render` without losing any of it.
49
+ * one place decides how props merge and what element comes out. A part supplies its
50
+ * default element and the props it wants; the consumer can replace the element
51
+ * entirely via `render` without losing any of it.
96
52
  *
97
53
  * Currently uses no hooks. The `use` prefix matches the API it will grow into
98
54
  * (memoised merging, so parts stop re-rendering their children needlessly) and keeps
99
55
  * call sites stable when that lands.
100
56
  */
101
- interface UseRenderParams<State extends Record<string, unknown>> {
102
- /** Element to render instead of the default. Props, className and ref are merged onto it. */
57
+ interface UseRenderParams {
58
+ /**
59
+ * Element to render instead of the default. Props, className and ref are merged onto it.
60
+ *
61
+ * Its own props are the last word, `children` included: an element with no children of
62
+ * its own takes the component's, and one with children keeps them.
63
+ */
103
64
  render?: ReactElement | undefined;
104
65
  /** Tag rendered when `render` is not supplied. */
105
66
  defaultTagName: string;
106
- /** The component's own props. */
67
+ /** The component's own props, including any `data-*` state attributes it emits. */
107
68
  props?: UnknownProps;
108
69
  /**
109
70
  * The consumer's props, spread from `...rest`.
@@ -113,12 +74,8 @@ interface UseRenderParams<State extends Record<string, unknown>> {
113
74
  * cannot put a consumer's props before its own, because there is nowhere to put them.
114
75
  */
115
76
  consumerProps?: UnknownProps;
116
- /** The part's state, projected onto the DOM through `stateAttributes`. */
117
- state?: State;
118
- /** How each state field becomes `data-*` attributes. */
119
- stateAttributes?: StateAttributeMapping<State>;
120
77
  }
121
- declare function useRender<State extends Record<string, unknown> = Record<string, never>>({ render, defaultTagName, props, consumerProps, state, stateAttributes, }: UseRenderParams<State>): ReactElement;
78
+ declare function useRender({ render, defaultTagName, props, consumerProps, }: UseRenderParams): ReactElement;
122
79
 
123
80
  /**
124
81
  * Supports both controlled and uncontrolled use of a single value.
@@ -138,4 +95,4 @@ declare function useControlled<T>({ controlled, default: defaultValue, name, sta
138
95
  state?: string;
139
96
  }): [T, (next: T) => void];
140
97
 
141
- export { type ComponentEvent, type StateAttributeMapping, type UnknownProps, type UseRenderParams, booleanAttribute, disabledAttribute, getStateAttributes, mergeProps, useControlled, useRender };
98
+ export { type ComponentEvent, type UnknownProps, type UseRenderParams, mergeProps, useControlled, useRender };
package/dist/index.js CHANGED
@@ -2,16 +2,10 @@ import {
2
2
  useControlled
3
3
  } from "./chunk-UEIB3I3T.js";
4
4
  import {
5
- booleanAttribute,
6
- disabledAttribute,
7
- getStateAttributes,
8
5
  mergeProps,
9
6
  useRender
10
- } from "./chunk-PYLPZVWQ.js";
7
+ } from "./chunk-OQTWWZ4B.js";
11
8
  export {
12
- booleanAttribute,
13
- disabledAttribute,
14
- getStateAttributes,
15
9
  mergeProps,
16
10
  useControlled,
17
11
  useRender
@@ -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,55 @@ 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 = useNativeButtonWarning();
327
201
  const element = useRender({
328
- render: safeRender,
202
+ render,
329
203
  defaultTagName: "button",
330
- state,
331
- stateAttributes: switchStateAttributes,
332
204
  props: {
205
+ // Without this a switch inside a form would submit it on every toggle.
206
+ type: "button",
333
207
  role: "switch",
334
208
  "aria-checked": checked,
209
+ // The platform suppresses activation, focus and the click handler below.
210
+ disabled: disabled || void 0,
211
+ ...switchDataAttributes(state),
335
212
  className,
336
213
  children,
337
- ref: buttonRef,
214
+ ref: elementRef,
338
215
  onClick() {
339
- if (disabled) return;
340
216
  const next = !checked;
341
217
  setChecked(next);
342
218
  onCheckedChange?.(next);
343
219
  }
344
220
  },
345
- consumerProps
221
+ consumerProps: rest
346
222
  });
347
223
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(SwitchRootContext.Provider, { value: state, children: [
348
224
  element,
349
- name !== void 0 && checked ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { type: "hidden", name, value }) : null
225
+ name !== void 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { type: "checkbox", hidden: true, readOnly: true, name, value, checked }) : null
350
226
  ] });
351
227
  }
228
+ function useNativeButtonWarning() {
229
+ const elementRef = (0, import_react4.useRef)(null);
230
+ (0, import_react4.useEffect)(() => {
231
+ if (process.env.NODE_ENV === "production") return;
232
+ const element = elementRef.current;
233
+ if (!element || element.tagName === "BUTTON") return;
234
+ console.error(
235
+ `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.`
236
+ );
237
+ }, []);
238
+ return elementRef;
239
+ }
352
240
 
353
- // src/switch/thumb/SwitchThumb.tsx
354
- function SwitchThumb({
355
- className,
356
- children,
357
- render,
358
- ...rest
359
- }) {
241
+ // src/switch/SwitchThumb.tsx
242
+ function SwitchThumb({ className, children, render, ...rest }) {
360
243
  const state = useSwitchRootContext();
361
244
  return useRender({
362
245
  render,
363
246
  defaultTagName: "span",
364
- state,
365
- stateAttributes: switchStateAttributes,
366
- props: { "aria-hidden": true, className, children },
247
+ props: { "aria-hidden": true, ...switchDataAttributes(state), className, children },
367
248
  consumerProps: rest
368
249
  });
369
250
  }