@arun-dev/headless 2.0.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.
@@ -1,7 +1,16 @@
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>`: the rendered `<button>` is not named implicitly by a wrapping
11
+ * `<label>`, and `jsx-a11y/label-has-associated-control` rejects it as a nested control.
12
+ */
13
+ type SwitchRootOwnProps = {
5
14
  /**
6
15
  * Controlled state. Provide `onCheckedChange` alongside it.
7
16
  *
@@ -12,15 +21,6 @@ interface SwitchRootProps {
12
21
  checked?: boolean;
13
22
  /** Initial state when uncontrolled. Read once, at mount. */
14
23
  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
24
  onCheckedChange?: (checked: boolean) => void;
25
25
  disabled?: boolean;
26
26
  /**
@@ -30,36 +30,44 @@ interface SwitchRootProps {
30
30
  name?: string;
31
31
  /** Value submitted when checked. Defaults to `"on"`, as a native checkbox does. */
32
32
  value?: string;
33
- className?: string;
34
- children?: ReactNode;
35
33
  /**
36
- * Element to render instead of the default `<button>`. Props, className, event
34
+ * Component to render instead of the default `<button>`. Props, className, event
37
35
  * handlers and ref are merged onto it.
36
+ *
37
+ * It must render a native `<button>` — a wrapper such as `<Tooltip.Trigger />` that
38
+ * forwards its props to one. Anything else is reported in development.
38
39
  */
39
40
  render?: ReactElement;
41
+ /** Ref to the rendered element. Merged with any ref on the `render` element. */
40
42
  ref?: Ref<HTMLElement>;
41
- }
43
+ };
44
+ type SwitchRootProps = SwitchRootOwnProps & Omit<ComponentPropsWithRef<'button'>, keyof SwitchRootOwnProps>;
42
45
  /**
43
46
  * A switch — an immediate on/off control, distinct from a checkbox in that it takes
44
47
  * effect at once rather than on submit.
45
48
  *
46
- * Renders a native `<button>`, which supplies focusability, Space and Enter
47
- * activation, and the disabled semantics for free. Per the WAI-ARIA switch pattern
48
- * it carries `role="switch"` and `aria-checked`.
49
+ * Always a native `<button>`, which is the whole reason this component is short: the
50
+ * platform supplies focusability, Space and Enter activation, and `disabled`, so none
51
+ * of it is synthesised here. Per the WAI-ARIA switch pattern it carries `role="switch"`
52
+ * and `aria-checked`.
49
53
  *
50
- * It has no accessible name of its own — wrap it in a `<label>`, or pass `aria-label`
51
- * or `aria-labelledby`. That is the consumer's decision, not something a headless
52
- * component should guess.
54
+ * It has no accessible name of its own — pair it with a `<label htmlFor>` by `id`, or
55
+ * pass `aria-label` or `aria-labelledby`. That is the consumer's decision, not one a
56
+ * headless component should guess.
53
57
  */
54
- declare function SwitchRoot({ checked: checkedProp, defaultChecked, nativeButton, onCheckedChange, disabled, name, value, className, children, render, ...rest }: SwitchRootProps & Record<string, unknown>): react.JSX.Element;
58
+ declare function SwitchRoot({ checked: checkedProp, defaultChecked, onCheckedChange, disabled, name, value, className, children, render, ...rest }: SwitchRootProps): react.JSX.Element;
55
59
 
56
- interface SwitchThumbProps {
57
- className?: string;
58
- children?: ReactNode;
60
+ /**
61
+ * Switch.Thumb's own props. Everything else — `id`, `className`, `children`, `aria-*`,
62
+ * `data-*` — comes from React's own `<span>` props.
63
+ */
64
+ type SwitchThumbOwnProps = {
59
65
  /** Element to render instead of the default `<span>`. */
60
66
  render?: ReactElement;
67
+ /** Ref to the rendered element, whatever `render` makes it. */
61
68
  ref?: Ref<HTMLElement>;
62
- }
69
+ };
70
+ type SwitchThumbProps = SwitchThumbOwnProps & Omit<ComponentPropsWithRef<'span'>, keyof SwitchThumbOwnProps>;
63
71
  /**
64
72
  * The moving part of the switch.
65
73
  *
@@ -73,7 +81,7 @@ interface SwitchThumbProps {
73
81
  * Purely presentational — hidden from assistive technology, since the Root already
74
82
  * announces the state.
75
83
  */
76
- declare function SwitchThumb({ className, children, render, ...rest }: SwitchThumbProps & Record<string, unknown>): ReactElement<unknown, string | react.JSXElementConstructor<any>>;
84
+ declare function SwitchThumb({ className, children, render, ...rest }: SwitchThumbProps): ReactElement<unknown, string | react.JSXElementConstructor<any>>;
77
85
 
78
86
  declare namespace index_parts {
79
87
  export { SwitchRoot as Root, SwitchThumb as Thumb };
@@ -1,7 +1,16 @@
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>`: the rendered `<button>` is not named implicitly by a wrapping
11
+ * `<label>`, and `jsx-a11y/label-has-associated-control` rejects it as a nested control.
12
+ */
13
+ type SwitchRootOwnProps = {
5
14
  /**
6
15
  * Controlled state. Provide `onCheckedChange` alongside it.
7
16
  *
@@ -12,15 +21,6 @@ interface SwitchRootProps {
12
21
  checked?: boolean;
13
22
  /** Initial state when uncontrolled. Read once, at mount. */
14
23
  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
24
  onCheckedChange?: (checked: boolean) => void;
25
25
  disabled?: boolean;
26
26
  /**
@@ -30,36 +30,44 @@ interface SwitchRootProps {
30
30
  name?: string;
31
31
  /** Value submitted when checked. Defaults to `"on"`, as a native checkbox does. */
32
32
  value?: string;
33
- className?: string;
34
- children?: ReactNode;
35
33
  /**
36
- * Element to render instead of the default `<button>`. Props, className, event
34
+ * Component to render instead of the default `<button>`. Props, className, event
37
35
  * handlers and ref are merged onto it.
36
+ *
37
+ * It must render a native `<button>` — a wrapper such as `<Tooltip.Trigger />` that
38
+ * forwards its props to one. Anything else is reported in development.
38
39
  */
39
40
  render?: ReactElement;
41
+ /** Ref to the rendered element. Merged with any ref on the `render` element. */
40
42
  ref?: Ref<HTMLElement>;
41
- }
43
+ };
44
+ type SwitchRootProps = SwitchRootOwnProps & Omit<ComponentPropsWithRef<'button'>, keyof SwitchRootOwnProps>;
42
45
  /**
43
46
  * A switch — an immediate on/off control, distinct from a checkbox in that it takes
44
47
  * effect at once rather than on submit.
45
48
  *
46
- * Renders a native `<button>`, which supplies focusability, Space and Enter
47
- * activation, and the disabled semantics for free. Per the WAI-ARIA switch pattern
48
- * it carries `role="switch"` and `aria-checked`.
49
+ * Always a native `<button>`, which is the whole reason this component is short: the
50
+ * platform supplies focusability, Space and Enter activation, and `disabled`, so none
51
+ * of it is synthesised here. Per the WAI-ARIA switch pattern it carries `role="switch"`
52
+ * and `aria-checked`.
49
53
  *
50
- * It has no accessible name of its own — wrap it in a `<label>`, or pass `aria-label`
51
- * or `aria-labelledby`. That is the consumer's decision, not something a headless
52
- * component should guess.
54
+ * It has no accessible name of its own — pair it with a `<label htmlFor>` by `id`, or
55
+ * pass `aria-label` or `aria-labelledby`. That is the consumer's decision, not one a
56
+ * headless component should guess.
53
57
  */
