@arun-dev/headless 4.1.0 → 4.2.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/radio-group/index.cjs +298 -0
- package/dist/radio-group/index.d.cts +128 -0
- package/dist/radio-group/index.d.ts +128 -0
- package/dist/radio-group/index.js +157 -0
- package/package.json +11 -1
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/radio-group/index.ts
|
|
21
|
+
var radio_group_exports = {};
|
|
22
|
+
__export(radio_group_exports, {
|
|
23
|
+
RadioGroup: () => index_parts_exports
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(radio_group_exports);
|
|
26
|
+
|
|
27
|
+
// src/radio-group/index.parts.ts
|
|
28
|
+
var index_parts_exports = {};
|
|
29
|
+
__export(index_parts_exports, {
|
|
30
|
+
Item: () => RadioGroupItem,
|
|
31
|
+
Root: () => RadioGroupRoot
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// src/radio-group/RadioGroupRoot.tsx
|
|
35
|
+
var import_react4 = require("react");
|
|
36
|
+
|
|
37
|
+
// src/core/useControlled.ts
|
|
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");
|
|
72
|
+
|
|
73
|
+
// src/core/mergeProps.ts
|
|
74
|
+
function isEventHandler(key) {
|
|
75
|
+
return key.length > 2 && key.startsWith("on") && key[2] === key[2]?.toUpperCase();
|
|
76
|
+
}
|
|
77
|
+
function mergeRefs(...refs) {
|
|
78
|
+
return (node) => {
|
|
79
|
+
const cleanups = refs.map((ref) => {
|
|
80
|
+
if (typeof ref === "function") {
|
|
81
|
+
const result = ref(node);
|
|
82
|
+
return typeof result === "function" ? result : () => ref(null);
|
|
83
|
+
}
|
|
84
|
+
if (ref && typeof ref === "object") {
|
|
85
|
+
ref.current = node;
|
|
86
|
+
return () => {
|
|
87
|
+
ref.current = null;
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
return void 0;
|
|
91
|
+
});
|
|
92
|
+
return () => {
|
|
93
|
+
cleanups.forEach((cleanup) => {
|
|
94
|
+
if (typeof cleanup === "function") cleanup();
|
|
95
|
+
});
|
|
96
|
+
};
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function isSyntheticEvent(event) {
|
|
100
|
+
return typeof event === "object" && event !== null && "nativeEvent" in event;
|
|
101
|
+
}
|
|
102
|
+
function makeEventPreventable(event) {
|
|
103
|
+
if (Object.prototype.hasOwnProperty.call(event, "preventComponentHandler")) return;
|
|
104
|
+
const target = event;
|
|
105
|
+
target.preventComponentHandler = () => {
|
|
106
|
+
target.componentHandlerPrevented = true;
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
function isComponentHandlerPrevented(event) {
|
|
110
|
+
return typeof event === "object" && event !== null && event.componentHandlerPrevented === true;
|
|
111
|
+
}
|
|
112
|
+
function chainHandlers(earlier, value) {
|
|
113
|
+
return (...args) => {
|
|
114
|
+
const event = args[0];
|
|
115
|
+
if (isSyntheticEvent(event)) makeEventPreventable(event);
|
|
116
|
+
value(...args);
|
|
117
|
+
if (isComponentHandlerPrevented(event)) return;
|
|
118
|
+
earlier(...args);
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
function mergeProps(...objects) {
|
|
122
|
+
const merged = {};
|
|
123
|
+
for (const props of objects) {
|
|
124
|
+
if (!props) continue;
|
|
125
|
+
for (const key of Object.keys(props)) {
|
|
126
|
+
const value = props[key];
|
|
127
|
+
if (value === void 0) continue;
|
|
128
|
+
if (isEventHandler(key) && typeof value === "function") {
|
|
129
|
+
const earlier = merged[key];
|
|
130
|
+
merged[key] = typeof earlier === "function" ? chainHandlers(earlier, value) : value;
|
|
131
|
+
} else if (key === "className") {
|
|
132
|
+
merged.className = merged.className ? `${String(merged.className)} ${String(value)}` : value;
|
|
133
|
+
} else if (key === "style") {
|
|
134
|
+
merged.style = { ...merged.style, ...value };
|
|
135
|
+
} else if (key === "ref") {
|
|
136
|
+
merged.ref = merged.ref ? mergeRefs(merged.ref, value) : value;
|
|
137
|
+
} else {
|
|
138
|
+
merged[key] = value;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return merged;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// src/core/useRender.ts
|
|
146
|
+
function useRender({
|
|
147
|
+
render,
|
|
148
|
+
defaultTagName,
|
|
149
|
+
props,
|
|
150
|
+
consumerProps
|
|
151
|
+
}) {
|
|
152
|
+
const merged = mergeProps(props, consumerProps);
|
|
153
|
+
if ((0, import_react2.isValidElement)(render)) {
|
|
154
|
+
return (0, import_react2.cloneElement)(render, mergeProps(merged, render.props));
|
|
155
|
+
}
|
|
156
|
+
return (0, import_react2.createElement)(defaultTagName, merged);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// src/radio-group/RadioGroupRootContext.ts
|
|
160
|
+
var import_react3 = require("react");
|
|
161
|
+
var RadioGroupRootContext = (0, import_react3.createContext)(null);
|
|
162
|
+
function useRadioGroupRootContext() {
|
|
163
|
+
const context = (0, import_react3.useContext)(RadioGroupRootContext);
|
|
164
|
+
if (context === null) {
|
|
165
|
+
throw new Error("<RadioGroup.Item> must be rendered inside <RadioGroup.Root>.");
|
|
166
|
+
}
|
|
167
|
+
return context;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// src/radio-group/radioGroupDataAttributes.ts
|
|
171
|
+
function radioGroupItemDataAttributes({ checked, disabled }) {
|
|
172
|
+
return {
|
|
173
|
+
"data-checked": checked ? "" : void 0,
|
|
174
|
+
"data-unchecked": checked ? void 0 : "",
|
|
175
|
+
"data-disabled": disabled ? "" : void 0
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
function radioGroupDataAttributes({ disabled }) {
|
|
179
|
+
return {
|
|
180
|
+
"data-disabled": disabled ? "" : void 0
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// src/radio-group/RadioGroupRoot.tsx
|
|
185
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
186
|
+
function radiosIn(root, name) {
|
|
187
|
+
if (!root) return [];
|
|
188
|
+
return [...root.querySelectorAll('input[type="radio"]')].filter(
|
|
189
|
+
(input) => input.name === name
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
function RadioGroupRoot({
|
|
193
|
+
value: valueProp,
|
|
194
|
+
defaultValue,
|
|
195
|
+
onValueChange,
|
|
196
|
+
name: nameProp,
|
|
197
|
+
disabled = false,
|
|
198
|
+
required = false,
|
|
199
|
+
form,
|
|
200
|
+
className,
|
|
201
|
+
children,
|
|
202
|
+
render,
|
|
203
|
+
...rest
|
|
204
|
+
}) {
|
|
205
|
+
const [value, setValue] = useControlled({
|
|
206
|
+
controlled: valueProp,
|
|
207
|
+
default: defaultValue ?? null,
|
|
208
|
+
name: "RadioGroup.Root",
|
|
209
|
+
state: "value"
|
|
210
|
+
});
|
|
211
|
+
const generatedName = (0, import_react4.useId)();
|
|
212
|
+
const name = nameProp ?? generatedName;
|
|
213
|
+
const commitValue = (0, import_react4.useCallback)(
|
|
214
|
+
(next) => {
|
|
215
|
+
setValue(next);
|
|
216
|
+
onValueChange?.(next);
|
|
217
|
+
},
|
|
218
|
+
[setValue, onValueChange]
|
|
219
|
+
);
|
|
220
|
+
const elementRef = (0, import_react4.useRef)(null);
|
|
221
|
+
const { current: initialValue } = (0, import_react4.useRef)(value);
|
|
222
|
+
(0, import_react4.useEffect)(() => {
|
|
223
|
+
const owner = radiosIn(elementRef.current, name)[0]?.form;
|
|
224
|
+
if (!owner) return;
|
|
225
|
+
function onReset(event) {
|
|
226
|
+
queueMicrotask(() => {
|
|
227
|
+
if (event.defaultPrevented) return;
|
|
228
|
+
for (const input of radiosIn(elementRef.current, name)) {
|
|
229
|
+
input.checked = input.value === value;
|
|
230
|
+
}
|
|
231
|
+
if (value === initialValue) return;
|
|
232
|
+
commitValue(initialValue);
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
owner.addEventListener("reset", onReset);
|
|
236
|
+
return () => owner.removeEventListener("reset", onReset);
|
|
237
|
+
}, [name, value, commitValue, initialValue]);
|
|
238
|
+
const state = (0, import_react4.useMemo)(
|
|
239
|
+
() => ({ value, disabled, required, name, form, select: commitValue }),
|
|
240
|
+
[value, disabled, required, name, form, commitValue]
|
|
241
|
+
);
|
|
242
|
+
const element = useRender({
|
|
243
|
+
render,
|
|
244
|
+
defaultTagName: "div",
|
|
245
|
+
props: {
|
|
246
|
+
role: "radiogroup",
|
|
247
|
+
"aria-disabled": disabled || void 0,
|
|
248
|
+
"aria-required": required || void 0,
|
|
249
|
+
...radioGroupDataAttributes(state),
|
|
250
|
+
className,
|
|
251
|
+
children,
|
|
252
|
+
ref: elementRef
|
|
253
|
+
},
|
|
254
|
+
consumerProps: rest
|
|
255
|
+
});
|
|
256
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(RadioGroupRootContext.Provider, { value: state, children: element });
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// src/radio-group/RadioGroupItem.tsx
|
|
260
|
+
var import_react5 = require("react");
|
|
261
|
+
function RadioGroupItem({
|
|
262
|
+
value,
|
|
263
|
+
disabled: disabledProp = false,
|
|
264
|
+
className,
|
|
265
|
+
render,
|
|
266
|
+
...rest
|
|
267
|
+
}) {
|
|
268
|
+
const group = useRadioGroupRootContext();
|
|
269
|
+
const disabled = group.disabled || disabledProp;
|
|
270
|
+
const checked = group.value === value;
|
|
271
|
+
const state = (0, import_react5.useMemo)(() => ({ checked, disabled }), [checked, disabled]);
|
|
272
|
+
return useRender({
|
|
273
|
+
render,
|
|
274
|
+
defaultTagName: "input",
|
|
275
|
+
props: {
|
|
276
|
+
type: "radio",
|
|
277
|
+
name: group.name,
|
|
278
|
+
value,
|
|
279
|
+
// Controlled from the group's value, so React restores it after any change the
|
|
280
|
+
// group declines — the platform never gets the last word on what is checked.
|
|
281
|
+
checked,
|
|
282
|
+
form: group.form,
|
|
283
|
+
required: group.required || void 0,
|
|
284
|
+
disabled: disabled || void 0,
|
|
285
|
+
...radioGroupItemDataAttributes(state),
|
|
286
|
+
className,
|
|
287
|
+
onChange() {
|
|
288
|
+
if (disabled) return;
|
|
289
|
+
group.select(value);
|
|
290
|
+
}
|
|
291
|
+
},
|
|
292
|
+
consumerProps: rest
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
296
|
+
0 && (module.exports = {
|
|
297
|
+
RadioGroup
|
|
298
|
+
});
|
|
@@ -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,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.
|
|
3
|
+
"version": "4.2.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,16 @@
|
|
|
54
54
|
"default": "./dist/checkbox/index.cjs"
|
|
55
55
|
}
|
|
56
56
|
},
|
|
57
|
+
"./radio-group": {
|
|
58
|
+
"import": {
|
|
59
|
+
"types": "./dist/radio-group/index.d.ts",
|
|
60
|
+
"default": "./dist/radio-group/index.js"
|
|
61
|
+
},
|
|
62
|
+
"require": {
|
|
63
|
+
"types": "./dist/radio-group/index.d.cts",
|
|
64
|
+
"default": "./dist/radio-group/index.cjs"
|
|
65
|
+
}
|
|
66
|
+
},
|
|
57
67
|
"./switch": {
|
|
58
68
|
"import": {
|
|
59
69
|
"types": "./dist/switch/index.d.ts",
|