@arun-dev/headless 0.4.0 → 2.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 +18 -0
- package/dist/button/index.cjs +253 -0
- package/dist/button/index.d.cts +44 -0
- package/dist/button/index.d.ts +44 -0
- package/dist/button/index.js +33 -0
- package/dist/chunk-ER7IAVR5.js +96 -0
- package/dist/{chunk-I477HMLD.js → chunk-IGKX2XWC.js} +13 -83
- package/dist/chunk-UEIB3I3T.js +36 -0
- package/dist/index.cjs +12 -50
- package/dist/index.d.cts +12 -26
- package/dist/index.d.ts +12 -26
- package/dist/index.js +4 -6
- package/dist/switch/index.cjs +168 -100
- package/dist/switch/index.d.cts +14 -4
- package/dist/switch/index.d.ts +14 -4
- package/dist/switch/index.js +34 -24
- 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.
|
|
@@ -105,3 +117,9 @@ import {
|
|
|
105
117
|
| `mergeProps` | Merges prop objects: handlers chain, `className` concatenates, refs merge |
|
|
106
118
|
| `getStateAttributes` | Projects a state object onto `data-*` attributes via a declared mapping |
|
|
107
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,253 @@
|
|
|
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 chainHandlers(earlier, value) {
|
|
72
|
+
return (...args) => {
|
|
73
|
+
const event = args[0];
|
|
74
|
+
if (isSyntheticEvent(event)) makeEventPreventable(event);
|
|
75
|
+
value(...args);
|
|
76
|
+
if (isComponentHandlerPrevented(event)) return;
|
|
77
|
+
earlier(...args);
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function mergeProps(...objects) {
|
|
81
|
+
const merged = {};
|
|
82
|
+
for (const props of objects) {
|
|
83
|
+
if (!props) continue;
|
|
84
|
+
for (const key of Object.keys(props)) {
|
|
85
|
+
const value = props[key];
|
|
86
|
+
if (value === void 0) continue;
|
|
87
|
+
if (isEventHandler(key) && typeof value === "function") {
|
|
88
|
+
const earlier = merged[key];
|
|
89
|
+
merged[key] = typeof earlier === "function" ? chainHandlers(earlier, value) : value;
|
|
90
|
+
} else if (key === "className") {
|
|
91
|
+
merged.className = merged.className ? `${String(merged.className)} ${String(value)}` : value;
|
|
92
|
+
} else if (key === "style") {
|
|
93
|
+
merged.style = { ...merged.style, ...value };
|
|
94
|
+
} else if (key === "ref") {
|
|
95
|
+
merged.ref = merged.ref ? mergeRefs(merged.ref, value) : value;
|
|
96
|
+
} else {
|
|
97
|
+
merged[key] = value;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return merged;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// src/core/stateAttributes.ts
|
|
105
|
+
function getStateAttributes(state, mapping) {
|
|
106
|
+
if (!mapping) return {};
|
|
107
|
+
const attributes = {};
|
|
108
|
+
for (const key of Object.keys(state)) {
|
|
109
|
+
const toAttributes = mapping[key];
|
|
110
|
+
if (!toAttributes) continue;
|
|
111
|
+
const result = toAttributes(state[key]);
|
|
112
|
+
if (result) Object.assign(attributes, result);
|
|
113
|
+
}
|
|
114
|
+
return attributes;
|
|
115
|
+
}
|
|
116
|
+
function booleanAttribute(whenTrue, whenFalse) {
|
|
117
|
+
return (value) => {
|
|
118
|
+
if (value) return { [whenTrue]: "" };
|
|
119
|
+
return whenFalse ? { [whenFalse]: "" } : null;
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
var disabledAttribute = booleanAttribute("data-disabled");
|
|
123
|
+
|
|
124
|
+
// src/core/useRender.ts
|
|
125
|
+
function useRender({
|
|
126
|
+
render,
|
|
127
|
+
defaultTagName,
|
|
128
|
+
props,
|
|
129
|
+
consumerProps,
|
|
130
|
+
state,
|
|
131
|
+
stateAttributes
|
|
132
|
+
}) {
|
|
133
|
+
const attributes = state ? getStateAttributes(state, stateAttributes) : {};
|
|
134
|
+
const merged = mergeProps(attributes, props, consumerProps);
|
|
135
|
+
if ((0, import_react.isValidElement)(render)) {
|
|
136
|
+
return (0, import_react.cloneElement)(render, mergeProps(merged, render.props));
|
|
137
|
+
}
|
|
138
|
+
return (0, import_react.createElement)(defaultTagName, merged);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// src/useButton.ts
|
|
142
|
+
var import_react2 = require("react");
|
|
143
|
+
var ACTIVATION_HANDLERS = [
|
|
144
|
+
"onClick",
|
|
145
|
+
"onDoubleClick",
|
|
146
|
+
"onMouseDown",
|
|
147
|
+
"onMouseUp",
|
|
148
|
+
"onPointerDown",
|
|
149
|
+
"onPointerUp",
|
|
150
|
+
"onTouchStart",
|
|
151
|
+
"onTouchEnd",
|
|
152
|
+
"onKeyDown",
|
|
153
|
+
"onKeyUp",
|
|
154
|
+
"onKeyPress"
|
|
155
|
+
];
|
|
156
|
+
function activationHandlers() {
|
|
157
|
+
return {
|
|
158
|
+
onKeyDown(event) {
|
|
159
|
+
if (event.target !== event.currentTarget || event.defaultPrevented) return;
|
|
160
|
+
if (event.key === " ") {
|
|
161
|
+
event.preventDefault();
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (event.key === "Enter") {
|
|
165
|
+
event.preventDefault();
|
|
166
|
+
click(event.currentTarget);
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
onKeyUp(event) {
|
|
170
|
+
if (event.target !== event.currentTarget || event.defaultPrevented) return;
|
|
171
|
+
if (event.key === " ") click(event.currentTarget);
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
function click(element) {
|
|
176
|
+
if (element && typeof element.click === "function") {
|
|
177
|
+
element.click();
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
function useButton({
|
|
181
|
+
disabled = false,
|
|
182
|
+
native,
|
|
183
|
+
props = {}
|
|
184
|
+
}) {
|
|
185
|
+
const elementRef = (0, import_react2.useRef)(null);
|
|
186
|
+
(0, import_react2.useEffect)(() => {
|
|
187
|
+
if (process.env.NODE_ENV === "production") return;
|
|
188
|
+
const element = elementRef.current;
|
|
189
|
+
if (!element) return;
|
|
190
|
+
const isButtonTag = element.tagName === "BUTTON";
|
|
191
|
+
if (native && !isButtonTag) {
|
|
192
|
+
console.error(
|
|
193
|
+
`A component expected a native <button> but rendered <${element.tagName.toLowerCase()}>. Focus, keyboard activation and \`disabled\` will not behave natively.`
|
|
194
|
+
);
|
|
195
|
+
} else if (!native && isButtonTag) {
|
|
196
|
+
console.error(
|
|
197
|
+
"A component rendered a native <button> while treating it as a non-native element, so it carries synthesised attributes it does not need."
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
}, [native]);
|
|
201
|
+
return { props: buttonProps({ disabled, native, props }), ref: elementRef };
|
|
202
|
+
}
|
|
203
|
+
function buttonProps({ disabled, native, props }) {
|
|
204
|
+
if (!disabled) {
|
|
205
|
+
return native ? mergeProps({ type: "button" }, props) : mergeProps({ tabIndex: 0 }, activationHandlers(), props);
|
|
206
|
+
}
|
|
207
|
+
if (native) return mergeProps({ type: "button" }, props, { disabled: true, "data-disabled": "" });
|
|
208
|
+
const sanitised = {};
|
|
209
|
+
for (const key of Object.keys(props)) {
|
|
210
|
+
if (key === "href" || ACTIVATION_HANDLERS.includes(key)) continue;
|
|
211
|
+
sanitised[key] = props[key];
|
|
212
|
+
}
|
|
213
|
+
return {
|
|
214
|
+
...sanitised,
|
|
215
|
+
"aria-disabled": true,
|
|
216
|
+
"data-disabled": "",
|
|
217
|
+
// Mirrors a native disabled button, which is not focusable.
|
|
218
|
+
tabIndex: -1
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
function retractActivationProps(props) {
|
|
222
|
+
const overrides = {};
|
|
223
|
+
for (const key of Object.keys(props)) {
|
|
224
|
+
if (key === "href" || ACTIVATION_HANDLERS.includes(key)) overrides[key] = void 0;
|
|
225
|
+
}
|
|
226
|
+
return overrides;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// src/button/Button.tsx
|
|
230
|
+
function Button({
|
|
231
|
+
disabled = false,
|
|
232
|
+
nativeButton,
|
|
233
|
+
className,
|
|
234
|
+
children,
|
|
235
|
+
render,
|
|
236
|
+
...rest
|
|
237
|
+
}) {
|
|
238
|
+
const native = nativeButton ?? (render === void 0 || render.type === "button");
|
|
239
|
+
const isAnchor = render !== void 0 && render.type === "a";
|
|
240
|
+
const role = native || isAnchor ? void 0 : "button";
|
|
241
|
+
const { props: elementProps, ref: buttonRef } = useButton({ disabled, native, props: rest });
|
|
242
|
+
const safeRender = disabled && !native && render !== void 0 ? (0, import_react3.cloneElement)(render, retractActivationProps(render.props)) : render;
|
|
243
|
+
return useRender({
|
|
244
|
+
render: safeRender,
|
|
245
|
+
defaultTagName: "button",
|
|
246
|
+
props: { role, className, children, ref: buttonRef },
|
|
247
|
+
consumerProps: elementProps
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
251
|
+
0 && (module.exports = {
|
|
252
|
+
Button
|
|
253
|
+
});
|
|
@@ -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,33 @@
|
|
|
1
|
+
import {
|
|
2
|
+
retractActivationProps,
|
|
3
|
+
useButton
|
|
4
|
+
} from "../chunk-ER7IAVR5.js";
|
|
5
|
+
import {
|
|
6
|
+
useRender
|
|
7
|
+
} from "../chunk-IGKX2XWC.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, ref: buttonRef },
|
|
28
|
+
consumerProps: elementProps
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
export {
|
|
32
|
+
Button
|
|
33
|
+
};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import {
|
|
2
|
+
mergeProps
|
|
3
|
+
} from "./chunk-IGKX2XWC.js";
|
|
4
|
+
|
|
5
|
+
// src/useButton.ts
|
|
6
|
+
import { useEffect, useRef } from "react";
|
|
7
|
+
var ACTIVATION_HANDLERS = [
|
|
8
|
+
"onClick",
|
|
9
|
+
"onDoubleClick",
|
|
10
|
+
"onMouseDown",
|
|
11
|
+
"onMouseUp",
|
|
12
|
+
"onPointerDown",
|
|
13
|
+
"onPointerUp",
|
|
14
|
+
"onTouchStart",
|
|
15
|
+
"onTouchEnd",
|
|
16
|
+
"onKeyDown",
|
|
17
|
+
"onKeyUp",
|
|
18
|
+
"onKeyPress"
|
|
19
|
+
];
|
|
20
|
+
function activationHandlers() {
|
|
21
|
+
return {
|
|
22
|
+
onKeyDown(event) {
|
|
23
|
+
if (event.target !== event.currentTarget || event.defaultPrevented) return;
|
|
24
|
+
if (event.key === " ") {
|
|
25
|
+
event.preventDefault();
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (event.key === "Enter") {
|
|
29
|
+
event.preventDefault();
|
|
30
|
+
click(event.currentTarget);
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
onKeyUp(event) {
|
|
34
|
+
if (event.target !== event.currentTarget || event.defaultPrevented) return;
|
|
35
|
+
if (event.key === " ") click(event.currentTarget);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function click(element) {
|
|
40
|
+
if (element && typeof element.click === "function") {
|
|
41
|
+
element.click();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function useButton({
|
|
45
|
+
disabled = false,
|
|
46
|
+
native,
|
|
47
|
+
props = {}
|
|
48
|
+
}) {
|
|
49
|
+
const elementRef = useRef(null);
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
if (process.env.NODE_ENV === "production") return;
|
|
52
|
+
const element = elementRef.current;
|
|
53
|
+
if (!element) return;
|
|
54
|
+
const isButtonTag = element.tagName === "BUTTON";
|
|
55
|
+
if (native && !isButtonTag) {
|
|
56
|
+
console.error(
|
|
57
|
+
`A component expected a native <button> but rendered <${element.tagName.toLowerCase()}>. Focus, keyboard activation and \`disabled\` will not behave natively.`
|
|
58
|
+
);
|
|
59
|
+
} else if (!native && isButtonTag) {
|
|
60
|
+
console.error(
|
|
61
|
+
"A component rendered a native <button> while treating it as a non-native element, so it carries synthesised attributes it does not need."
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
}, [native]);
|
|
65
|
+
return { props: buttonProps({ disabled, native, props }), ref: elementRef };
|
|
66
|
+
}
|
|
67
|
+
function buttonProps({ disabled, native, props }) {
|
|
68
|
+
if (!disabled) {
|
|
69
|
+
return native ? mergeProps({ type: "button" }, props) : mergeProps({ tabIndex: 0 }, activationHandlers(), props);
|
|
70
|
+
}
|
|
71
|
+
if (native) return mergeProps({ type: "button" }, props, { disabled: true, "data-disabled": "" });
|
|
72
|
+
const sanitised = {};
|
|
73
|
+
for (const key of Object.keys(props)) {
|
|
74
|
+
if (key === "href" || ACTIVATION_HANDLERS.includes(key)) continue;
|
|
75
|
+
sanitised[key] = props[key];
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
...sanitised,
|
|
79
|
+
"aria-disabled": true,
|
|
80
|
+
"data-disabled": "",
|
|
81
|
+
// Mirrors a native disabled button, which is not focusable.
|
|
82
|
+
tabIndex: -1
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function retractActivationProps(props) {
|
|
86
|
+
const overrides = {};
|
|
87
|
+
for (const key of Object.keys(props)) {
|
|
88
|
+
if (key === "href" || ACTIVATION_HANDLERS.includes(key)) overrides[key] = void 0;
|
|
89
|
+
}
|
|
90
|
+
return overrides;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export {
|
|
94
|
+
useButton,
|
|
95
|
+
retractActivationProps
|
|
96
|
+
};
|
|
@@ -42,6 +42,15 @@ function makeEventPreventable(event) {
|
|
|
42
42
|
function isComponentHandlerPrevented(event) {
|
|
43
43
|
return typeof event === "object" && event !== null && event.componentHandlerPrevented === true;
|
|
44
44
|
}
|
|
45
|
+
function chainHandlers(earlier, value) {
|
|
46
|
+
return (...args) => {
|
|
47
|
+
const event = args[0];
|
|
48
|
+
if (isSyntheticEvent(event)) makeEventPreventable(event);
|
|
49
|
+
value(...args);
|
|
50
|
+
if (isComponentHandlerPrevented(event)) return;
|
|
51
|
+
earlier(...args);
|
|
52
|
+
};
|
|
53
|
+
}
|
|
45
54
|
function mergeProps(...objects) {
|
|
46
55
|
const merged = {};
|
|
47
56
|
for (const props of objects) {
|
|
@@ -51,13 +60,7 @@ function mergeProps(...objects) {
|
|
|
51
60
|
if (value === void 0) continue;
|
|
52
61
|
if (isEventHandler(key) && typeof value === "function") {
|
|
53
62
|
const earlier = merged[key];
|
|
54
|
-
merged[key] = typeof earlier === "function" ? (
|
|
55
|
-
const event = args[0];
|
|
56
|
-
if (isSyntheticEvent(event)) makeEventPreventable(event);
|
|
57
|
-
value(...args);
|
|
58
|
-
if (isComponentHandlerPrevented(event)) return;
|
|
59
|
-
earlier(...args);
|
|
60
|
-
} : value;
|
|
63
|
+
merged[key] = typeof earlier === "function" ? chainHandlers(earlier, value) : value;
|
|
61
64
|
} else if (key === "className") {
|
|
62
65
|
merged.className = merged.className ? `${String(merged.className)} ${String(value)}` : value;
|
|
63
66
|
} else if (key === "style") {
|
|
@@ -98,96 +101,23 @@ function useRender({
|
|
|
98
101
|
render,
|
|
99
102
|
defaultTagName,
|
|
100
103
|
props,
|
|
104
|
+
consumerProps,
|
|
101
105
|
state,
|
|
102
106
|
stateAttributes
|
|
103
107
|
}) {
|
|
104
108
|
const attributes = state ? getStateAttributes(state, stateAttributes) : {};
|
|
105
|
-
const merged = mergeProps(attributes,
|
|
109
|
+
const merged = mergeProps(attributes, props, consumerProps);
|
|
106
110
|
if (isValidElement(render)) {
|
|
107
111
|
return cloneElement(render, mergeProps(merged, render.props));
|
|
108
112
|
}
|
|
109
113
|
return createElement(defaultTagName, merged);
|
|
110
114
|
}
|
|
111
115
|
|
|
112
|
-
// src/core/useControlled.ts
|
|
113
|
-
import { useCallback, useRef, useState } from "react";
|
|
114
|
-
function useControlled({
|
|
115
|
-
controlled,
|
|
116
|
-
default: defaultValue,
|
|
117
|
-
name = "Component",
|
|
118
|
-
state = "value"
|
|
119
|
-
}) {
|
|
120
|
-
const { current: isControlled } = useRef(controlled !== void 0);
|
|
121
|
-
const [uncontrolled, setUncontrolled] = useState(defaultValue);
|
|
122
|
-
const defaultRef = useRef(defaultValue);
|
|
123
|
-
const value = isControlled ? controlled : uncontrolled;
|
|
124
|
-
if (process.env.NODE_ENV !== "production") {
|
|
125
|
-
if (isControlled !== (controlled !== void 0)) {
|
|
126
|
-
console.error(
|
|
127
|
-
`${name}: cannot switch between controlled and uncontrolled \`${state}\`. Decide which one this component is for the whole of its life.`
|
|
128
|
-
);
|
|
129
|
-
}
|
|
130
|
-
if (!isControlled && defaultRef.current !== defaultValue) {
|
|
131
|
-
console.error(
|
|
132
|
-
`${name}: cannot change the default \`${state}\` after mount. It is only read once, so later changes are silently ignored.`
|
|
133
|
-
);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
const setValue = useCallback(
|
|
137
|
-
(next) => {
|
|
138
|
-
if (!isControlled) setUncontrolled(next);
|
|
139
|
-
},
|
|
140
|
-
[isControlled]
|
|
141
|
-
);
|
|
142
|
-
return [value, setValue];
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// src/useButton.ts
|
|
146
|
-
var ACTIVATION_HANDLERS = [
|
|
147
|
-
"onClick",
|
|
148
|
-
"onDoubleClick",
|
|
149
|
-
"onMouseDown",
|
|
150
|
-
"onMouseUp",
|
|
151
|
-
"onPointerDown",
|
|
152
|
-
"onPointerUp",
|
|
153
|
-
"onTouchStart",
|
|
154
|
-
"onTouchEnd",
|
|
155
|
-
"onKeyDown",
|
|
156
|
-
"onKeyUp",
|
|
157
|
-
"onKeyPress"
|
|
158
|
-
];
|
|
159
|
-
function useButton({ disabled = false, native, props = {} }) {
|
|
160
|
-
if (!disabled) return props;
|
|
161
|
-
if (native) return { ...props, disabled: true, "data-disabled": "" };
|
|
162
|
-
const sanitised = {};
|
|
163
|
-
for (const key of Object.keys(props)) {
|
|
164
|
-
if (key === "href" || ACTIVATION_HANDLERS.includes(key)) continue;
|
|
165
|
-
sanitised[key] = props[key];
|
|
166
|
-
}
|
|
167
|
-
return {
|
|
168
|
-
...sanitised,
|
|
169
|
-
"aria-disabled": true,
|
|
170
|
-
"data-disabled": "",
|
|
171
|
-
// Mirrors a native disabled button, which is not focusable.
|
|
172
|
-
tabIndex: -1
|
|
173
|
-
};
|
|
174
|
-
}
|
|
175
|
-
function retractActivationProps(props) {
|
|
176
|
-
const overrides = {};
|
|
177
|
-
for (const key of Object.keys(props)) {
|
|
178
|
-
if (key === "href" || ACTIVATION_HANDLERS.includes(key)) overrides[key] = void 0;
|
|
179
|
-
}
|
|
180
|
-
return overrides;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
116
|
export {
|
|
184
117
|
__export,
|
|
185
118
|
mergeProps,
|
|
186
119
|
getStateAttributes,
|
|
187
120
|
booleanAttribute,
|
|
188
121
|
disabledAttribute,
|
|
189
|
-
useRender
|
|
190
|
-
useControlled,
|
|
191
|
-
useButton,
|
|
192
|
-
retractActivationProps
|
|
122
|
+
useRender
|
|
193
123
|
};
|
|
@@ -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
|
+
};
|