@arun-dev/headless 4.1.0 → 4.3.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.
@@ -0,0 +1,128 @@
1
+ import * as react from 'react';
2
+ import { ReactElement, Ref, ComponentPropsWithRef } from 'react';
3
+
4
+ /**
5
+ * RadioGroup.Root's own props. Everything else — `id`, `className`, `children`,
6
+ * `aria-*`, `data-*`, event handlers — comes from React's own `<div>` props.
7
+ *
8
+ * `aria-labelledby` in particular is how a group gets its accessible name, pointing at
9
+ * the visible caption. A `<fieldset>` with a `<legend>` does the same through `render`.
10
+ */
11
+ type RadioGroupRootOwnProps = {
12
+ /**
13
+ * Controlled value — the `value` of the selected item, or `null` for none. Provide
14
+ * `onValueChange` alongside it.
15
+ *
16
+ * Never `undefined` once mounted. The mode is latched at mount, so an `undefined`
17
+ * first render makes the group uncontrolled for good. Use `null` for "nothing
18
+ * selected" — `value={x ?? null}`.
19
+ */
20
+ value?: string | null;
21
+ /** Initial value when uncontrolled. Read once, at mount. */
22
+ defaultValue?: string | null;
23
+ /**
24
+ * Called with the value being moved to, in both controlled and uncontrolled modes.
25
+ * A user can only move to a value; `null` arrives only from `form.reset()` returning
26
+ * a group that mounted with nothing selected.
27
+ */
28
+ onValueChange?: (value: string | null) => void;
29
+ /**
30
+ * The name every radio in the group shares, and submits under.
31
+ *
32
+ * The platform groups radios by name — that is what makes arrow keys move between
33
+ * them and keeps one checked at a time — so one is generated when this is omitted.
34
+ * A generated name still submits: pass your own when the group is inside a form.
35
+ */
36
+ name?: string;
37
+ /** Disables every radio in the group. */
38
+ disabled?: boolean;
39
+ /** Blocks form submission until a radio is selected, via the platform's validation. */
40
+ required?: boolean;
41
+ /** Associates the radios with a `<form>` by id, when the group is rendered outside it. */
42
+ form?: string;
43
+ /**
44
+ * Component to render instead of the default `<div>` — a `<fieldset>`, say. Props,
45
+ * className, event handlers and ref are merged onto it.
46
+ */
47
+ render?: ReactElement;
48
+ /** Ref to the rendered element. Merged with any ref on the `render` element. */
49
+ ref?: Ref<HTMLElement>;
50
+ };
51
+ type RadioGroupRootProps = RadioGroupRootOwnProps & Omit<ComponentPropsWithRef<'div'>, keyof RadioGroupRootOwnProps>;
52
+ /**
53
+ * A set of radios, of which at most one is selected.
54
+ *
55
+ * The radios are native `<input type="radio">` elements — the native element passes all
56
+ * three of decision 7's tests, so nothing here rebuilds what the platform already does.
57
+ * Arrow keys move the selection, Tab enters the group at the checked radio, `required`
58
+ * blocks submission and the form submits the checked value, all without JavaScript.
59
+ *
60
+ * What the group adds is the one thing the platform leaves to the page: a single value
61
+ * in React. It owns that value, and every radio derives `checked` from it.
62
+ *
63
+ * Inside a form, `form.reset()` returns the group to the value it mounted with. The
64
+ * platform resets the inputs by itself, but React's value would not follow — the same
65
+ * seam `useFormReset` closes for Checkbox and Switch, closed here for a group.
66
+ */
67
+ declare function RadioGroupRoot({ value: valueProp, defaultValue, onValueChange, name: nameProp, disabled, required, form, className, children, render, ...rest }: RadioGroupRootProps): react.JSX.Element;
68
+
69
+ /**
70
+ * RadioGroup.Item's own props. Everything else — `id`, `className`, `aria-*`, `data-*`,
71
+ * event handlers — comes from React's own `<input>` props.
72
+ *
73
+ * `name`, `checked`, `defaultChecked`, `required` and `form` belong to the group and are
74
+ * not accepted here: a radio's checkedness is the group's value, not its own. `type` is
75
+ * always `radio`, and `children` is left out because an `<input>` cannot hold any.
76
+ */
77
+ type RadioGroupItemOwnProps = {
78
+ /** The group's value when this radio is selected, and what the form submits for it. */
79
+ value: string;
80
+ /** Disables this radio alone. A disabled group disables it regardless. */
81
+ disabled?: boolean;
82
+ /** Element to render instead of the default `<input type="radio">`. */
83
+ render?: ReactElement;
84
+ /** Ref to the rendered element. Merged with any ref on the `render` element. */
85
+ ref?: Ref<HTMLElement>;
86
+ };
87
+ type RadioGroupItemProps = RadioGroupItemOwnProps & Omit<ComponentPropsWithRef<'input'>, keyof RadioGroupItemOwnProps | 'type' | 'name' | 'checked' | 'defaultChecked' | 'required' | 'form' | 'children'>;
88
+ /**
89
+ * One radio — a native `<input type="radio">`.
90
+ *
91
+ * Void, so it has no parts: there is no indicator to hold, and the dot is drawn by CSS
92
+ * off `:checked` or the `data-*` attributes. Decision 11 — one element, one component.
93
+ *
94
+ * It has no accessible name of its own — wrap it in a `<label>`, pair it with one by
95
+ * `id`, or pass `aria-label`. A wrapping label names it natively, but
96
+ * jsx-a11y/label-has-associated-control cannot see through this component to the input,
97
+ * so under that rule pair it by `id` too.
98
+ */
99
+ declare function RadioGroupItem({ value, disabled: disabledProp, className, render, ...rest }: RadioGroupItemProps): ReactElement<unknown, string | react.JSXElementConstructor<any>>;
100
+
101
+ declare namespace index_parts {
102
+ export { RadioGroupItem as Item, RadioGroupRoot as Root };
103
+ }
104
+
105
+ /**
106
+ * The state RadioGroup.Root shares with its items.
107
+ *
108
+ * `value` is `null` when nothing is selected — a real state for a radio group, and the
109
+ * one a native group starts in. `null` rather than `undefined`, because `undefined` is
110
+ * what tells `useControlled` a group is uncontrolled.
111
+ */
112
+ type RadioGroupState = {
113
+ value: string | null;
114
+ disabled: boolean;
115
+ required: boolean;
116
+ /** Always set — the consumer's, or one generated so the radios still form a group. */
117
+ name: string;
118
+ form: string | undefined;
119
+ /** The group's single setter. Every change goes through it (decision 10, rule 5). */
120
+ select: (value: string) => void;
121
+ };
122
+ /** What one radio projects as `data-*` attributes. */
123
+ type RadioGroupItemState = {
124
+ checked: boolean;
125
+ disabled: boolean;
126
+ };
127
+
128
+ export { index_parts as RadioGroup, type RadioGroupItemProps, type RadioGroupItemState, type RadioGroupRootProps, type RadioGroupState };
@@ -0,0 +1,157 @@
1
+ import {
2
+ useControlled
3
+ } from "../chunk-UEIB3I3T.js";
4
+ import {
5
+ __export,
6
+ useRender
7
+ } from "../chunk-OQTWWZ4B.js";
8
+
9
+ // src/radio-group/index.parts.ts
10
+ var index_parts_exports = {};
11
+ __export(index_parts_exports, {
12
+ Item: () => RadioGroupItem,
13
+ Root: () => RadioGroupRoot
14
+ });
15
+
16
+ // src/radio-group/RadioGroupRoot.tsx
17
+ import { useCallback, useEffect, useId, useMemo, useRef } from "react";
18
+
19
+ // src/radio-group/RadioGroupRootContext.ts
20
+ import { createContext, useContext } from "react";
21
+ var RadioGroupRootContext = createContext(null);
22
+ function useRadioGroupRootContext() {
23
+ const context = useContext(RadioGroupRootContext);
24
+ if (context === null) {
25
+ throw new Error("<RadioGroup.Item> must be rendered inside <RadioGroup.Root>.");
26
+ }
27
+ return context;
28
+ }
29
+
30
+ // src/radio-group/radioGroupDataAttributes.ts
31
+ function radioGroupItemDataAttributes({ checked, disabled }) {
32
+ return {
33
+ "data-checked": checked ? "" : void 0,
34
+ "data-unchecked": checked ? void 0 : "",
35
+ "data-disabled": disabled ? "" : void 0
36
+ };
37
+ }
38
+ function radioGroupDataAttributes({ disabled }) {
39
+ return {
40
+ "data-disabled": disabled ? "" : void 0
41
+ };
42
+ }
43
+
44
+ // src/radio-group/RadioGroupRoot.tsx
45
+ import { jsx } from "react/jsx-runtime";
46
+ function radiosIn(root, name) {
47
+ if (!root) return [];
48
+ return [...root.querySelectorAll('input[type="radio"]')].filter(
49
+ (input) => input.name === name
50
+ );
51
+ }
52
+ function RadioGroupRoot({
53
+ value: valueProp,
54
+ defaultValue,
55
+ onValueChange,
56
+ name: nameProp,
57
+ disabled = false,
58
+ required = false,
59
+ form,
60
+ className,
61
+ children,
62
+ render,
63
+ ...rest
64
+ }) {
65
+ const [value, setValue] = useControlled({
66
+ controlled: valueProp,
67
+ default: defaultValue ?? null,
68
+ name: "RadioGroup.Root",
69
+ state: "value"
70
+ });
71
+ const generatedName = useId();
72
+ const name = nameProp ?? generatedName;
73
+ const commitValue = useCallback(
74
+ (next) => {
75
+ setValue(next);
76
+ onValueChange?.(next);
77
+ },
78
+ [setValue, onValueChange]
79
+ );
80
+ const elementRef = useRef(null);
81
+ const { current: initialValue } = useRef(value);
82
+ useEffect(() => {
83
+ const owner = radiosIn(elementRef.current, name)[0]?.form;
84
+ if (!owner) return;
85
+ function onReset(event) {
86
+ queueMicrotask(() => {
87
+ if (event.defaultPrevented) return;
88
+ for (const input of radiosIn(elementRef.current, name)) {
89
+ input.checked = input.value === value;
90
+ }
91
+ if (value === initialValue) return;
92
+ commitValue(initialValue);
93
+ });
94
+ }
95
+ owner.addEventListener("reset", onReset);
96
+ return () => owner.removeEventListener("reset", onReset);
97
+ }, [name, value, commitValue, initialValue]);
98
+ const state = useMemo(
99
+ () => ({ value, disabled, required, name, form, select: commitValue }),
100
+ [value, disabled, required, name, form, commitValue]
101
+ );
102
+ const element = useRender({
103
+ render,
104
+ defaultTagName: "div",
105
+ props: {
106
+ role: "radiogroup",
107
+ "aria-disabled": disabled || void 0,
108
+ "aria-required": required || void 0,
109
+ ...radioGroupDataAttributes(state),
110
+ className,
111
+ children,
112
+ ref: elementRef
113
+ },
114
+ consumerProps: rest
115
+ });
116
+ return /* @__PURE__ */ jsx(RadioGroupRootContext.Provider, { value: state, children: element });
117
+ }
118
+
119
+ // src/radio-group/RadioGroupItem.tsx
120
+ import { useMemo as useMemo2 } from "react";
121
+ function RadioGroupItem({
122
+ value,
123
+ disabled: disabledProp = false,
124
+ className,
125
+ render,
126
+ ...rest
127
+ }) {
128
+ const group = useRadioGroupRootContext();
129
+ const disabled = group.disabled || disabledProp;
130
+ const checked = group.value === value;
131
+ const state = useMemo2(() => ({ checked, disabled }), [checked, disabled]);
132
+ return useRender({
133
+ render,
134
+ defaultTagName: "input",
135
+ props: {
136
+ type: "radio",
137
+ name: group.name,
138
+ value,
139
+ // Controlled from the group's value, so React restores it after any change the
140
+ // group declines — the platform never gets the last word on what is checked.
141
+ checked,
142
+ form: group.form,
143
+ required: group.required || void 0,
144
+ disabled: disabled || void 0,
145
+ ...radioGroupItemDataAttributes(state),
146
+ className,
147
+ onChange() {
148
+ if (disabled) return;
149
+ group.select(value);
150
+ }
151
+ },
152
+ consumerProps: rest
153
+ });
154
+ }
155
+ export {
156
+ index_parts_exports as RadioGroup
157
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arun-dev/headless",
3
- "version": "4.1.0",
3
+ "version": "4.3.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",
@@ -54,6 +54,26 @@
54
54
  "default": "./dist/checkbox/index.cjs"
55
55
  }