54
- declare function SwitchRoot({ checked: checkedProp, defaultChecked, nativeButton, onCheckedChange, disabled, name, value, className, children, render, ...rest }: SwitchRootProps & Record<string, unknown>): react.JSX.Element;
58
+ declare function SwitchRoot({ checked: checkedProp, defaultChecked, onCheckedChange, disabled, name, value, className, children, render, ...rest }: SwitchRootProps): react.JSX.Element;
55
59
 
56
- interface SwitchThumbProps {
57
- className?: string;
58
- children?: ReactNode;
60
+ /**
61
+ * Switch.Thumb's own props. Everything else — `id`, `className`, `children`, `aria-*`,
62
+ * `data-*` — comes from React's own `<span>` props.
63
+ */
64
+ type SwitchThumbOwnProps = {
59
65
  /** Element to render instead of the default `<span>`. */
60
66
  render?: ReactElement;
67
+ /** Ref to the rendered element, whatever `render` makes it. */
61
68
  ref?: Ref<HTMLElement>;
62
- }
69
+ };
70
+ type SwitchThumbProps = SwitchThumbOwnProps & Omit<ComponentPropsWithRef<'span'>, keyof SwitchThumbOwnProps>;
63
71
  /**
64
72
  * The moving part of the switch.
65
73
  *
@@ -73,7 +81,7 @@ interface SwitchThumbProps {
73
81
  * Purely presentational — hidden from assistive technology, since the Root already
74
82
  * announces the state.
75
83
  */
