@uniflowed/ui 0.0.0-alpha.4 → 0.0.0-alpha.5
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/checkbox.js +2 -1
- package/combobox.js +8 -10
- package/dialog.js +9 -8
- package/field.js +7 -9
- package/internal/merge-props.js +56 -6
- package/menu.js +8 -7
- package/package.json +3 -3
- package/switch.js +2 -1
- package/tabs.js +5 -8
package/checkbox.js
CHANGED
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
|
|
32
32
|
import * as React from "@uniflowed/react";
|
|
33
33
|
|
|
34
|
+
import type { Rest } from "./internal/merge-props.js";
|
|
34
35
|
import { composeHandlers, withoutComposed } from "./internal/merge-props.js";
|
|
35
36
|
import { useControlled } from "./internal/controlled-state.js";
|
|
36
37
|
|
|
@@ -42,7 +43,7 @@ export component Checkbox(
|
|
|
42
43
|
onCheckedChange?: (checked: boolean) => void,
|
|
43
44
|
disabled?: boolean = false,
|
|
44
45
|
children?: React.Node,
|
|
45
|
-
...rest:
|
|
46
|
+
...rest: Rest
|
|
46
47
|
) {
|
|
47
48
|
const [on, setOn] = useControlled(checked, defaultChecked, onCheckedChange);
|
|
48
49
|
// A mixed checkbox moves to checked, not to "the opposite of the boolean
|
package/combobox.js
CHANGED
|
@@ -64,6 +64,7 @@ import {
|
|
|
64
64
|
} from "@uniflowed/react";
|
|
65
65
|
import { useStableCallback } from "@uniflowed/hooks/lifecycle";
|
|
66
66
|
|
|
67
|
+
import type { Rest } from "./internal/merge-props.js";
|
|
67
68
|
import { composeHandlers, composeRefs, withoutComposed } from "./internal/merge-props.js";
|
|
68
69
|
import { itemsOf, moveTo } from "./internal/roving-focus.js";
|
|
69
70
|
import { useControlled } from "./internal/controlled-state.js";
|
|
@@ -134,7 +135,7 @@ export component ComboboxRoot(
|
|
|
134
135
|
open?: boolean,
|
|
135
136
|
defaultOpen?: boolean = false,
|
|
136
137
|
onOpenChange?: (open: boolean) => void,
|
|
137
|
-
...rest:
|
|
138
|
+
...rest: Rest
|
|
138
139
|
) {
|
|
139
140
|
const base = useId();
|
|
140
141
|
const [chosen, setChosen] = useControlled(value, defaultValue, onValueChange);
|
|
@@ -203,7 +204,7 @@ export component ComboboxRoot(
|
|
|
203
204
|
* because the list names it, and naming a label that is not rendered is worse
|
|
204
205
|
* than leaving the list unnamed.
|
|
205
206
|
*/
|
|
206
|
-
export component ComboboxLabel(children: React.Node, ...rest:
|
|
207
|
+
export component ComboboxLabel(children: React.Node, ...rest: Rest) {
|
|
207
208
|
const combobox = useCombobox("Combobox.Label");
|
|
208
209
|
const register = combobox.registerLabel;
|
|
209
210
|
useEffect(() => {
|
|
@@ -219,7 +220,7 @@ export component ComboboxLabel(children: React.Node, ...rest: { readonly [string
|
|
|
219
220
|
}
|
|
220
221
|
|
|
221
222
|
/** The text field, and every key the pattern defines. */
|
|
222
|
-
export component ComboboxInput(...rest:
|
|
223
|
+
export component ComboboxInput(...rest: Rest) {
|
|
223
224
|
const combobox = useCombobox("Combobox.Input");
|
|
224
225
|
const passed = withoutComposed(rest, ["onChange", "onKeyDown", "ref"]);
|
|
225
226
|
|
|
@@ -337,10 +338,7 @@ export component ComboboxInput(...rest: { readonly [string]: mixed }) {
|
|
|
337
338
|
* the count the live region announces, and the invariant that
|
|
338
339
|
* `aria-activedescendant` never names an option that has left the list.
|
|
339
340
|
*/
|
|
340
|
-
export component ComboboxList(
|
|
341
|
-
children: renders* ComboboxOption,
|
|
342
|
-
...rest: { readonly [string]: mixed }
|
|
343
|
-
) {
|
|
341
|
+
export component ComboboxList(children: renders* ComboboxOption, ...rest: Rest) {
|
|
344
342
|
const combobox = useCombobox("Combobox.List");
|
|
345
343
|
const { activeId, count, listRef, inputRef, pendingActive, setActiveId, setCount } = combobox;
|
|
346
344
|
const close = useStableCallback(() => {
|
|
@@ -444,7 +442,7 @@ export component ComboboxOption(
|
|
|
444
442
|
children: React.Node,
|
|
445
443
|
label?: string,
|
|
446
444
|
disabled?: boolean = false,
|
|
447
|
-
...rest:
|
|
445
|
+
...rest: Rest
|
|
448
446
|
) {
|
|
449
447
|
const combobox = useCombobox("Combobox.Option");
|
|
450
448
|
const id = useId();
|
|
@@ -495,7 +493,7 @@ export component ComboboxOption(
|
|
|
495
493
|
* contain options: an "no matches" row inside one is announced as an option a
|
|
496
494
|
* reader can choose, and choosing it does nothing.
|
|
497
495
|
*/
|
|
498
|
-
export component ComboboxEmpty(children: React.Node, ...rest:
|
|
496
|
+
export component ComboboxEmpty(children: React.Node, ...rest: Rest) {
|
|
499
497
|
const combobox = useCombobox("Combobox.Empty");
|
|
500
498
|
if (!combobox.open || combobox.count > 0) {
|
|
501
499
|
return null;
|
|
@@ -514,7 +512,7 @@ export component ComboboxEmpty(children: React.Node, ...rest: { readonly [string
|
|
|
514
512
|
* `children` overrides the wording — the default is English and a real
|
|
515
513
|
* application has a translation table.
|
|
516
514
|
*/
|
|
517
|
-
export component ComboboxStatus(children?: React.Node, ...rest:
|
|
515
|
+
export component ComboboxStatus(children?: React.Node, ...rest: Rest) {
|
|
518
516
|
const combobox = useCombobox("Combobox.Status");
|
|
519
517
|
const message = children ?? defaultAnnouncement(combobox.open, combobox.count);
|
|
520
518
|
|
package/dialog.js
CHANGED
|
@@ -51,6 +51,7 @@ import {
|
|
|
51
51
|
} from "@uniflowed/react";
|
|
52
52
|
import { useStableCallback } from "@uniflowed/hooks/lifecycle";
|
|
53
53
|
|
|
54
|
+
import type { Rest } from "./internal/merge-props.js";
|
|
54
55
|
import { composeHandlers, composeRefs, withoutComposed } from "./internal/merge-props.js";
|
|
55
56
|
import { useControlled } from "./internal/controlled-state.js";
|
|
56
57
|
|
|
@@ -114,7 +115,7 @@ export component DialogRoot(
|
|
|
114
115
|
}
|
|
115
116
|
|
|
116
117
|
/** What opens the dialog, and what focus comes back to when it closes. */
|
|
117
|
-
export component DialogTrigger(children: React.Node, ...rest:
|
|
118
|
+
export component DialogTrigger(children: React.Node, ...rest: Rest) {
|
|
118
119
|
const dialog = useDialog("Dialog.Trigger");
|
|
119
120
|
const passed = withoutComposed(rest, ["onClick", "ref"]);
|
|
120
121
|
|
|
@@ -147,7 +148,7 @@ export component DialogTrigger(children: React.Node, ...rest: { readonly [string
|
|
|
147
148
|
* their own backdrop or omits one entirely must still get it. That lives on
|
|
148
149
|
* `Dialog.Body`, which is the part that knows where "outside" is.
|
|
149
150
|
*/
|
|
150
|
-
export component DialogOverlay(...rest:
|
|
151
|
+
export component DialogOverlay(...rest: Rest) {
|
|
151
152
|
const dialog = useDialog("Dialog.Overlay");
|
|
152
153
|
if (!dialog.open) {
|
|
153
154
|
return null;
|
|
@@ -162,7 +163,7 @@ export component DialogOverlay(...rest: { readonly [string]: mixed }) {
|
|
|
162
163
|
* which is the half of "modal" that CSS cannot express; `inert` on everything
|
|
163
164
|
* outside is the half the browser enforces.
|
|
164
165
|
*/
|
|
165
|
-
export component DialogBody(children: React.Node, ...rest:
|
|
166
|
+
export component DialogBody(children: React.Node, ...rest: Rest) {
|
|
166
167
|
const dialog = useDialog("Dialog.Body");
|
|
167
168
|
const bodyRef = useRef<HTMLElement | null>(null);
|
|
168
169
|
// Stable, so the effect below depends on `open` and on nothing else. Keyed on
|
|
@@ -298,7 +299,7 @@ export component DialogBody(children: React.Node, ...rest: { readonly [string]:
|
|
|
298
299
|
* rendered — a conditional title that is absent used to leave the dialog
|
|
299
300
|
* pointing at an id nothing had.
|
|
300
301
|
*/
|
|
301
|
-
export component DialogTitle(children: React.Node, ...rest:
|
|
302
|
+
export component DialogTitle(children: React.Node, ...rest: Rest) {
|
|
302
303
|
const dialog = useDialog("Dialog.Title");
|
|
303
304
|
const register = dialog.registerTitle;
|
|
304
305
|
useEffect(() => {
|
|
@@ -320,7 +321,7 @@ export component DialogTitle(children: React.Node, ...rest: { readonly [string]:
|
|
|
320
321
|
* the one moment the reader has to decide whether they care — so this is where
|
|
321
322
|
* "this cannot be undone" belongs, not in body text further down.
|
|
322
323
|
*/
|
|
323
|
-
export component DialogDescription(children: React.Node, ...rest:
|
|
324
|
+
export component DialogDescription(children: React.Node, ...rest: Rest) {
|
|
324
325
|
const dialog = useDialog("Dialog.Description");
|
|
325
326
|
const register = dialog.registerDescription;
|
|
326
327
|
useEffect(() => {
|
|
@@ -344,17 +345,17 @@ export component DialogDescription(children: React.Node, ...rest: { readonly [st
|
|
|
344
345
|
* styling layer has a name to attach to, and contributes no semantics because
|
|
345
346
|
* it has none to contribute.
|
|
346
347
|
*/
|
|
347
|
-
export component DialogHeader(children: React.Node, ...rest:
|
|
348
|
+
export component DialogHeader(children: React.Node, ...rest: Rest) {
|
|
348
349
|
return <div {...rest}>{children}</div>;
|
|
349
350
|
}
|
|
350
351
|
|
|
351
352
|
/** The bottom of the dialog, where the actions go. See `Dialog.Header`. */
|
|
352
|
-
export component DialogFooter(children: React.Node, ...rest:
|
|
353
|
+
export component DialogFooter(children: React.Node, ...rest: Rest) {
|
|
353
354
|
return <div {...rest}>{children}</div>;
|
|
354
355
|
}
|
|
355
356
|
|
|
356
357
|
/** A button that closes the dialog. */
|
|
357
|
-
export component DialogClose(children: React.Node, ...rest:
|
|
358
|
+
export component DialogClose(children: React.Node, ...rest: Rest) {
|
|
358
359
|
const dialog = useDialog("Dialog.Close");
|
|
359
360
|
const passed = withoutComposed(rest, ["onClick"]);
|
|
360
361
|
|
package/field.js
CHANGED
|
@@ -31,6 +31,8 @@
|
|
|
31
31
|
import * as React from "@uniflowed/react";
|
|
32
32
|
import { createContext, useContext, useEffect, useId, useMemo, useState } from "@uniflowed/react";
|
|
33
33
|
|
|
34
|
+
import type { Rest } from "./internal/merge-props.js";
|
|
35
|
+
|
|
34
36
|
type FieldState = {|
|
|
35
37
|
readonly controlId: string,
|
|
36
38
|
readonly labelId: string,
|
|
@@ -66,11 +68,7 @@ hook useField(part: string): FieldState {
|
|
|
66
68
|
* message is rendered or not, and the control's `aria-describedby` includes the
|
|
67
69
|
* error's id or not.
|
|
68
70
|
*/
|
|
69
|
-
export component FieldRoot(
|
|
70
|
-
children: React.Node,
|
|
71
|
-
invalid?: boolean = false,
|
|
72
|
-
...rest: { readonly [string]: mixed }
|
|
73
|
-
) {
|
|
71
|
+
export component FieldRoot(children: React.Node, invalid?: boolean = false, ...rest: Rest) {
|
|
74
72
|
const base = useId();
|
|
75
73
|
const [hasDescription, setHasDescription] = useState(false);
|
|
76
74
|
const [hasError, setHasError] = useState(false);
|
|
@@ -105,7 +103,7 @@ export component FieldRoot(
|
|
|
105
103
|
}
|
|
106
104
|
|
|
107
105
|
/** The label, pointing at the control by id rather than by nesting. */
|
|
108
|
-
export component FieldLabel(children: React.Node, ...rest:
|
|
106
|
+
export component FieldLabel(children: React.Node, ...rest: Rest) {
|
|
109
107
|
const field = useField("Field.Label");
|
|
110
108
|
// `rest` first: a caller `id` here would break the relationship the control
|
|
111
109
|
// points at, and it would break it silently.
|
|
@@ -121,7 +119,7 @@ export component FieldLabel(children: React.Node, ...rest: { readonly [string]:
|
|
|
121
119
|
*
|
|
122
120
|
* See the module header for why this takes a render function.
|
|
123
121
|
*/
|
|
124
|
-
export component FieldControl(render: (props:
|
|
122
|
+
export component FieldControl(render: (props: Rest) => React.Node) {
|
|
125
123
|
const field = useField("Field.Control");
|
|
126
124
|
return render({
|
|
127
125
|
id: field.controlId,
|
|
@@ -132,7 +130,7 @@ export component FieldControl(render: (props: { readonly [string]: mixed }) => R
|
|
|
132
130
|
}
|
|
133
131
|
|
|
134
132
|
/** Help text, which the control points at while it is rendered. */
|
|
135
|
-
export component FieldDescription(children: React.Node, ...rest:
|
|
133
|
+
export component FieldDescription(children: React.Node, ...rest: Rest) {
|
|
136
134
|
const field = useField("Field.Description");
|
|
137
135
|
const register = field.registerDescription;
|
|
138
136
|
useEffect(() => {
|
|
@@ -153,7 +151,7 @@ export component FieldDescription(children: React.Node, ...rest: { readonly [str
|
|
|
153
151
|
* `role="alert"` so it is announced when it appears, which is the point of an
|
|
154
152
|
* error that arrives after a blur or a submit.
|
|
155
153
|
*/
|
|
156
|
-
export component FieldError(children: React.Node, ...rest:
|
|
154
|
+
export component FieldError(children: React.Node, ...rest: Rest) {
|
|
157
155
|
const field = useField("Field.Error");
|
|
158
156
|
const register = field.registerError;
|
|
159
157
|
useEffect(() => {
|
package/internal/merge-props.js
CHANGED
|
@@ -28,8 +28,52 @@
|
|
|
28
28
|
// consumer to build a part that spreads `rest` last, which is the failure this
|
|
29
29
|
// exists to prevent — so it stays unreachable from outside the package.
|
|
30
30
|
|
|
31
|
-
/**
|
|
32
|
-
|
|
31
|
+
/**
|
|
32
|
+
* Props on their way onto an element: what a caller hands a part, and what
|
|
33
|
+
* `Field.Control` hands back for a caller to spread.
|
|
34
|
+
*
|
|
35
|
+
* `key` is named out of the indexer rather than left to it, and that one
|
|
36
|
+
* property is the whole subtlety of this type. React takes `key` off the
|
|
37
|
+
* attributes before a component is called, so a part's props never contain
|
|
38
|
+
* one — but an indexer does not know that, and `{ readonly [string]: mixed }`
|
|
39
|
+
* answers `mixed` for every name, `key` included. React's `key` is
|
|
40
|
+
* `string | number`, so every intrinsic this package rendered was rejected for
|
|
41
|
+
* a property that cannot be there:
|
|
42
|
+
*
|
|
43
|
+
* error[incompatible-type]: Cannot create button element because in
|
|
44
|
+
* property key: Either unknown is incompatible with string. Or unknown is
|
|
45
|
+
* incompatible with number.
|
|
46
|
+
*
|
|
47
|
+
* thirty-two times, one per element, which was 32 of `@uniflowed/ui`'s 73 type
|
|
48
|
+
* errors. `key?: empty` states what React already guarantees, and the errors
|
|
49
|
+
* are the checker agreeing.
|
|
50
|
+
*
|
|
51
|
+
* # Two answers that look better than they are
|
|
52
|
+
*
|
|
53
|
+
* **`readonly key?: string | number`** — React's own type for the property —
|
|
54
|
+
* also silences the error, and is a lie in the shape of a fix. It says a
|
|
55
|
+
* caller may pass a `key` here; a part would then spread it onto its element,
|
|
56
|
+
* which is the "spreading a key into JSX" mistake React 19 added a warning
|
|
57
|
+
* for. `empty` is the same repair and a true sentence. It reads oddly for
|
|
58
|
+
* about a second and then reads as exactly what it is: there is no value you
|
|
59
|
+
* can pass under this name.
|
|
60
|
+
*
|
|
61
|
+
* **`React.PropsOf<"button">`** — the props of the element actually being
|
|
62
|
+
* rendered, which is what this type would like to say — cannot be written
|
|
63
|
+
* here. uf does not merge Flow's `jsx.js` environment, deliberately and for
|
|
64
|
+
* reasons `crates/uf_check/src/upstream/environments.rs` gives, so
|
|
65
|
+
* `$JSXIntrinsics` is the bare-bones table in `lib/react.js`, every
|
|
66
|
+
* intrinsic's `props` is `any`, and `React.PropsOf` itself reads as an
|
|
67
|
+
* any-typed value. Nothing about an element is checked here except its `key`:
|
|
68
|
+
* `<button className={5} nonsenseAttr={{}} />` is not an error today. A named
|
|
69
|
+
* type per element would therefore not be React's contract but a hand-written
|
|
70
|
+
* copy of `jsx.js` living in a UI package, drifting from the DOM on its own
|
|
71
|
+
* schedule — and it would still need an indexer for `data-*` and `aria-*`,
|
|
72
|
+
* which is where this started. So it stays one `Rest`, and the day
|
|
73
|
+
* `$JSXIntrinsics` is real is the day this becomes `React.PropsOf` and the
|
|
74
|
+
* parts say which element they render.
|
|
75
|
+
*/
|
|
76
|
+
export type Rest = { readonly key?: empty, readonly [string]: mixed };
|
|
33
77
|
|
|
34
78
|
/**
|
|
35
79
|
* Call the caller's handler and then the component's.
|
|
@@ -76,10 +120,16 @@ export function composeRefs<T>(
|
|
|
76
120
|
* leaving them in would put the caller's copy back on top of the composed one.
|
|
77
121
|
*/
|
|
78
122
|
export function withoutComposed(rest: Rest, names: $ReadOnlyArray<string>): Rest {
|
|
79
|
-
const kept: { [string]: mixed } = {};
|
|
80
|
-
for (const
|
|
81
|
-
|
|
82
|
-
|
|
123
|
+
const kept: { key?: empty, [string]: mixed } = {};
|
|
124
|
+
for (const name of Object.keys(rest)) {
|
|
125
|
+
// `key` is dropped whatever the caller asked to compose, because it is the
|
|
126
|
+
// one name the indexer does not speak for: writing `rest[name]` under it
|
|
127
|
+
// would put a `mixed` back where `Rest` promises nothing can be, and Flow
|
|
128
|
+
// says so. Nothing is lost — React removed the `key` long before this ran,
|
|
129
|
+
// so this is the type-level statement made at runtime rather than a filter
|
|
130
|
+
// that ever has work to do.
|
|
131
|
+
if (name !== "key" && !names.includes(name)) {
|
|
132
|
+
kept[name] = rest[name];
|
|
83
133
|
}
|
|
84
134
|
}
|
|
85
135
|
return kept;
|
package/menu.js
CHANGED
|
@@ -58,6 +58,7 @@ import {
|
|
|
58
58
|
} from "@uniflowed/react";
|
|
59
59
|
import { useStableCallback } from "@uniflowed/hooks/lifecycle";
|
|
60
60
|
|
|
61
|
+
import type { Rest } from "./internal/merge-props.js";
|
|
61
62
|
import { composeHandlers, composeRefs, withoutComposed } from "./internal/merge-props.js";
|
|
62
63
|
import {
|
|
63
64
|
indexOfActive,
|
|
@@ -257,7 +258,7 @@ component MenuLevel(
|
|
|
257
258
|
}
|
|
258
259
|
|
|
259
260
|
/** The button that opens the menu. */
|
|
260
|
-
export component MenuTrigger(children: React.Node, ...rest:
|
|
261
|
+
export component MenuTrigger(children: React.Node, ...rest: Rest) {
|
|
261
262
|
const menu = useMenu("Menu.Trigger");
|
|
262
263
|
const passed = withoutComposed(rest, ["onClick", "onKeyDown", "ref"]);
|
|
263
264
|
useTriggerRegistration(menu);
|
|
@@ -308,7 +309,7 @@ export component MenuTrigger(children: React.Node, ...rest: { readonly [string]:
|
|
|
308
309
|
*/
|
|
309
310
|
export component MenuBody(
|
|
310
311
|
children: renders* (MenuItem | MenuSeparator | MenuGroup | MenuSub),
|
|
311
|
-
...rest:
|
|
312
|
+
...rest: Rest
|
|
312
313
|
) {
|
|
313
314
|
const menu = useMenu("Menu.Body");
|
|
314
315
|
const bodyRef = useRef<HTMLElement | null>(null);
|
|
@@ -481,7 +482,7 @@ export component MenuItem(
|
|
|
481
482
|
children: React.Node,
|
|
482
483
|
disabled?: boolean = false,
|
|
483
484
|
onSelect?: () => mixed,
|
|
484
|
-
...rest:
|
|
485
|
+
...rest: Rest
|
|
485
486
|
) {
|
|
486
487
|
const menu = useMenu("Menu.Item");
|
|
487
488
|
const list = useContext(MenuListContext);
|
|
@@ -521,7 +522,7 @@ export component MenuItem(
|
|
|
521
522
|
* is why it reads the list context of the menu around it and the menu context
|
|
522
523
|
* of the one below it.
|
|
523
524
|
*/
|
|
524
|
-
export component MenuSubTrigger(children: React.Node, ...rest:
|
|
525
|
+
export component MenuSubTrigger(children: React.Node, ...rest: Rest) {
|
|
525
526
|
const menu = useMenu("Menu.SubTrigger");
|
|
526
527
|
const list = useContext(MenuListContext);
|
|
527
528
|
const passed = withoutComposed(rest, ["onClick", "onFocus", "onKeyDown", "ref"]);
|
|
@@ -574,7 +575,7 @@ export component MenuSubTrigger(children: React.Node, ...rest: { readonly [strin
|
|
|
574
575
|
* moving through the menu is told the group changed. It is not focusable and
|
|
575
576
|
* the arrow keys pass straight over it.
|
|
576
577
|
*/
|
|
577
|
-
export component MenuSeparator(...rest:
|
|
578
|
+
export component MenuSeparator(...rest: Rest) {
|
|
578
579
|
return <div {...rest} aria-orientation="horizontal" role="separator" />;
|
|
579
580
|
}
|
|
580
581
|
|
|
@@ -586,7 +587,7 @@ export component MenuSeparator(...rest: { readonly [string]: mixed }) {
|
|
|
586
587
|
* that is not in the document makes a screen reader announce *nothing*, which
|
|
587
588
|
* is worse than an unnamed group.
|
|
588
589
|
*/
|
|
589
|
-
export component MenuGroup(children: React.Node, ...rest:
|
|
590
|
+
export component MenuGroup(children: React.Node, ...rest: Rest) {
|
|
590
591
|
const base = useId();
|
|
591
592
|
const [labelled, setLabelled] = useState(false);
|
|
592
593
|
|
|
@@ -608,7 +609,7 @@ export component MenuGroup(children: React.Node, ...rest: { readonly [string]: m
|
|
|
608
609
|
* as ordinary content would have a reader hear the heading once as the group's
|
|
609
610
|
* name and again as a stray line of text between the items.
|
|
610
611
|
*/
|
|
611
|
-
export component MenuLabel(children: React.Node, ...rest:
|
|
612
|
+
export component MenuLabel(children: React.Node, ...rest: Rest) {
|
|
612
613
|
const group = useContext(MenuGroupContext);
|
|
613
614
|
const register = group?.registerLabel;
|
|
614
615
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniflowed/ui",
|
|
3
|
-
"version": "0.0.0-alpha.
|
|
3
|
+
"version": "0.0.0-alpha.5",
|
|
4
4
|
"description": "Headless, accessible React components whose composition Flow checks, part of the Unified Toolchain for Flow.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -25,8 +25,8 @@
|
|
|
25
25
|
"internal"
|
|
26
26
|
],
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@uniflowed/hooks": "0.0.0-alpha.
|
|
29
|
-
"@uniflowed/react": "0.0.0-alpha.
|
|
28
|
+
"@uniflowed/hooks": "0.0.0-alpha.5",
|
|
29
|
+
"@uniflowed/react": "0.0.0-alpha.5"
|
|
30
30
|
},
|
|
31
31
|
"peerDependencies": {
|
|
32
32
|
"react": ">=19"
|
package/switch.js
CHANGED
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
|
|
28
28
|
import * as React from "@uniflowed/react";
|
|
29
29
|
|
|
30
|
+
import type { Rest } from "./internal/merge-props.js";
|
|
30
31
|
import { composeHandlers, withoutComposed } from "./internal/merge-props.js";
|
|
31
32
|
import { useControlled } from "./internal/controlled-state.js";
|
|
32
33
|
|
|
@@ -37,7 +38,7 @@ export component Switch(
|
|
|
37
38
|
onCheckedChange?: (checked: boolean) => void,
|
|
38
39
|
disabled?: boolean = false,
|
|
39
40
|
children?: React.Node,
|
|
40
|
-
...rest:
|
|
41
|
+
...rest: Rest
|
|
41
42
|
) {
|
|
42
43
|
const [on, setOn] = useControlled(checked, defaultChecked, onCheckedChange);
|
|
43
44
|
const passed = withoutComposed(rest, ["onClick", "onKeyDown"]);
|
package/tabs.js
CHANGED
|
@@ -50,6 +50,7 @@ import {
|
|
|
50
50
|
useState,
|
|
51
51
|
} from "@uniflowed/react";
|
|
52
52
|
|
|
53
|
+
import type { Rest } from "./internal/merge-props.js";
|
|
53
54
|
import { composeHandlers, withoutComposed } from "./internal/merge-props.js";
|
|
54
55
|
import { indexOfActive, itemsOf, movementFor, moveTo } from "./internal/roving-focus.js";
|
|
55
56
|
import { useControlled } from "./internal/controlled-state.js";
|
|
@@ -93,7 +94,7 @@ export component TabsRoot(
|
|
|
93
94
|
onValueChange?: (value: string) => void,
|
|
94
95
|
activationMode?: ActivationMode = "automatic",
|
|
95
96
|
orientation?: Orientation = "horizontal",
|
|
96
|
-
...rest:
|
|
97
|
+
...rest: Rest
|
|
97
98
|
) {
|
|
98
99
|
const base = useId();
|
|
99
100
|
const [selected, select] = useControlled(value, defaultValue, onValueChange);
|
|
@@ -141,7 +142,7 @@ export component TabsRoot(
|
|
|
141
142
|
* tabs push themselves into as they mount answers with mount order, which stops
|
|
142
143
|
* being document order the first time a tab is conditional.
|
|
143
144
|
*/
|
|
144
|
-
export component TabsList(children: renders* TabsTab, ...rest:
|
|
145
|
+
export component TabsList(children: renders* TabsTab, ...rest: Rest) {
|
|
145
146
|
const tabs = useTabs("Tabs.List");
|
|
146
147
|
const passed = withoutComposed(rest, ["onKeyDown"]);
|
|
147
148
|
|
|
@@ -190,7 +191,7 @@ export component TabsTab(
|
|
|
190
191
|
value: string,
|
|
191
192
|
children: React.Node,
|
|
192
193
|
disabled?: boolean = false,
|
|
193
|
-
...rest:
|
|
194
|
+
...rest: Rest
|
|
194
195
|
) {
|
|
195
196
|
const tabs = useTabs("Tabs.Tab");
|
|
196
197
|
const active = tabs.selected === value;
|
|
@@ -248,11 +249,7 @@ export component TabsTab(
|
|
|
248
249
|
* subscription rather than "the selected value equals mine", because a caller
|
|
249
250
|
* may render a subset of panels, or none at all until data arrives.
|
|
250
251
|
*/
|
|
251
|
-
export component TabsPanel(
|
|
252
|
-
value: string,
|
|
253
|
-
children: React.Node,
|
|
254
|
-
...rest: { readonly [string]: mixed }
|
|
255
|
-
) {
|
|
252
|
+
export component TabsPanel(value: string, children: React.Node, ...rest: Rest) {
|
|
256
253
|
const tabs = useTabs("Tabs.Panel");
|
|
257
254
|
const register = tabs.registerPanel;
|
|
258
255
|
const selected = tabs.selected === value;
|