56
56
  },
57
+ "./dialog": {
58
+ "import": {
59
+ "types": "./dist/dialog/index.d.ts",
60
+ "default": "./dist/dialog/index.js"
61
+ },
62
+ "require": {
63
+ "types": "./dist/dialog/index.d.cts",
64
+ "default": "./dist/dialog/index.cjs"
65
+ }
66
+ },
67
+ "./radio-group": {
68
+ "import": {
69
+ "types": "./dist/radio-group/index.d.ts",
70
+ "default": "./dist/radio-group/index.js"
71
+ },
72
+ "require": {
73
+ "types": "./dist/radio-group/index.d.cts",
74
+ "default": "./dist/radio-group/index.cjs"
75
+ }
76
+ },
57
77
  "./switch": {
58
78
  "import": {
59
79
  "types": "./dist/switch/index.d.ts",
@@ -83,8 +103,10 @@
83
103
  "@testing-library/react": "16.3.0",
84
104
  "@types/react": "19.2.18",
85
105
  "@types/react-dom": "19.2.5",
106
+ "@vitest/browser-playwright": "4.1.11",
86
107
  "eslint": "9.39.4",
87
108
  "jsdom": "27.0.0",
109
+ "playwright": "1.63.0",
88
110
  "react": "19.2.8",
89
111
  "react-dom": "19.2.8",
90
112
  "tsup": "8.5.1",
@@ -99,6 +121,7 @@
99
121
  "lint": "eslint .",
100
122
  "lint:fix": "eslint . --fix",
101
123
  "test": "pnpm run test:unit",
102
- "test:unit": "vitest run --passWithNoTests --config vitest.unit.config.ts"
124
+ "test:unit": "vitest run --passWithNoTests --config vitest.unit.config.ts",
125
+ "test:browser": "vitest run --config vitest.browser.config.ts"
103
126
  }
104
127
  }