@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.
@@ -1,16 +1,10 @@
1
1
  import {
2
2
  useControlled
3
3
  } from "../chunk-UEIB3I3T.js";
4
- import {
5
- retractActivationProps,
6
- useButton
7
- } from "../chunk-TNMGZWHA.js";
8
4
  import {
9
5
  __export,
10
- booleanAttribute,
11
- disabledAttribute,
12
6
  useRender
13
- } from "../chunk-PYLPZVWQ.js";
7
+ } from "../chunk-OQTWWZ4B.js";
14
8
 
15
9
  // src/switch/index.parts.ts
16
10
  var index_parts_exports = {};
@@ -19,8 +13,8 @@ __export(index_parts_exports, {
19
13
  Thumb: () => SwitchThumb
20
14
  });
21
15
 
22
- // src/switch/root/SwitchRoot.tsx
23
- import { cloneElement, useMemo } from "react";
16
+ // src/switch/SwitchRoot.tsx
17
+ import { useEffect, useMemo, useRef } from "react";
24
18
 
25
19
  // src/switch/SwitchRootContext.ts
26
20
  import { createContext, useContext } from "react";
@@ -33,18 +27,20 @@ function useSwitchRootContext() {
33
27
  return context;
34
28
  }
35
29
 
36
- // src/switch/stateAttributes.ts
37
- var switchStateAttributes = {
38
- checked: booleanAttribute("data-checked", "data-unchecked"),
39
- disabled: disabledAttribute
40
- };
30
+ // src/switch/switchDataAttributes.ts
31
+ function switchDataAttributes({ checked, disabled }) {
32
+ return {
33
+ "data-checked": checked ? "" : void 0,
34
+ "data-unchecked": checked ? void 0 : "",
35
+ "data-disabled": disabled ? "" : void 0
36
+ };
37
+ }
41
38
 
42
- // src/switch/root/SwitchRoot.tsx
39
+ // src/switch/SwitchRoot.tsx
43
40
  import { jsx, jsxs } from "react/jsx-runtime";
