@arun-dev/headless 0.3.0 → 1.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/README.md +29 -3
- package/dist/button/index.cjs +248 -0
- package/dist/button/index.d.cts +44 -0
- package/dist/button/index.d.ts +44 -0
- package/dist/button/index.js +32 -0
- package/dist/chunk-2XSRAJHB.js +96 -0
- package/dist/{chunk-LU4ZQE3V.js → chunk-HWG3GMTO.js} +19 -38
- package/dist/chunk-UEIB3I3T.js +36 -0
- package/dist/index.cjs +18 -45
- package/dist/index.d.cts +24 -23
- package/dist/index.d.ts +24 -23
- package/dist/index.js +4 -42
- package/dist/switch/index.cjs +161 -49
- package/dist/switch/index.d.cts +4 -3
- package/dist/switch/index.d.ts +4 -3
- package/dist/switch/index.js +20 -7
- package/package.json +11 -1
package/README.md
CHANGED
|
@@ -28,8 +28,20 @@ import { Switch } from '@arun-dev/headless/switch';
|
|
|
28
28
|
|
|
29
29
|
| Component | Parts | Props |
|
|
30
30
|
| --------- | ----------------------- | --------------------------------------------------------------------------- |
|
|
31
|
+
| `Button` | — | `disabled`, `nativeButton` |
|
|
31
32
|
| `Switch` | `Switch.Root`, `.Thumb` | `checked`, `defaultChecked`, `onCheckedChange`, `disabled`, `name`, `value` |
|
|
32
33
|
|
|
34
|
+
`Button` exists for the moment `render` points at something that is not a `<button>`. It then
|
|
35
|
+
supplies what the platform stops giving you — focus, `Enter` and `Space` activation, and a
|
|
36
|
+
`disabled` state that actually disables. It takes no `href`: a control that navigates should be an
|
|
37
|
+
anchor, so pass one in and keep middle-click, cmd-click and "link" in assistive technology.
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
import { Button } from '@arun-dev/headless/button';
|
|
41
|
+
|
|
42
|
+
<Button render={<a href="/docs" target="_blank" rel="noreferrer" />}>Docs</Button>;
|
|
43
|
+
```
|
|
44
|
+
|
|
33
45
|
`Switch.Root` renders a native `<button>`, so focus, `Space`, `Enter` and disabled semantics come
|
|
34
46
|
from the platform. It carries `role="switch"` and `aria-checked`, but **no accessible name** —
|
|
35
47
|
wrap it in a `<label>` or pass `aria-label`. A headless component should not guess at your copy.
|
|
@@ -72,9 +84,17 @@ Every part takes a `render` prop to change the element, and spreads unrecognised
|
|
|
72
84
|
<Switch.Root render={<Tooltip.Trigger />} />
|
|
73
85
|
```
|
|
74
86
|
|
|
75
|
-
`className` is concatenated,
|
|
76
|
-
|
|
77
|
-
|
|
87
|
+
`className` is concatenated, `style` is merged, and refs are merged — so a `ref` on the `render`
|
|
88
|
+
element and a `ref` on the component both receive the node.
|
|
89
|
+
|
|
90
|
+
Event handlers are chained rather than replaced, and **your handler runs before the component's**,
|
|
91
|
+
so you can stop it:
|
|
92
|
+
|
|
93
|
+
```tsx
|
|
94
|
+
<Switch.Root onClick={(event) => event.preventComponentHandler()} />
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
`preventDefault()` is left alone — it still means only "cancel the browser's default action".
|
|
78
98
|
|
|
79
99
|
## Engine
|
|
80
100
|
|
|
@@ -97,3 +117,9 @@ import {
|
|
|
97
117
|
| `mergeProps` | Merges prop objects: handlers chain, `className` concatenates, refs merge |
|
|
98
118
|
| `getStateAttributes` | Projects a state object onto `data-*` attributes via a declared mapping |
|
|
99
119
|
| `booleanAttribute` | The common mapping — one attribute when true, another when false |
|
|
120
|
+
| `disabledAttribute` | The shared spelling of `data-disabled`, so every component agrees on it |
|
|
121
|
+
| `ComponentEvent` | Type for a handler that can call `preventComponentHandler()` |
|
|
122
|
+
|
|
123
|
+
Everything this library uses to implement its own components stays private, so it can
|
|
124
|
+
change without breaking anyone. The public surface is the table above plus the
|
|
125
|
+
components themselves.
|
|
@@ -0,0 +1,248 @@
|
|
|
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/button/index.ts
|
|
21
|
+
var button_exports = {};
|
|
22
|
+
__export(button_exports, {
|
|
23
|
+
Button: () => Button
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(button_exports);
|
|
26
|
+
|
|
27
|
+
// src/button/Button.tsx
|
|
28
|
+
var import_react3 = require("react");
|
|
29
|
+
|
|
30
|
+
// src/core/useRender.ts
|
|
31
|
+
var import_react = require("react");
|
|
32
|
+
|
|
33
|
+
// src/core/mergeProps.ts
|
|
34
|
+
function isEventHandler(key) {
|
|
35
|
+
return key.length > 2 && key.startsWith("on") && key[2] === key[2]?.toUpperCase();
|
|
36
|
+
}
|
|
37
|
+
function mergeRefs(...refs) {
|
|
38
|
+
return (node) => {
|
|
39
|
+
const cleanups = refs.map((ref) => {
|
|
40
|
+
if (typeof ref === "function") {
|
|
41
|
+
const result = ref(node);
|
|
42
|
+
return typeof result === "function" ? result : () => ref(null);
|
|
43
|
+
}
|
|
44
|
+
if (ref && typeof ref === "object") {
|
|
45
|
+
ref.current = node;
|
|
46
|
+
return () => {
|
|
47
|
+
ref.current = null;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
return void 0;
|
|
51
|
+
});
|
|
52
|
+
return () => {
|
|
53
|
+
cleanups.forEach((cleanup) => {
|
|
54
|
+
if (typeof cleanup === "function") cleanup();
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function isSyntheticEvent(event) {
|
|
60
|
+
return typeof event === "object" && event !== null && "nativeEvent" in event;
|
|
61
|
+
}
|
|
62
|
+
function makeEventPreventable(event) {
|
|
63
|
+
const target = event;
|
|
64
|
+
target.preventComponentHandler = () => {
|
|
65
|
+
target.componentHandlerPrevented = true;
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function isComponentHandlerPrevented(event) {
|
|
69
|
+
return typeof event === "object" && event !== null && event.componentHandlerPrevented === true;
|
|
70
|
+
}
|
|
71
|
+
function mergeProps(...objects) {
|
|
72
|
+
const merged = {};
|
|
73
|
+
for (const props of objects) {
|
|
74
|
+
if (!props) continue;
|
|
75
|
+
for (const key of Object.keys(props)) {
|
|
76
|
+
const value = props[key];
|
|
77
|
+
if (value === void 0) continue;
|
|
78
|
+
if (isEventHandler(key) && typeof value === "function") {
|
|
79
|
+
const earlier = merged[key];
|
|
80
|
+
merged[key] = typeof earlier === "function" ? (...args) => {
|
|
81
|
+
const event = args[0];
|
|
82
|
+
if (isSyntheticEvent(event)) makeEventPreventable(event);
|
|
83
|
+
value(...args);
|
|
84
|
+
if (isComponentHandlerPrevented(event)) return;
|
|
85
|
+
earlier(...args);
|
|
86
|
+
} : value;
|
|
87
|
+
} else if (key === "className") {
|
|
88
|
+
merged.className = merged.className ? `${String(merged.className)} ${String(value)}` : value;
|
|
89
|
+
} else if (key === "style") {
|
|
90
|
+
merged.style = { ...merged.style, ...value };
|
|
91
|
+
} else if (key === "ref") {
|
|
92
|
+
merged.ref = merged.ref ? mergeRefs(merged.ref, value) : value;
|
|
93
|
+
} else {
|
|
94
|
+
merged[key] = value;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return merged;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// src/core/stateAttributes.ts
|
|
102
|
+
function getStateAttributes(state, mapping) {
|
|
103
|
+
if (!mapping) return {};
|
|
104
|
+
const attributes = {};
|
|
105
|
+
for (const key of Object.keys(state)) {
|
|
106
|
+
const toAttributes = mapping[key];
|
|
107
|
+
if (!toAttributes) continue;
|
|
108
|
+
const result = toAttributes(state[key]);
|
|
109
|
+
if (result) Object.assign(attributes, result);
|
|
110
|
+
}
|
|
111
|
+
return attributes;
|
|
112
|
+
}
|
|
113
|
+
function booleanAttribute(whenTrue, whenFalse) {
|
|
114
|
+
return (value) => {
|
|
115
|
+
if (value) return { [whenTrue]: "" };
|
|
116
|
+
return whenFalse ? { [whenFalse]: "" } : null;
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
var disabledAttribute = booleanAttribute("data-disabled");
|
|
120
|
+
|
|
121
|
+
// src/core/useRender.ts
|
|
122
|
+
function useRender({
|
|
123
|
+
render,
|
|
124
|
+
defaultTagName,
|
|
125
|
+
props,
|
|
126
|
+
state,
|
|
127
|
+
stateAttributes
|
|
128
|
+
}) {
|
|
129
|
+
const attributes = state ? getStateAttributes(state, stateAttributes) : {};
|
|
130
|
+
const merged = mergeProps(attributes, ...props);
|
|
131
|
+
if ((0, import_react.isValidElement)(render)) {
|
|
132
|
+
return (0, import_react.cloneElement)(render, mergeProps(merged, render.props));
|
|
133
|
+
}
|
|
134
|
+
return (0, import_react.createElement)(defaultTagName, merged);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// src/useButton.ts
|
|
138
|
+
var import_react2 = require("react");
|
|
139
|
+
var ACTIVATION_HANDLERS = [
|
|
140
|
+
"onClick",
|
|
141
|
+
"onDoubleClick",
|
|
142
|
+
"onMouseDown",
|
|
143
|
+
"onMouseUp",
|
|
144
|
+
"onPointerDown",
|
|
145
|
+
"onPointerUp",
|
|
146
|
+
"onTouchStart",
|
|
147
|
+
"onTouchEnd",
|
|
148
|
+
"onKeyDown",
|
|
149
|
+
"onKeyUp",
|
|
150
|
+
"onKeyPress"
|
|
151
|
+
];
|
|
152
|
+
function activationHandlers() {
|
|
153
|
+
return {
|
|
154
|
+
onKeyDown(event) {
|
|
155
|
+
if (event.target !== event.currentTarget || event.defaultPrevented) return;
|
|
156
|
+
if (event.key === " ") {
|
|
157
|
+
event.preventDefault();
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (event.key === "Enter") {
|
|
161
|
+
event.preventDefault();
|
|
162
|
+
click(event.currentTarget);
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
onKeyUp(event) {
|
|
166
|
+
if (event.target !== event.currentTarget || event.defaultPrevented) return;
|
|
167
|
+
if (event.key === " ") click(event.currentTarget);
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
function click(element) {
|
|
172
|
+
if (element && typeof element.click === "function") {
|
|
173
|
+
element.click();
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
function useButton({
|
|
177
|
+
disabled = false,
|
|
178
|
+
native,
|
|
179
|
+
props = {}
|
|
180
|
+
}) {
|
|
181
|
+
const elementRef = (0, import_react2.useRef)(null);
|
|
182
|
+
(0, import_react2.useEffect)(() => {
|
|
183
|
+
if (process.env.NODE_ENV === "production") return;
|
|
184
|
+
const element = elementRef.current;
|
|
185
|
+
if (!element) return;
|
|
186
|
+
const isButtonTag = element.tagName === "BUTTON";
|
|
187
|
+
if (native && !isButtonTag) {
|
|
188
|
+
console.error(
|
|
189
|
+
`A component expected a native <button> but rendered <${element.tagName.toLowerCase()}>. Focus, keyboard activation and \`disabled\` will not behave natively.`
|
|
190
|
+
);
|
|
191
|
+
} else if (!native && isButtonTag) {
|
|
192
|
+
console.error(
|
|
193
|
+
"A component rendered a native <button> while treating it as a non-native element, so it carries synthesised attributes it does not need."
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
}, [native]);
|
|
197
|
+
return { props: buttonProps({ disabled, native, props }), ref: elementRef };
|
|
198
|
+
}
|
|
199
|
+
function buttonProps({ disabled, native, props }) {
|
|
200
|
+
if (!disabled) {
|
|
201
|
+
return native ? mergeProps({ type: "button" }, props) : mergeProps({ tabIndex: 0 }, activationHandlers(), props);
|
|
202
|
+
}
|
|
203
|
+
if (native) return mergeProps({ type: "button" }, props, { disabled: true, "data-disabled": "" });
|
|
204
|
+
const sanitised = {};
|
|
205
|
+
for (const key of Object.keys(props)) {
|
|
206
|
+
if (key === "href" || ACTIVATION_HANDLERS.includes(key)) continue;
|
|
207
|
+
sanitised[key] = props[key];
|
|
208
|
+
}
|
|
209
|
+
return {
|
|
210
|
+
...sanitised,
|
|
211
|
+
"aria-disabled": true,
|
|
212
|
+
"data-disabled": "",
|
|
213
|
+
// Mirrors a native disabled button, which is not focusable.
|
|
214
|
+
tabIndex: -1
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
function retractActivationProps(props) {
|
|
218
|
+
const overrides = {};
|
|
219
|
+
for (const key of Object.keys(props)) {
|
|
220
|
+
if (key === "href" || ACTIVATION_HANDLERS.includes(key)) overrides[key] = void 0;
|
|
221
|
+
}
|
|
222
|
+
return overrides;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// src/button/Button.tsx
|
|
226
|
+
function Button({
|
|
227
|
+
disabled = false,
|
|
228
|
+
nativeButton,
|
|
229
|
+
className,
|
|
230
|
+
children,
|
|
231
|
+
render,
|
|
232
|
+
...rest
|
|
233
|
+
}) {
|
|
234
|
+
const native = nativeButton ?? (render === void 0 || render.type === "button");
|
|
235
|
+
const isAnchor = render !== void 0 && render.type === "a";
|
|
236
|
+
const role = native || isAnchor ? void 0 : "button";
|
|
237
|
+
const { props: elementProps, ref: buttonRef } = useButton({ disabled, native, props: rest });
|
|
238
|
+
const safeRender = disabled && !native && render !== void 0 ? (0, import_react3.cloneElement)(render, retractActivationProps(render.props)) : render;
|
|
239
|
+
return useRender({
|
|
240
|
+
render: safeRender,
|
|
241
|
+
defaultTagName: "button",
|
|
242
|
+
props: [{ role, className, children }, elementProps, { ref: buttonRef }]
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
246
|
+
0 && (module.exports = {
|
|
247
|
+
Button
|
|
248
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ReactNode, ReactElement, Ref } from 'react';
|
|
3
|
+
|
|
4
|
+
interface ButtonProps {
|
|
5
|
+
/**
|
|
6
|
+
* Prevents activation. On a native `<button>` the platform handles it; on any other
|
|
7
|
+
* element the state is synthesised — `aria-disabled`, removal from the tab order,
|
|
8
|
+
* no activation, and a navigation target dropped.
|
|
9
|
+
*/
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Whether the rendered element is a native `<button>`.
|
|
13
|
+
*
|
|
14
|
+
* Inferred from `render`, which is right for an element literal. Set it explicitly
|
|
15
|
+
* when rendering a *component* — `render={<MyButton />}` cannot be inspected, and
|
|
16
|
+
* gets treated as non-native. A mismatch logs a development warning.
|
|
17
|
+
*/
|
|
18
|
+
nativeButton?: boolean;
|
|
19
|
+
className?: string;
|
|
20
|
+
children?: ReactNode;
|
|
21
|
+
/**
|
|
22
|
+
* Element to render instead of the default `<button>`. Props, className, event
|
|
23
|
+
* handlers and ref are merged onto it.
|
|
24
|
+
*/
|
|
25
|
+
render?: ReactElement;
|
|
26
|
+
ref?: Ref<HTMLElement>;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* A button — behaviour only, no styling.
|
|
30
|
+
*
|
|
31
|
+
* A native `<button>` needs almost none of this; the platform supplies focus, `Enter`
|
|
32
|
+
* and `Space` activation, and `disabled`. The component earns its place the moment
|
|
33
|
+
* `render` points somewhere else, which is exactly when all of that stops holding.
|
|
34
|
+
*
|
|
35
|
+
* It has no `href`. A control that navigates should be an `<a>`, so that middle-click,
|
|
36
|
+
* cmd-click, the status bar and "link" in assistive technology all work — pass one in:
|
|
37
|
+
*
|
|
38
|
+
* <Button render={<a href="/docs" target="_blank" rel="noreferrer" />}>Docs</Button>
|
|
39
|
+
*
|
|
40
|
+
* Emits `data-disabled` so one selector styles a disabled control whatever the element.
|
|
41
|
+
*/
|
|
42
|
+
declare function Button({ disabled, nativeButton, className, children, render, ...rest }: ButtonProps & Record<string, unknown>): ReactElement<unknown, string | react.JSXElementConstructor<any>>;
|
|
43
|
+
|
|
44
|
+
export { Button, type ButtonProps };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ReactNode, ReactElement, Ref } from 'react';
|
|
3
|
+
|
|
4
|
+
interface ButtonProps {
|
|
5
|
+
/**
|
|
6
|
+
* Prevents activation. On a native `<button>` the platform handles it; on any other
|
|
7
|
+
* element the state is synthesised — `aria-disabled`, removal from the tab order,
|
|
8
|
+
* no activation, and a navigation target dropped.
|
|
9
|
+
*/
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Whether the rendered element is a native `<button>`.
|
|
13
|
+
*
|
|
14
|
+
* Inferred from `render`, which is right for an element literal. Set it explicitly
|
|
15
|
+
* when rendering a *component* — `render={<MyButton />}` cannot be inspected, and
|
|
16
|
+
* gets treated as non-native. A mismatch logs a development warning.
|
|
17
|
+
*/
|
|
18
|
+
nativeButton?: boolean;
|
|
19
|
+
className?: string;
|
|
20
|
+
children?: ReactNode;
|
|
21
|
+
/**
|
|
22
|
+
* Element to render instead of the default `<button>`. Props, className, event
|
|
23
|
+
* handlers and ref are merged onto it.
|
|
24
|
+
*/
|
|
25
|
+
render?: ReactElement;
|
|
26
|
+
ref?: Ref<HTMLElement>;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* A button — behaviour only, no styling.
|
|
30
|
+
*
|
|
31
|
+
* A native `<button>` needs almost none of this; the platform supplies focus, `Enter`
|
|
32
|
+
* and `Space` activation, and `disabled`. The component earns its place the moment
|
|
33
|
+
* `render` points somewhere else, which is exactly when all of that stops holding.
|
|
34
|
+
*
|
|
35
|
+
* It has no `href`. A control that navigates should be an `<a>`, so that middle-click,
|
|
36
|
+
* cmd-click, the status bar and "link" in assistive technology all work — pass one in:
|
|
37
|
+
*
|
|
38
|
+
* <Button render={<a href="/docs" target="_blank" rel="noreferrer" />}>Docs</Button>
|
|
39
|
+
*
|
|
40
|
+
* Emits `data-disabled` so one selector styles a disabled control whatever the element.
|
|
41
|
+
*/
|
|
42
|
+
declare function Button({ disabled, nativeButton, className, children, render, ...rest }: ButtonProps & Record<string, unknown>): ReactElement<unknown, string | react.JSXElementConstructor<any>>;
|
|
43
|
+
|
|
44
|
+
export { Button, type ButtonProps };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import {
|
|
2
|
+
retractActivationProps,
|
|
3
|
+
useButton
|
|
4
|
+
} from "../chunk-2XSRAJHB.js";
|
|
5
|
+
import {
|
|
6
|
+
useRender
|
|
7
|
+
} from "../chunk-HWG3GMTO.js";
|
|
8
|
+
|
|
9
|
+
// src/button/Button.tsx
|
|
10
|
+
import { cloneElement } from "react";
|
|
11
|
+
function Button({
|
|
12
|
+
disabled = false,
|
|
13
|
+
nativeButton,
|
|
14
|
+
className,
|
|
15
|
+
children,
|
|
16
|
+
render,
|
|
17
|
+
...rest
|
|
18
|
+
}) {
|
|
19
|
+
const native = nativeButton ?? (render === void 0 || render.type === "button");
|
|
20
|
+
const isAnchor = render !== void 0 && render.type === "a";
|
|
21
|
+
const role = native || isAnchor ? void 0 : "button";
|
|
22
|
+
const { props: elementProps, ref: buttonRef } = useButton({ disabled, native, props: rest });
|
|
23
|
+
const safeRender = disabled && !native && render !== void 0 ? cloneElement(render, retractActivationProps(render.props)) : render;
|
|
24
|
+
return useRender({
|
|
25
|
+
render: safeRender,
|
|
26
|
+
defaultTagName: "button",
|
|
27
|
+
props: [{ role, className, children }, elementProps, { ref: buttonRef }]
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
export {
|
|
31
|
+
Button
|
|
32
|
+
};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import {
|
|
2
|
+
mergeProps
|
|
3
|
+
} from "./chunk-HWG3GMTO.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
|
+
};
|
|
@@ -30,6 +30,18 @@ function mergeRefs(...refs) {
|
|
|
30
30
|
};
|
|
31
31
|
};
|
|
32
32
|
}
|
|
33
|
+
function isSyntheticEvent(event) {
|
|
34
|
+
return typeof event === "object" && event !== null && "nativeEvent" in event;
|
|
35
|
+
}
|
|
36
|
+
function makeEventPreventable(event) {
|
|
37
|
+
const target = event;
|
|
38
|
+
target.preventComponentHandler = () => {
|
|
39
|
+
target.componentHandlerPrevented = true;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function isComponentHandlerPrevented(event) {
|
|
43
|
+
return typeof event === "object" && event !== null && event.componentHandlerPrevented === true;
|
|
44
|
+
}
|
|
33
45
|
function mergeProps(...objects) {
|
|
34
46
|
const merged = {};
|
|
35
47
|
for (const props of objects) {
|
|
@@ -38,10 +50,13 @@ function mergeProps(...objects) {
|
|
|
38
50
|
const value = props[key];
|
|
39
51
|
if (value === void 0) continue;
|
|
40
52
|
if (isEventHandler(key) && typeof value === "function") {
|
|
41
|
-
const
|
|
42
|
-
merged[key] = typeof
|
|
43
|
-
|
|
53
|
+
const earlier = merged[key];
|
|
54
|
+
merged[key] = typeof earlier === "function" ? (...args) => {
|
|
55
|
+
const event = args[0];
|
|
56
|
+
if (isSyntheticEvent(event)) makeEventPreventable(event);
|
|
44
57
|
value(...args);
|
|
58
|
+
if (isComponentHandlerPrevented(event)) return;
|
|
59
|
+
earlier(...args);
|
|
45
60
|
} : value;
|
|
46
61
|
} else if (key === "className") {
|
|
47
62
|
merged.className = merged.className ? `${String(merged.className)} ${String(value)}` : value;
|
|
@@ -94,45 +109,11 @@ function useRender({
|
|
|
94
109
|
return createElement(defaultTagName, merged);
|
|
95
110
|
}
|
|
96
111
|
|
|
97
|
-
// src/core/useControlled.ts
|
|
98
|
-
import { useCallback, useRef, useState } from "react";
|
|
99
|
-
function useControlled({
|
|
100
|
-
controlled,
|
|
101
|
-
default: defaultValue,
|
|
102
|
-
name = "Component",
|
|
103
|
-
state = "value"
|
|
104
|
-
}) {
|
|
105
|
-
const { current: isControlled } = useRef(controlled !== void 0);
|
|
106
|
-
const [uncontrolled, setUncontrolled] = useState(defaultValue);
|
|
107
|
-
const defaultRef = useRef(defaultValue);
|
|
108
|
-
const value = isControlled ? controlled : uncontrolled;
|
|
109
|
-
if (process.env.NODE_ENV !== "production") {
|
|
110
|
-
if (isControlled !== (controlled !== void 0)) {
|
|
111
|
-
console.error(
|
|
112
|
-
`${name}: cannot switch between controlled and uncontrolled \`${state}\`. Decide which one this component is for the whole of its life.`
|
|
113
|
-
);
|
|
114
|
-
}
|
|
115
|
-
if (!isControlled && defaultRef.current !== defaultValue) {
|
|
116
|
-
console.error(
|
|
117
|
-
`${name}: cannot change the default \`${state}\` after mount. It is only read once, so later changes are silently ignored.`
|
|
118
|
-
);
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
const setValue = useCallback(
|
|
122
|
-
(next) => {
|
|
123
|
-
if (!isControlled) setUncontrolled(next);
|
|
124
|
-
},
|
|
125
|
-
[isControlled]
|
|
126
|
-
);
|
|
127
|
-
return [value, setValue];
|
|
128
|
-
}
|
|
129
|
-
|
|
130
112
|
export {
|
|
131
113
|
__export,
|
|
132
114
|
mergeProps,
|
|
133
115
|
getStateAttributes,
|
|
134
116
|
booleanAttribute,
|
|
135
117
|
disabledAttribute,
|
|
136
|
-
useRender
|
|
137
|
-
useControlled
|
|
118
|
+
useRender
|
|
138
119
|
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// src/core/useControlled.ts
|
|
2
|
+
import { useCallback, useRef, useState } from "react";
|
|
3
|
+
function useControlled({
|
|
4
|
+
controlled,
|
|
5
|
+
default: defaultValue,
|
|
6
|
+
name = "Component",
|
|
7
|
+
state = "value"
|
|
8
|
+
}) {
|
|
9
|
+
const { current: isControlled } = useRef(controlled !== void 0);
|
|
10
|
+
const [uncontrolled, setUncontrolled] = useState(defaultValue);
|
|
11
|
+
const defaultRef = useRef(defaultValue);
|
|
12
|
+
const value = isControlled ? controlled : uncontrolled;
|
|
13
|
+
if (process.env.NODE_ENV !== "production") {
|
|
14
|
+
if (isControlled !== (controlled !== void 0)) {
|
|
15
|
+
console.error(
|
|
16
|
+
`${name}: cannot switch between controlled and uncontrolled \`${state}\`. Decide which one this component is for the whole of its life.`
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
if (!isControlled && defaultRef.current !== defaultValue) {
|
|
20
|
+
console.error(
|
|
21
|
+
`${name}: cannot change the default \`${state}\` after mount. It is only read once, so later changes are silently ignored.`
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const setValue = useCallback(
|
|
26
|
+
(next) => {
|
|
27
|
+
if (!isControlled) setUncontrolled(next);
|
|
28
|
+
},
|
|
29
|
+
[isControlled]
|
|
30
|
+
);
|
|
31
|
+
return [value, setValue];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export {
|
|
35
|
+
useControlled
|
|
36
|
+
};
|
package/dist/index.cjs
CHANGED
|
@@ -24,8 +24,6 @@ __export(index_exports, {
|
|
|
24
24
|
disabledAttribute: () => disabledAttribute,
|
|
25
25
|
getStateAttributes: () => getStateAttributes,
|
|
26
26
|
mergeProps: () => mergeProps,
|
|
27
|
-
retractActivationProps: () => retractActivationProps,
|
|
28
|
-
useButton: () => useButton,
|
|
29
27
|
useControlled: () => useControlled,
|
|
30
28
|
useRender: () => useRender
|
|
31
29
|
});
|
|
@@ -57,6 +55,18 @@ function mergeRefs(...refs) {
|
|
|
57
55
|
};
|
|
58
56
|
};
|
|
59
57
|
}
|
|
58
|
+
function isSyntheticEvent(event) {
|
|
59
|
+
return typeof event === "object" && event !== null && "nativeEvent" in event;
|
|
60
|
+
}
|
|
61
|
+
function makeEventPreventable(event) {
|
|
62
|
+
const target = event;
|
|
63
|
+
target.preventComponentHandler = () => {
|
|
64
|
+
target.componentHandlerPrevented = true;
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function isComponentHandlerPrevented(event) {
|
|
68
|
+
return typeof event === "object" && event !== null && event.componentHandlerPrevented === true;
|
|
69
|
+
}
|
|
60
70
|
function mergeProps(...objects) {
|
|
61
71
|
const merged = {};
|
|
62
72
|
for (const props of objects) {
|
|
@@ -65,10 +75,13 @@ function mergeProps(...objects) {
|
|
|
65
75
|
const value = props[key];
|
|
66
76
|
if (value === void 0) continue;
|
|
67
77
|
if (isEventHandler(key) && typeof value === "function") {
|
|
68
|
-
const
|
|
69
|
-
merged[key] = typeof
|
|
70
|
-
|
|
78
|
+
const earlier = merged[key];
|
|
79
|
+
merged[key] = typeof earlier === "function" ? (...args) => {
|
|
80
|
+
const event = args[0];
|
|
81
|
+
if (isSyntheticEvent(event)) makeEventPreventable(event);
|
|
71
82
|
value(...args);
|
|
83
|
+
if (isComponentHandlerPrevented(event)) return;
|
|
84
|
+
earlier(...args);
|
|
72
85
|
} : value;
|
|
73
86
|
} else if (key === "className") {
|
|
74
87
|
merged.className = merged.className ? `${String(merged.className)} ${String(value)}` : value;
|
|
@@ -155,52 +168,12 @@ function useControlled({
|
|
|
155
168
|
);
|
|
156
169
|
return [value, setValue];
|
|
157
170
|
}
|
|
158
|
-
|
|
159
|
-
// src/useButton.ts
|
|
160
|
-
var ACTIVATION_HANDLERS = [
|
|
161
|
-
"onClick",
|
|
162
|
-
"onDoubleClick",
|
|
163
|
-
"onMouseDown",
|
|
164
|
-
"onMouseUp",
|
|
165
|
-
"onPointerDown",
|
|
166
|
-
"onPointerUp",
|
|
167
|
-
"onTouchStart",
|
|
168
|
-
"onTouchEnd",
|
|
169
|
-
"onKeyDown",
|
|
170
|
-
"onKeyUp",
|
|
171
|
-
"onKeyPress"
|
|
172
|
-
];
|
|
173
|
-
function useButton({ disabled = false, native, props = {} }) {
|
|
174
|
-
if (!disabled) return props;
|
|
175
|
-
if (native) return { ...props, disabled: true, "data-disabled": "" };
|
|
176
|
-
const sanitised = {};
|
|
177
|
-
for (const key of Object.keys(props)) {
|
|
178
|
-
if (key === "href" || ACTIVATION_HANDLERS.includes(key)) continue;
|
|
179
|
-
sanitised[key] = props[key];
|
|
180
|
-
}
|
|
181
|
-
return {
|
|
182
|
-
...sanitised,
|
|
183
|
-
"aria-disabled": true,
|
|
184
|
-
"data-disabled": "",
|
|
185
|
-
// Mirrors a native disabled button, which is not focusable.
|
|
186
|
-
tabIndex: -1
|
|
187
|
-
};
|
|
188
|
-
}
|
|
189
|
-
function retractActivationProps(props) {
|
|
190
|
-
const overrides = {};
|
|
191
|
-
for (const key of Object.keys(props)) {
|
|
192
|
-
if (key === "href" || ACTIVATION_HANDLERS.includes(key)) overrides[key] = void 0;
|
|
193
|
-
}
|
|
194
|
-
return overrides;
|
|
195
|
-
}
|
|
196
171
|
// Annotate the CommonJS export names for ESM import in node:
|
|
197
172
|
0 && (module.exports = {
|
|
198
173
|
booleanAttribute,
|
|
199
174
|
disabledAttribute,
|
|
200
175
|
getStateAttributes,
|
|
201
176
|
mergeProps,
|
|
202
|
-
retractActivationProps,
|
|
203
|
-
useButton,
|
|
204
177
|
useControlled,
|
|
205
178
|
useRender
|
|
206
179
|
});
|
package/dist/index.d.cts
CHANGED
|
@@ -8,6 +8,18 @@ import { ReactElement } from 'react';
|
|
|
8
8
|
* kind of prop is combined rather than replaced.
|
|
9
9
|
*/
|
|
10
10
|
type UnknownProps = Record<string, unknown>;
|
|
11
|
+
/**
|
|
12
|
+
* An event a consumer's handler can use to stop the component's own handler running.
|
|
13
|
+
*
|
|
14
|
+
* `preventDefault()` is deliberately not overloaded for this: it already means "cancel
|
|
15
|
+
* the browser's default action", which is a different statement. A link inside a
|
|
16
|
+
* component may well want one without the other.
|
|
17
|
+
*/
|
|
18
|
+
type ComponentEvent<E = unknown> = E & {
|
|
19
|
+
/** Stops the component's own handler for this event. */
|
|
20
|
+
preventComponentHandler(): void;
|
|
21
|
+
readonly componentHandlerPrevented?: boolean;
|
|
22
|
+
};
|
|
11
23
|
/**
|
|
12
24
|
* Merge prop objects left to right. Later objects win for plain values, but:
|
|
13
25
|
* - event handlers are chained rather than replaced
|
|
@@ -17,6 +29,17 @@ type UnknownProps = Record<string, unknown>;
|
|
|
17
29
|
*
|
|
18
30
|
* `undefined` values are skipped, so an absent key on a later object never
|
|
19
31
|
* clobbers a value set by an earlier one.
|
|
32
|
+
*
|
|
33
|
+
* **Event handlers run right to left** — the last object's handler first, the first
|
|
34
|
+
* object's last. Since a component passes its own props first and the consumer's last,
|
|
35
|
+
* the consumer's handler runs before the component's and can stop it:
|
|
36
|
+
*
|
|
37
|
+
* <Switch.Root onClick={(event) => event.preventComponentHandler()} />
|
|
38
|
+
*
|
|
39
|
+
* That is the opposite direction to plain values, where later objects win. The escape
|
|
40
|
+
* hatch belongs to the consumer: a component already decides which props reach the
|
|
41
|
+
* element, so it never needs the chain to suppress anything — it declines to attach the
|
|
42
|
+
* handler instead. See docs/architecture.md decision 8.
|
|
20
43
|
*/
|
|
21
44
|
declare function mergeProps(...objects: (UnknownProps | undefined)[]): UnknownProps;
|
|
22
45
|
|
|
@@ -102,26 +125,4 @@ declare function useControlled<T>({ controlled, default: defaultValue, name, sta
|
|
|
102
125
|
state?: string;
|
|
103
126
|
}): [T, (next: T) => void];
|
|
104
127
|
|
|
105
|
-
|
|
106
|
-
disabled?: boolean;
|
|
107
|
-
/** Whether the rendered element is a native `<button>`, which the platform disables for us. */
|
|
108
|
-
native: boolean;
|
|
109
|
-
/** Props destined for the element. Returned unchanged unless `disabled`. */
|
|
110
|
-
props?: UnknownProps;
|
|
111
|
-
}
|
|
112
|
-
declare function useButton({ disabled, native, props }: UseButtonParams): UnknownProps;
|
|
113
|
-
/**
|
|
114
|
-
* Overrides that neutralise a consumer-supplied `render` element.
|
|
115
|
-
*
|
|
116
|
-
* `useButton` sanitises the props a component passes, but a `render` element's own
|
|
117
|
-
* props merge at the highest precedence inside `useRender`, so a `href` written
|
|
118
|
-
* directly on it survives. Those have to be retracted on the element itself:
|
|
119
|
-
*
|
|
120
|
-
* cloneElement(render, retractActivationProps(render.props))
|
|
121
|
-
*
|
|
122
|
-
* `cloneElement` overwrites with `undefined`, which `mergeProps` cannot do — it skips
|
|
123
|
-
* `undefined` so an absent prop never clobbers a present one.
|
|
124
|
-
*/
|
|
125
|
-
declare function retractActivationProps(props: UnknownProps): UnknownProps;
|
|
126
|
-
|
|
127
|
-
export { type StateAttributeMapping, type UnknownProps, type UseButtonParams, type UseRenderParams, booleanAttribute, disabledAttribute, getStateAttributes, mergeProps, retractActivationProps, useButton, useControlled, useRender };
|
|
128
|
+
export { type ComponentEvent, type StateAttributeMapping, type UnknownProps, type UseRenderParams, booleanAttribute, disabledAttribute, getStateAttributes, mergeProps, useControlled, useRender };
|
package/dist/index.d.ts
CHANGED
|
@@ -8,6 +8,18 @@ import { ReactElement } from 'react';
|
|
|
8
8
|
* kind of prop is combined rather than replaced.
|
|
9
9
|
*/
|
|
10
10
|
type UnknownProps = Record<string, unknown>;
|
|
11
|
+
/**
|
|
12
|
+
* An event a consumer's handler can use to stop the component's own handler running.
|
|
13
|
+
*
|
|
14
|
+
* `preventDefault()` is deliberately not overloaded for this: it already means "cancel
|
|
15
|
+
* the browser's default action", which is a different statement. A link inside a
|
|
16
|
+
* component may well want one without the other.
|
|
17
|
+
*/
|
|
18
|
+
type ComponentEvent<E = unknown> = E & {
|
|
19
|
+
/** Stops the component's own handler for this event. */
|
|
20
|
+
preventComponentHandler(): void;
|
|
21
|
+
readonly componentHandlerPrevented?: boolean;
|
|
22
|
+
};
|
|
11
23
|
/**
|
|
12
24
|
* Merge prop objects left to right. Later objects win for plain values, but:
|
|
13
25
|
* - event handlers are chained rather than replaced
|
|
@@ -17,6 +29,17 @@ type UnknownProps = Record<string, unknown>;
|
|
|
17
29
|
*
|
|
18
30
|
* `undefined` values are skipped, so an absent key on a later object never
|
|
19
31
|
* clobbers a value set by an earlier one.
|
|
32
|
+
*
|
|
33
|
+
* **Event handlers run right to left** — the last object's handler first, the first
|
|
34
|
+
* object's last. Since a component passes its own props first and the consumer's last,
|
|
35
|
+
* the consumer's handler runs before the component's and can stop it:
|
|
36
|
+
*
|
|
37
|
+
* <Switch.Root onClick={(event) => event.preventComponentHandler()} />
|
|
38
|
+
*
|
|
39
|
+
* That is the opposite direction to plain values, where later objects win. The escape
|
|
40
|
+
* hatch belongs to the consumer: a component already decides which props reach the
|
|
41
|
+
* element, so it never needs the chain to suppress anything — it declines to attach the
|
|
42
|
+
* handler instead. See docs/architecture.md decision 8.
|
|
20
43
|
*/
|
|
21
44
|
declare function mergeProps(...objects: (UnknownProps | undefined)[]): UnknownProps;
|
|
22
45
|
|
|
@@ -102,26 +125,4 @@ declare function useControlled<T>({ controlled, default: defaultValue, name, sta
|
|
|
102
125
|
state?: string;
|
|
103
126
|
}): [T, (next: T) => void];
|
|
104
127
|
|
|
105
|
-
|
|
106
|
-
disabled?: boolean;
|
|
107
|
-
/** Whether the rendered element is a native `<button>`, which the platform disables for us. */
|
|
108
|
-
native: boolean;
|
|
109
|
-
/** Props destined for the element. Returned unchanged unless `disabled`. */
|
|
110
|
-
props?: UnknownProps;
|
|
111
|
-
}
|
|
112
|
-
declare function useButton({ disabled, native, props }: UseButtonParams): UnknownProps;
|
|
113
|
-
/**
|
|
114
|
-
* Overrides that neutralise a consumer-supplied `render` element.
|
|
115
|
-
*
|
|
116
|
-
* `useButton` sanitises the props a component passes, but a `render` element's own
|
|
117
|
-
* props merge at the highest precedence inside `useRender`, so a `href` written
|
|
118
|
-
* directly on it survives. Those have to be retracted on the element itself:
|
|
119
|
-
*
|
|
120
|
-
* cloneElement(render, retractActivationProps(render.props))
|
|
121
|
-
*
|
|
122
|
-
* `cloneElement` overwrites with `undefined`, which `mergeProps` cannot do — it skips
|
|
123
|
-
* `undefined` so an absent prop never clobbers a present one.
|
|
124
|
-
*/
|
|
125
|
-
declare function retractActivationProps(props: UnknownProps): UnknownProps;
|
|
126
|
-
|
|
127
|
-
export { type StateAttributeMapping, type UnknownProps, type UseButtonParams, type UseRenderParams, booleanAttribute, disabledAttribute, getStateAttributes, mergeProps, retractActivationProps, useButton, useControlled, useRender };
|
|
128
|
+
export { type ComponentEvent, type StateAttributeMapping, type UnknownProps, type UseRenderParams, booleanAttribute, disabledAttribute, getStateAttributes, mergeProps, useControlled, useRender };
|
package/dist/index.js
CHANGED
|
@@ -1,56 +1,18 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useControlled
|
|
3
|
+
} from "./chunk-UEIB3I3T.js";
|
|
1
4
|
import {
|
|
2
5
|
booleanAttribute,
|
|
3
6
|
disabledAttribute,
|
|
4
7
|
getStateAttributes,
|
|
5
8
|
mergeProps,
|
|
6
|
-
useControlled,
|
|
7
9
|
useRender
|
|
8
|
-
} from "./chunk-
|
|
9
|
-
|
|
10
|
-
// src/useButton.ts
|
|
11
|
-
var ACTIVATION_HANDLERS = [
|
|
12
|
-
"onClick",
|
|
13
|
-
"onDoubleClick",
|
|
14
|
-
"onMouseDown",
|
|
15
|
-
"onMouseUp",
|
|
16
|
-
"onPointerDown",
|
|
17
|
-
"onPointerUp",
|
|
18
|
-
"onTouchStart",
|
|
19
|
-
"onTouchEnd",
|
|
20
|
-
"onKeyDown",
|
|
21
|
-
"onKeyUp",
|
|
22
|
-
"onKeyPress"
|
|
23
|
-
];
|
|
24
|
-
function useButton({ disabled = false, native, props = {} }) {
|
|
25
|
-
if (!disabled) return props;
|
|
26
|
-
if (native) return { ...props, disabled: true, "data-disabled": "" };
|
|
27
|
-
const sanitised = {};
|
|
28
|
-
for (const key of Object.keys(props)) {
|
|
29
|
-
if (key === "href" || ACTIVATION_HANDLERS.includes(key)) continue;
|
|
30
|
-
sanitised[key] = props[key];
|
|
31
|
-
}
|
|
32
|
-
return {
|
|
33
|
-
...sanitised,
|
|
34
|
-
"aria-disabled": true,
|
|
35
|
-
"data-disabled": "",
|
|
36
|
-
// Mirrors a native disabled button, which is not focusable.
|
|
37
|
-
tabIndex: -1
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
function retractActivationProps(props) {
|
|
41
|
-
const overrides = {};
|
|
42
|
-
for (const key of Object.keys(props)) {
|
|
43
|
-
if (key === "href" || ACTIVATION_HANDLERS.includes(key)) overrides[key] = void 0;
|
|
44
|
-
}
|
|
45
|
-
return overrides;
|
|
46
|
-
}
|
|
10
|
+
} from "./chunk-HWG3GMTO.js";
|
|
47
11
|
export {
|
|
48
12
|
booleanAttribute,
|
|
49
13
|
disabledAttribute,
|
|
50
14
|
getStateAttributes,
|
|
51
15
|
mergeProps,
|
|
52
|
-
retractActivationProps,
|
|
53
|
-
useButton,
|
|
54
16
|
useControlled,
|
|
55
17
|
useRender
|
|
56
18
|
};
|
package/dist/switch/index.cjs
CHANGED
|
@@ -32,43 +32,10 @@ __export(index_parts_exports, {
|
|
|
32
32
|
});
|
|
33
33
|
|
|
34
34
|
// src/switch/root/SwitchRoot.tsx
|
|
35
|
-
var
|
|
35
|
+
var import_react5 = require("react");
|
|
36
36
|
|
|
37
|
-
// src/
|
|
37
|
+
// src/useButton.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");
|
|
72
39
|
|
|
73
40
|
// src/core/mergeProps.ts
|
|
74
41
|
function isEventHandler(key) {
|
|
@@ -96,6 +63,18 @@ function mergeRefs(...refs) {
|
|
|
96
63
|
};
|
|
97
64
|
};
|
|
98
65
|
}
|
|
66
|
+
function isSyntheticEvent(event) {
|
|
67
|
+
return typeof event === "object" && event !== null && "nativeEvent" in event;
|
|
68
|
+
}
|
|
69
|
+
function makeEventPreventable(event) {
|
|
70
|
+
const target = event;
|
|
71
|
+
target.preventComponentHandler = () => {
|
|
72
|
+
target.componentHandlerPrevented = true;
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function isComponentHandlerPrevented(event) {
|
|
76
|
+
return typeof event === "object" && event !== null && event.componentHandlerPrevented === true;
|
|
77
|
+
}
|
|
99
78
|
function mergeProps(...objects) {
|
|
100
79
|
const merged = {};
|
|
101
80
|
for (const props of objects) {
|
|
@@ -104,10 +83,13 @@ function mergeProps(...objects) {
|
|
|
104
83
|
const value = props[key];
|
|
105
84
|
if (value === void 0) continue;
|
|
106
85
|
if (isEventHandler(key) && typeof value === "function") {
|
|
107
|
-
const
|
|
108
|
-
merged[key] = typeof
|
|
109
|
-
|
|
86
|
+
const earlier = merged[key];
|
|
87
|
+
merged[key] = typeof earlier === "function" ? (...args) => {
|
|
88
|
+
const event = args[0];
|
|
89
|
+
if (isSyntheticEvent(event)) makeEventPreventable(event);
|
|
110
90
|
value(...args);
|
|
91
|
+
if (isComponentHandlerPrevented(event)) return;
|
|
92
|
+
earlier(...args);
|
|
111
93
|
} : value;
|
|
112
94
|
} else if (key === "className") {
|
|
113
95
|
merged.className = merged.className ? `${String(merged.className)} ${String(value)}` : value;
|
|
@@ -123,6 +105,129 @@ function mergeProps(...objects) {
|
|
|
123
105
|
return merged;
|
|
124
106
|
}
|
|
125
107
|
|
|
108
|
+
// src/useButton.ts
|
|
109
|
+
var ACTIVATION_HANDLERS = [
|
|
110
|
+
"onClick",
|
|
111
|
+
"onDoubleClick",
|
|
112
|
+
"onMouseDown",
|
|
113
|
+
"onMouseUp",
|
|
114
|
+
"onPointerDown",
|
|
115
|
+
"onPointerUp",
|
|
116
|
+
"onTouchStart",
|
|
117
|
+
"onTouchEnd",
|
|
118
|
+
"onKeyDown",
|
|
119
|
+
"onKeyUp",
|
|
120
|
+
"onKeyPress"
|
|
121
|
+
];
|
|
122
|
+
function activationHandlers() {
|
|
123
|
+
return {
|
|
124
|
+
onKeyDown(event) {
|
|
125
|
+
if (event.target !== event.currentTarget || event.defaultPrevented) return;
|
|
126
|
+
if (event.key === " ") {
|
|
127
|
+
event.preventDefault();
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (event.key === "Enter") {
|
|
131
|
+
event.preventDefault();
|
|
132
|
+
click(event.currentTarget);
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
onKeyUp(event) {
|
|
136
|
+
if (event.target !== event.currentTarget || event.defaultPrevented) return;
|
|
137
|
+
if (event.key === " ") click(event.currentTarget);
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
function click(element) {
|
|
142
|
+
if (element && typeof element.click === "function") {
|
|
143
|
+
element.click();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function useButton({
|
|
147
|
+
disabled = false,
|
|
148
|
+
native,
|
|
149
|
+
props = {}
|
|
150
|
+
}) {
|
|
151
|
+
const elementRef = (0, import_react.useRef)(null);
|
|
152
|
+
(0, import_react.useEffect)(() => {
|
|
153
|
+
if (process.env.NODE_ENV === "production") return;
|
|
154
|
+
const element = elementRef.current;
|
|
155
|
+
if (!element) return;
|
|
156
|
+
const isButtonTag = element.tagName === "BUTTON";
|
|
157
|
+
if (native && !isButtonTag) {
|
|
158
|
+
console.error(
|
|
159
|
+
`A component expected a native <button> but rendered <${element.tagName.toLowerCase()}>. Focus, keyboard activation and \`disabled\` will not behave natively.`
|
|
160
|
+
);
|
|
161
|
+
} else if (!native && isButtonTag) {
|
|
162
|
+
console.error(
|
|
163
|
+
"A component rendered a native <button> while treating it as a non-native element, so it carries synthesised attributes it does not need."
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
}, [native]);
|
|
167
|
+
return { props: buttonProps({ disabled, native, props }), ref: elementRef };
|
|
168
|
+
}
|
|
169
|
+
function buttonProps({ disabled, native, props }) {
|
|
170
|
+
if (!disabled) {
|
|
171
|
+
return native ? mergeProps({ type: "button" }, props) : mergeProps({ tabIndex: 0 }, activationHandlers(), props);
|
|
172
|
+
}
|
|
173
|
+
if (native) return mergeProps({ type: "button" }, props, { disabled: true, "data-disabled": "" });
|
|
174
|
+
const sanitised = {};
|
|
175
|
+
for (const key of Object.keys(props)) {
|
|
176
|
+
if (key === "href" || ACTIVATION_HANDLERS.includes(key)) continue;
|
|
177
|
+
sanitised[key] = props[key];
|
|
178
|
+
}
|
|
179
|
+
return {
|
|
180
|
+
...sanitised,
|
|
181
|
+
"aria-disabled": true,
|
|
182
|
+
"data-disabled": "",
|
|
183
|
+
// Mirrors a native disabled button, which is not focusable.
|
|
184
|
+
tabIndex: -1
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
function retractActivationProps(props) {
|
|
188
|
+
const overrides = {};
|
|
189
|
+
for (const key of Object.keys(props)) {
|
|
190
|
+
if (key === "href" || ACTIVATION_HANDLERS.includes(key)) overrides[key] = void 0;
|
|
191
|
+
}
|
|
192
|
+
return overrides;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// src/core/useControlled.ts
|
|
196
|
+
var import_react2 = require("react");
|
|
197
|
+
function useControlled({
|
|
198
|
+
controlled,
|
|
199
|
+
default: defaultValue,
|
|
200
|
+
name = "Component",
|
|
201
|
+
state = "value"
|
|
202
|
+
}) {
|
|
203
|
+
const { current: isControlled } = (0, import_react2.useRef)(controlled !== void 0);
|
|
204
|
+
const [uncontrolled, setUncontrolled] = (0, import_react2.useState)(defaultValue);
|
|
205
|
+
const defaultRef = (0, import_react2.useRef)(defaultValue);
|
|
206
|
+
const value = isControlled ? controlled : uncontrolled;
|
|
207
|
+
if (process.env.NODE_ENV !== "production") {
|
|
208
|
+
if (isControlled !== (controlled !== void 0)) {
|
|
209
|
+
console.error(
|
|
210
|
+
`${name}: cannot switch between controlled and uncontrolled \`${state}\`. Decide which one this component is for the whole of its life.`
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
if (!isControlled && defaultRef.current !== defaultValue) {
|
|
214
|
+
console.error(
|
|
215
|
+
`${name}: cannot change the default \`${state}\` after mount. It is only read once, so later changes are silently ignored.`
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
const setValue = (0, import_react2.useCallback)(
|
|
220
|
+
(next) => {
|
|
221
|
+
if (!isControlled) setUncontrolled(next);
|
|
222
|
+
},
|
|
223
|
+
[isControlled]
|
|
224
|
+
);
|
|
225
|
+
return [value, setValue];
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// src/core/useRender.ts
|
|
229
|
+
var import_react3 = require("react");
|
|
230
|
+
|
|
126
231
|
// src/core/stateAttributes.ts
|
|
127
232
|
function getStateAttributes(state, mapping) {
|
|
128
233
|
if (!mapping) return {};
|
|
@@ -153,17 +258,17 @@ function useRender({
|
|
|
153
258
|
}) {
|
|
154
259
|
const attributes = state ? getStateAttributes(state, stateAttributes) : {};
|
|
155
260
|
const merged = mergeProps(attributes, ...props);
|
|
156
|
-
if ((0,
|
|
157
|
-
return (0,
|
|
261
|
+
if ((0, import_react3.isValidElement)(render)) {
|
|
262
|
+
return (0, import_react3.cloneElement)(render, mergeProps(merged, render.props));
|
|
158
263
|
}
|
|
159
|
-
return (0,
|
|
264
|
+
return (0, import_react3.createElement)(defaultTagName, merged);
|
|
160
265
|
}
|
|
161
266
|
|
|
162
267
|
// src/switch/SwitchRootContext.ts
|
|
163
|
-
var
|
|
164
|
-
var SwitchRootContext = (0,
|
|
268
|
+
var import_react4 = require("react");
|
|
269
|
+
var SwitchRootContext = (0, import_react4.createContext)(null);
|
|
165
270
|
function useSwitchRootContext() {
|
|
166
|
-
const context = (0,
|
|
271
|
+
const context = (0, import_react4.useContext)(SwitchRootContext);
|
|
167
272
|
if (context === null) {
|
|
168
273
|
throw new Error("<Switch.Thumb> must be rendered inside <Switch.Root>.");
|
|
169
274
|
}
|
|
@@ -196,27 +301,34 @@ function SwitchRoot({
|
|
|
196
301
|
name: "Switch.Root",
|
|
197
302
|
state: "checked"
|
|
198
303
|
});
|
|
199
|
-
const state = (0,
|
|
304
|
+
const state = (0, import_react5.useMemo)(() => ({ checked, disabled }), [checked, disabled]);
|
|
305
|
+
const isNativeButton = render === void 0 || render.type === "button";
|
|
306
|
+
const { props: consumerProps, ref: buttonRef } = useButton({
|
|
307
|
+
disabled,
|
|
308
|
+
native: isNativeButton,
|
|
309
|
+
props: rest
|
|
310
|
+
});
|
|
311
|
+
const safeRender = disabled && !isNativeButton && render !== void 0 ? (0, import_react5.cloneElement)(render, retractActivationProps(render.props)) : render;
|
|
200
312
|
const element = useRender({
|
|
201
|
-
render,
|
|
313
|
+
render: safeRender,
|
|
202
314
|
defaultTagName: "button",
|
|
203
315
|
state,
|
|
204
316
|
stateAttributes: switchStateAttributes,
|
|
205
317
|
props: [
|
|
206
318
|
{
|
|
207
|
-
type: "button",
|
|
208
319
|
role: "switch",
|
|
209
320
|
"aria-checked": checked,
|
|
210
|
-
disabled,
|
|
211
321
|
className,
|
|
212
322
|
children,
|
|
213
323
|
onClick() {
|
|
324
|
+
if (disabled) return;
|
|
214
325
|
const next = !checked;
|
|
215
326
|
setChecked(next);
|
|
216
327
|
onCheckedChange?.(next);
|
|
217
328
|
}
|
|
218
329
|
},
|
|
219
|
-
|
|
330
|
+
consumerProps,
|
|
331
|
+
{ ref: buttonRef }
|
|
220
332
|
]
|
|
221
333
|
});
|
|
222
334
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(SwitchRootContext.Provider, { value: state, children: [
|
package/dist/switch/index.d.cts
CHANGED
|
@@ -55,10 +55,11 @@ interface SwitchThumbProps {
|
|
|
55
55
|
* The moving part of the switch.
|
|
56
56
|
*
|
|
57
57
|
* Reads state from the Root rather than taking props, so a consumer cannot get the
|
|
58
|
-
* two out of step. Carries the same `data-*` attributes as the Root,
|
|
59
|
-
*
|
|
58
|
+
* two out of step. Carries the same `data-*` attributes as the Root, so it can be
|
|
59
|
+
* styled from either — on the thumb itself, or by descending from the Root:
|
|
60
60
|
*
|
|
61
|
-
* .
|
|
61
|
+
* .thumb[data-checked] { transform: translateX(100%); }
|
|
62
|
+
* .switch[data-checked] .thumb { transform: translateX(100%); }
|
|
62
63
|
*
|
|
63
64
|
* Purely presentational — hidden from assistive technology, since the Root already
|
|
64
65
|
* announces the state.
|
package/dist/switch/index.d.ts
CHANGED
|
@@ -55,10 +55,11 @@ interface SwitchThumbProps {
|
|
|
55
55
|
* The moving part of the switch.
|
|
56
56
|
*
|
|
57
57
|
* Reads state from the Root rather than taking props, so a consumer cannot get the
|
|
58
|
-
* two out of step. Carries the same `data-*` attributes as the Root,
|
|
59
|
-
*
|
|
58
|
+
* two out of step. Carries the same `data-*` attributes as the Root, so it can be
|
|
59
|
+
* styled from either — on the thumb itself, or by descending from the Root:
|
|
60
60
|
*
|
|
61
|
-
* .
|
|
61
|
+
* .thumb[data-checked] { transform: translateX(100%); }
|
|
62
|
+
* .switch[data-checked] .thumb { transform: translateX(100%); }
|
|
62
63
|
*
|
|
63
64
|
* Purely presentational — hidden from assistive technology, since the Root already
|
|
64
65
|
* announces the state.
|
package/dist/switch/index.js
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useControlled
|
|
3
|
+
} from "../chunk-UEIB3I3T.js";
|
|
4
|
+
import {
|
|
5
|
+
retractActivationProps,
|
|
6
|
+
useButton
|
|
7
|
+
} from "../chunk-2XSRAJHB.js";
|
|
1
8
|
import {
|
|
2
9
|
__export,
|
|
3
10
|
booleanAttribute,
|
|
4
11
|
disabledAttribute,
|
|
5
|
-
useControlled,
|
|
6
12
|
useRender
|
|
7
|
-
} from "../chunk-
|
|
13
|
+
} from "../chunk-HWG3GMTO.js";
|
|
8
14
|
|
|
9
15
|
// src/switch/index.parts.ts
|
|
10
16
|
var index_parts_exports = {};
|
|
@@ -14,7 +20,7 @@ __export(index_parts_exports, {
|
|
|
14
20
|
});
|
|
15
21
|
|
|
16
22
|
// src/switch/root/SwitchRoot.tsx
|
|
17
|
-
import { useMemo } from "react";
|
|
23
|
+
import { cloneElement, useMemo } from "react";
|
|
18
24
|
|
|
19
25
|
// src/switch/SwitchRootContext.ts
|
|
20
26
|
import { createContext, useContext } from "react";
|
|
@@ -54,26 +60,33 @@ function SwitchRoot({
|
|
|
54
60
|
state: "checked"
|
|
55
61
|
});
|
|
56
62
|
const state = useMemo(() => ({ checked, disabled }), [checked, disabled]);
|
|
63
|
+
const isNativeButton = render === void 0 || render.type === "button";
|
|
64
|
+
const { props: consumerProps, ref: buttonRef } = useButton({
|
|
65
|
+
disabled,
|
|
66
|
+
native: isNativeButton,
|
|
67
|
+
props: rest
|
|
68
|
+
});
|
|
69
|
+
const safeRender = disabled && !isNativeButton && render !== void 0 ? cloneElement(render, retractActivationProps(render.props)) : render;
|
|
57
70
|
const element = useRender({
|
|
58
|
-
render,
|
|
71
|
+
render: safeRender,
|
|
59
72
|
defaultTagName: "button",
|
|
60
73
|
state,
|
|
61
74
|
stateAttributes: switchStateAttributes,
|
|
62
75
|
props: [
|
|
63
76
|
{
|
|
64
|
-
type: "button",
|
|
65
77
|
role: "switch",
|
|
66
78
|
"aria-checked": checked,
|
|
67
|
-
disabled,
|
|
68
79
|
className,
|
|
69
80
|
children,
|
|
70
81
|
onClick() {
|
|
82
|
+
if (disabled) return;
|
|
71
83
|
const next = !checked;
|
|
72
84
|
setChecked(next);
|
|
73
85
|
onCheckedChange?.(next);
|
|
74
86
|
}
|
|
75
87
|
},
|
|
76
|
-
|
|
88
|
+
consumerProps,
|
|
89
|
+
{ ref: buttonRef }
|
|
77
90
|
]
|
|
78
91
|
});
|
|
79
92
|
return /* @__PURE__ */ jsxs(SwitchRootContext.Provider, { value: state, children: [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arun-dev/headless",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.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",
|
|
@@ -34,6 +34,16 @@
|
|
|
34
34
|
"default": "./dist/index.cjs"
|
|
35
35
|
}
|
|
36
36
|
},
|
|
37
|
+
"./button": {
|
|
38
|
+
"import": {
|
|
39
|
+
"types": "./dist/button/index.d.ts",
|
|
40
|
+
"default": "./dist/button/index.js"
|
|
41
|
+
},
|
|
42
|
+
"require": {
|
|
43
|
+
"types": "./dist/button/index.d.cts",
|
|
44
|
+
"default": "./dist/button/index.cjs"
|
|
45
|
+
}
|
|
46
|
+
},
|
|
37
47
|
"./switch": {
|
|
38
48
|
"import": {
|
|
39
49
|
"types": "./dist/switch/index.d.ts",
|