76
- declare function SwitchThumb({ className, children, render, ...rest }: SwitchThumbProps & Record<string, unknown>): ReactElement<unknown, string | react.JSXElementConstructor<any>>;
84
+ declare function SwitchThumb({ className, children, render, ...rest }: SwitchThumbProps): ReactElement<unknown, string | react.JSXElementConstructor<any>>;
77
85
 
78
86
  declare namespace index_parts {
79
87
  export { SwitchRoot as Root, SwitchThumb as Thumb };
@@ -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-ER7IAVR5.js";
8
4
  import {
9
5
  __export,
10
- booleanAttribute,
11
- disabledAttribute,
12
6
  useRender
13
- } from "../chunk-IGKX2XWC.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,54 @@ 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 = useNativeButtonWarning();
71
61
  const element = useRender({
72
- render: safeRender,
62
+ render,
73
63
  defaultTagName: "button",
74
- state,
75
- stateAttributes: switchStateAttributes,
76
64
  props: {
65
+ // Without this a switch inside a form would submit it on every toggle.
66
+ type: "button",
77
67
  role: "switch",
78
68
  "aria-checked": checked,
69
+ // The platform suppresses activation, focus and the click handler below.
70
+ disabled: disabled || void 0,
71
+ ...switchDataAttributes(state),
79
72
  className,
80
73
  children,
81
- ref: buttonRef,
74
+ ref: elementRef,
82
75
  onClick() {
83
- if (disabled) return;
84
76
  const next = !checked;
85
77
  setChecked(next);
86
78
  onCheckedChange?.(next);
87
79
  }
88
80
  },
89
- consumerProps
81
+ consumerProps: rest
90
82
  });
91
83
  return /* @__PURE__ */ jsxs(SwitchRootContext.Provider, { value: state, children: [
92
84
  element,
93
- name !== void 0 && checked ? /* @__PURE__ */ jsx("input", { type: "hidden", name, value }) : null
85
+ name !== void 0 ? /* @__PURE__ */ jsx("input", { type: "checkbox", hidden: true, readOnly: true, name, value, checked }) : null
94
86
  ] });
95
87
  }
88
+ function useNativeButtonWarning() {
89
+ const elementRef = useRef(null);
90
+ useEffect(() => {
91
+ if (process.env.NODE_ENV === "production") return;
92
+ const element = elementRef.current;
93
+ if (!element || element.tagName === "BUTTON") return;
94
+ console.error(
95
+ `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.`
96
+ );
97
+ }, []);
98
+ return elementRef;
99
+ }
96
100
 
97
- // src/switch/thumb/SwitchThumb.tsx
98
- function SwitchThumb({
99
- className,
100
- children,
101
- render,
102
- ...rest
103
- }) {
101
+ // src/switch/SwitchThumb.tsx
102
+ function SwitchThumb({ className, children, render, ...rest }) {
104
103
  const state = useSwitchRootContext();
105
104
  return useRender({
106
105
  render,
107
106
  defaultTagName: "span",
108
- state,
109
- stateAttributes: switchStateAttributes,
110
- props: { "aria-hidden": true, className, children },
107
+ props: { "aria-hidden": true, ...switchDataAttributes(state), className, children },
111
108
  consumerProps: rest
112
109
  });
113
110
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arun-dev/headless",
3
- "version": "2.0.0",
3
+ "version": "3.0.0",
4
4
  "description": "Unstyled React behaviour primitives — render engine, controlled/uncontrolled state, and data-* state projection. Ships no CSS and no class names.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -1,96 +0,0 @@
1
- import {
2
- mergeProps
3
- } from "./chunk-IGKX2XWC.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
- };