44
41
  function SwitchRoot({
45
42
  checked: checkedProp,
46
43
  defaultChecked,
47
- nativeButton,
48
44
  onCheckedChange,
49
45
  disabled = false,
50
46
  name,
@@ -61,53 +57,91 @@ function SwitchRoot({
61
57
  state: "checked"
62
58
  });
63
59
  const state = useMemo(() => ({ checked, disabled }), [checked, disabled]);
64
- const isNativeButton = nativeButton ?? (render === void 0 || render.type === "button");
65
- const { props: consumerProps, ref: buttonRef } = useButton({
66
- disabled,
67
- native: isNativeButton,
68
- props: rest
69
- });
70
- const safeRender = disabled && !isNativeButton && render !== void 0 ? cloneElement(render, retractActivationProps(render.props)) : render;
60
+ const elementRef = useRef(null);
61
+ const inputRef = useRef(null);
62
+ useNativeButtonWarning(elementRef);
63
+ useFormReset({ elementRef, inputRef, checked, setChecked, onCheckedChange });
71
64
  const element = useRender({
72
- render: safeRender,
65
+ render,
73
66
  defaultTagName: "button",
74
- state,
75
- stateAttributes: switchStateAttributes,
76
67
  props: {
68
+ // Without this a switch inside a form would submit it on every toggle.
69
+ type: "button",
77
70
  role: "switch",
78
71
  "aria-checked": checked,
72
+ // The platform suppresses activation, focus and the click handler below.
73
+ disabled: disabled || void 0,
74
+ ...switchDataAttributes(state),
79
75
  className,
80
76
  children,
81
- ref: buttonRef,
77
+ ref: elementRef,
82
78
  onClick() {
83
- if (disabled) return;
84
79
  const next = !checked;
85
80
  setChecked(next);
86
81
  onCheckedChange?.(next);
87
82
  }
88
83
  },
89
- consumerProps
84
+ consumerProps: rest
90
85
  });
91
86
  return /* @__PURE__ */ jsxs(SwitchRootContext.Provider, { value: state, children: [
92
87
  element,
93
- name !== void 0 && checked ? /* @__PURE__ */ jsx("input", { type: "hidden", name, value }) : null
88
+ name !== void 0 ? /* @__PURE__ */ jsx(
89
+ "input",
90
+ {
91
+ ref: inputRef,
92
+ type: "checkbox",
93
+ hidden: true,
94
+ readOnly: true,
95
+ name,
96
+ value,
97
+ checked,
98
+ disabled
99
+ }
100
+ ) : null
94
101
  ] });
95
102
  }
96
-
97
- // src/switch/thumb/SwitchThumb.tsx
98
- function SwitchThumb({
99
- className,
100
- children,
101
- render,
102
- ...rest
103
+ function useFormReset({
104
+ elementRef,
105
+ inputRef,
106
+ checked,
107
+ setChecked,
108
+ onCheckedChange
103
109
  }) {
110
+ const { current: initialChecked } = useRef(checked);
111
+ useEffect(() => {
112
+ const form = elementRef.current?.form;
113
+ if (!form) return;
114
+ function onReset(event) {
115
+ queueMicrotask(() => {
116
+ if (event.defaultPrevented) return;
117
+ if (inputRef.current) inputRef.current.checked = checked;
118
+ if (checked === initialChecked) return;
119
+ setChecked(initialChecked);
120
+ onCheckedChange?.(initialChecked);
121
+ });
122
+ }
123
+ form.addEventListener("reset", onReset);
124
+ return () => form.removeEventListener("reset", onReset);
125
+ }, [elementRef, inputRef, checked, setChecked, onCheckedChange, initialChecked]);
126
+ }
127
+ function useNativeButtonWarning(elementRef) {
128
+ useEffect(() => {
129
+ if (process.env.NODE_ENV === "production") return;
130
+ const element = elementRef.current;
131
+ if (!element || element.tagName === "BUTTON") return;
132
+ console.error(
133
+ `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.`
134
+ );
135
+ }, [elementRef]);
136
+ }
137
+
138
+ // src/switch/SwitchThumb.tsx
139
+ function SwitchThumb({ className, children, render, ...rest }) {
104
140
  const state = useSwitchRootContext();
105
141
  return useRender({
106
142
  render,
107
143
  defaultTagName: "span",
108
- state,
109
- stateAttributes: switchStateAttributes,
110
- props: { "aria-hidden": true, className, children },
144
+ props: { "aria-hidden": true, ...switchDataAttributes(state), className, children },
111
145
  consumerProps: rest
112
146
  });
113
147
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arun-dev/headless",
3
- "version": "2.1.0",
3
+ "version": "3.0.1",
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": "3.2.6",
82
+ "vitest": "4.1.11",
83
83
  "@arun-dev/config": "0.0.0"
84
84
  },
85
85
  "scripts": {
@@ -1,96 +0,0 @@
1
- import {
2
- mergeProps
3
- } from "./chunk-PYLPZVWQ.js";
4
-
5
- // src/useButton.ts
6
- import { useEffect, useRef } from "react";
7
- var ACTIVATION_HANDLERS = [
8
- "onClick",
9
- "onDoubleClick",
10
- "onMouseDown",
11
- "onMouseUp",
12
- "onPointerDown",
13
- "onPointerUp",
14
- "onTouchStart",
15
- "onTouchEnd",
16
- "onKeyDown",
17
- "onKeyUp",
18
- "onKeyPress"
19
- ];
20
- function activationHandlers() {
21
- return {
22
- onKeyDown(event) {
23
- if (event.target !== event.currentTarget || event.defaultPrevented) return;
24
- if (event.key === " ") {
25
- event.preventDefault();
26
- return;
27
- }
28
- if (event.key === "Enter") {
29
- event.preventDefault();
30
- click(event.currentTarget);
31
- }
32
- },
33
- onKeyUp(event) {
34
- if (event.target !== event.currentTarget || event.defaultPrevented) return;
35
- if (event.key === " ") click(event.currentTarget);
36
- }
37
- };
38
- }
39
- function click(element) {
40
- if (element && typeof element.click === "function") {
41
- element.click();
42
- }
43
- }
44
- function useButton({
45
- disabled = false,
46
- native,
47
- props = {}
48
- }) {
49
- const elementRef = useRef(null);
50
- useEffect(() => {
51
- if (process.env.NODE_ENV === "production") return;
52
- const element = elementRef.current;
53
- if (!element) return;
54
- const isButtonTag = element.tagName === "BUTTON";
55
- if (native && !isButtonTag) {
56
- console.error(
57
- `A component expected a native <button> but rendered <${element.tagName.toLowerCase()}>. Focus, keyboard activation and \`disabled\` will not behave natively.`
58
- );
59
- } else if (!native && isButtonTag) {
60
- console.error(
61
- "A component rendered a native <button> while treating it as a non-native element, so it carries synthesised attributes it does not need."
62
- );
63
- }
64
- }, [native]);
65
- return { props: buttonProps({ disabled, native, props }), ref: elementRef };
66
- }
67
- function buttonProps({ disabled, native, props }) {
68
- if (!disabled) {
69
- return native ? mergeProps({ type: "button" }, props) : mergeProps({ tabIndex: 0 }, activationHandlers(), props);
70
- }
71
- if (native) return mergeProps({ type: "button" }, props, { disabled: true, "data-disabled": "" });
72
- const sanitised = {};
73
- for (const key of Object.keys(props)) {
74
- if (key === "href" || ACTIVATION_HANDLERS.includes(key)) continue;
75
- sanitised[key] = props[key];
76
- }
77
- return {
78
- ...sanitised,
79
- "aria-disabled": true,
80
- "data-disabled": "",
81
- // Mirrors a native disabled button, which is not focusable.
82
- tabIndex: -1
83
- };
84
- }
85
- function retractActivationProps(props) {
86
- const overrides = {};
87
- for (const key of Object.keys(props)) {
88
- if (key === "href" || ACTIVATION_HANDLERS.includes(key)) overrides[key] = void 0;
89
- }
90
- return overrides;
91
- }
92
-
93
- export {
94
- useButton,
95
- retractActivationProps
96
- };