@uniflowed/ui 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/accordion.js +22 -9
- package/alert-dialog.js +41 -19
- package/alert.js +14 -8
- package/avatar.js +14 -8
- package/breadcrumb.js +24 -6
- package/calendar.js +54 -10
- package/carousel.js +24 -6
- package/collapsible.js +14 -3
- package/color-picker.js +22 -5
- package/combobox.js +30 -21
- package/context-menu.js +31 -2
- package/date-picker.js +37 -11
- package/date-range-picker.js +25 -8
- package/dialog.js +39 -11
- package/drawer.js +47 -34
- package/field.js +24 -6
- package/hover-card.js +14 -3
- package/index.js +107 -740
- package/input-otp.js +20 -4
- package/interactions.js +0 -1
- package/internal/collection.js +262 -95
- package/internal/focus.js +13 -4
- package/internal/roving-focus.js +12 -3
- package/internal/segmented-field.js +2 -1
- package/internal/selection.js +171 -0
- package/internal/visually-hidden-style.js +41 -0
- package/menu.js +36 -13
- package/menubar.js +32 -5
- package/navigation-menu.js +24 -6
- package/number-field.js +20 -12
- package/package.json +5 -5
- package/pagination.js +22 -5
- package/popover.js +14 -4
- package/radio-group.js +15 -4
- package/range-calendar.js +22 -3
- package/resizable.js +14 -3
- package/scroll-area.js +14 -3
- package/select.js +30 -13
- package/sheet.js +39 -18
- package/sidebar.js +31 -16
- package/skeleton.js +13 -3
- package/slider.js +15 -4
- package/table.js +34 -24
- package/tabs.js +15 -9
- package/toast.js +24 -6
- package/toggle-group.js +14 -3
- package/tooltip.js +21 -5
- package/visually-hidden.js +259 -0
|
@@ -9,6 +9,7 @@ import { composeHandlers, withProps } from "./merge-props.js";
|
|
|
9
9
|
import type { RenderProp, Rest } from "./merge-props.js";
|
|
10
10
|
import { directionOf } from "./roving-focus.js";
|
|
11
11
|
import { useLocale } from "../i18n-provider.js";
|
|
12
|
+
import { visuallyHiddenStyle } from "./visually-hidden-style.js";
|
|
12
13
|
|
|
13
14
|
type Segment = "year" | "month" | "day" | "hour" | "minute" | "second" | "dayPeriod";
|
|
14
15
|
type Fields = { [string]: string };
|
|
@@ -306,7 +307,7 @@ export component SegmentedField(time: boolean, options: DateFieldProps) {
|
|
|
306
307
|
children: (
|
|
307
308
|
<>
|
|
308
309
|
{segments}
|
|
309
|
-
<span role="status" aria-live="polite">
|
|
310
|
+
<span role="status" aria-live="polite" style={visuallyHiddenStyle}>
|
|
310
311
|
{announcement}
|
|
311
312
|
</span>
|
|
312
313
|
</>
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
//
|
|
3
|
+
// What a collection's selection becomes after one gesture, as pure functions.
|
|
4
|
+
//
|
|
5
|
+
// `ListBox`, `GridList`, `Tree` and `TagGroup` all ask the same four questions
|
|
6
|
+
// — toggle this key, make it the only one, extend to it, take everything — and
|
|
7
|
+
// the answers depend on three settings rather than on which collection is
|
|
8
|
+
// asking. Keeping them here, apart from React, is what lets every rule be
|
|
9
|
+
// tested without rendering and lets all four collections agree by construction.
|
|
10
|
+
//
|
|
11
|
+
// The rules are React Aria's selection manager (`@react-stately/selection`),
|
|
12
|
+
// which is itself the WAI-ARIA APG's listbox and grid selection models made
|
|
13
|
+
// precise:
|
|
14
|
+
//
|
|
15
|
+
// * `mode` is what may be chosen: nothing, one key, or any number.
|
|
16
|
+
// * `behavior` is what a plain gesture does. `"toggle"` flips the key — the
|
|
17
|
+
// checkbox model, right for touch and for lists whose rows carry a
|
|
18
|
+
// checkbox. `"replace"` makes the key the whole selection — the file
|
|
19
|
+
// manager model, where Ctrl/Cmd adds one and Shift adds a range.
|
|
20
|
+
// * `disallowEmpty` refuses the gesture that would leave nothing selected,
|
|
21
|
+
// rather than accepting it and then reselecting.
|
|
22
|
+
//
|
|
23
|
+
// Every function returns the *same* set it was given when nothing changes, so a
|
|
24
|
+
// caller can compare by identity and skip `onSelectionChange` for a gesture
|
|
25
|
+
// that did nothing — a controlled parent is not told about a change that is
|
|
26
|
+
// not one.
|
|
27
|
+
//
|
|
28
|
+
// # Why this is `internal/`
|
|
29
|
+
//
|
|
30
|
+
// A public selection manager would be a second way to build a collection, and
|
|
31
|
+
// the point of this package is that there is one. The collections are the
|
|
32
|
+
// public surface; `packages/ui/index.js` says why the internals stay internal.
|
|
33
|
+
|
|
34
|
+
export type SelectionMode = "none" | "single" | "multiple";
|
|
35
|
+
export type SelectionBehavior = "toggle" | "replace";
|
|
36
|
+
|
|
37
|
+
/** The three settings every answer below depends on. */
|
|
38
|
+
export type SelectionPolicy = {|
|
|
39
|
+
readonly mode: SelectionMode,
|
|
40
|
+
readonly behavior: SelectionBehavior,
|
|
41
|
+
readonly disallowEmpty: boolean,
|
|
42
|
+
|};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The keys a range covers, in collection order, inclusive at both ends.
|
|
46
|
+
*
|
|
47
|
+
* `order` is the selectable keys only — disabled rows are not in it — so a range
|
|
48
|
+
* dragged across a disabled row does not pick it up, which is what React Aria
|
|
49
|
+
* and every native list do. An end that is not in `order` (a key filtered out
|
|
50
|
+
* since it was chosen) contributes nothing rather than stretching the range to
|
|
51
|
+
* the start of the list.
|
|
52
|
+
*/
|
|
53
|
+
export function keyRange(
|
|
54
|
+
order: $ReadOnlyArray<string>,
|
|
55
|
+
from: string,
|
|
56
|
+
to: string,
|
|
57
|
+
): $ReadOnlyArray<string> {
|
|
58
|
+
const start = order.indexOf(from);
|
|
59
|
+
const end = order.indexOf(to);
|
|
60
|
+
if (start < 0 || end < 0) return end < 0 ? [] : [to];
|
|
61
|
+
return order.slice(Math.min(start, end), Math.max(start, end) + 1);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Flip one key, unless that would empty a selection that must not be empty. */
|
|
65
|
+
export function toggleKey(
|
|
66
|
+
policy: SelectionPolicy,
|
|
67
|
+
selected: $ReadOnlySet<string>,
|
|
68
|
+
key: string,
|
|
69
|
+
): $ReadOnlySet<string> {
|
|
70
|
+
if (policy.mode === "none") return selected;
|
|
71
|
+
if (selected.has(key)) {
|
|
72
|
+
if (policy.disallowEmpty && selected.size === 1) return selected;
|
|
73
|
+
if (policy.mode === "single") return new Set();
|
|
74
|
+
const next = new Set(selected);
|
|
75
|
+
next.delete(key);
|
|
76
|
+
return next;
|
|
77
|
+
}
|
|
78
|
+
if (policy.mode === "single") return new Set([key]);
|
|
79
|
+
const next = new Set(selected);
|
|
80
|
+
next.add(key);
|
|
81
|
+
return next;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Make one key the whole selection. */
|
|
85
|
+
export function replaceWith(
|
|
86
|
+
policy: SelectionPolicy,
|
|
87
|
+
selected: $ReadOnlySet<string>,
|
|
88
|
+
key: string,
|
|
89
|
+
): $ReadOnlySet<string> {
|
|
90
|
+
if (policy.mode === "none") return selected;
|
|
91
|
+
if (selected.size === 1 && selected.has(key)) return selected;
|
|
92
|
+
return new Set([key]);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Extend from `anchor` to `key`, replacing the range the previous extension made.
|
|
97
|
+
*
|
|
98
|
+
* `lead` is where the last extension ended. Shift+Down, Shift+Down, Shift+Up
|
|
99
|
+
* must end with two rows selected rather than three, so the old range
|
|
100
|
+
* (`anchor`…`lead`) comes out before the new one (`anchor`…`key`) goes in —
|
|
101
|
+
* while anything selected outside both ranges, by an earlier Ctrl/Cmd+click,
|
|
102
|
+
* stays. With no anchor the extension starts at `key`.
|
|
103
|
+
*/
|
|
104
|
+
export function extendTo(
|
|
105
|
+
policy: SelectionPolicy,
|
|
106
|
+
selected: $ReadOnlySet<string>,
|
|
107
|
+
order: $ReadOnlyArray<string>,
|
|
108
|
+
anchor: string | null,
|
|
109
|
+
lead: string | null,
|
|
110
|
+
key: string,
|
|
111
|
+
): $ReadOnlySet<string> {
|
|
112
|
+
if (policy.mode === "none") return selected;
|
|
113
|
+
if (policy.mode === "single") return replaceWith(policy, selected, key);
|
|
114
|
+
const from = anchor ?? key;
|
|
115
|
+
const next = new Set(selected);
|
|
116
|
+
if (lead != null) for (const each of keyRange(order, from, lead)) next.delete(each);
|
|
117
|
+
for (const each of keyRange(order, from, key)) next.add(each);
|
|
118
|
+
if (next.size === 0 && policy.disallowEmpty) return selected;
|
|
119
|
+
return sameKeys(next, selected) ? selected : next;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Every selectable key, when more than one may be chosen. */
|
|
123
|
+
export function selectAll(
|
|
124
|
+
policy: SelectionPolicy,
|
|
125
|
+
selected: $ReadOnlySet<string>,
|
|
126
|
+
order: $ReadOnlyArray<string>,
|
|
127
|
+
): $ReadOnlySet<string> {
|
|
128
|
+
if (policy.mode !== "multiple" || order.length === 0) return selected;
|
|
129
|
+
const next = new Set(selected);
|
|
130
|
+
for (const key of order) next.add(key);
|
|
131
|
+
return sameKeys(next, selected) ? selected : next;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Nothing, unless nothing is not allowed. */
|
|
135
|
+
export function clearAll(
|
|
136
|
+
policy: SelectionPolicy,
|
|
137
|
+
selected: $ReadOnlySet<string>,
|
|
138
|
+
): $ReadOnlySet<string> {
|
|
139
|
+
if (policy.mode === "none" || policy.disallowEmpty || selected.size === 0) return selected;
|
|
140
|
+
return new Set();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* A selection as the array `onSelectionChange` reports: collection order first,
|
|
145
|
+
* then any key the collection is not showing, in the order it was chosen.
|
|
146
|
+
*
|
|
147
|
+
* Keys the collection is not showing are real — a child of a collapsed tree
|
|
148
|
+
* row, a row a filter hid — and dropping them because they are out of sight
|
|
149
|
+
* would deselect something the reader chose and cannot see go.
|
|
150
|
+
*/
|
|
151
|
+
export function orderedKeys(
|
|
152
|
+
selected: $ReadOnlySet<string>,
|
|
153
|
+
order: $ReadOnlyArray<string>,
|
|
154
|
+
): $ReadOnlyArray<string> {
|
|
155
|
+
const result = [];
|
|
156
|
+
const placed = new Set<string>();
|
|
157
|
+
for (const key of order) {
|
|
158
|
+
if (selected.has(key)) {
|
|
159
|
+
result.push(key);
|
|
160
|
+
placed.add(key);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
for (const key of selected) if (!placed.has(key)) result.push(key);
|
|
164
|
+
return result;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function sameKeys(a: $ReadOnlySet<string>, b: $ReadOnlySet<string>): boolean {
|
|
168
|
+
if (a.size !== b.size) return false;
|
|
169
|
+
for (const key of a) if (!b.has(key)) return false;
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
//
|
|
3
|
+
// The one inline style for "hidden from sight, present for a screen reader".
|
|
4
|
+
//
|
|
5
|
+
// Its own module, with no `"use client"`, so a part that renders on the server
|
|
6
|
+
// can import it without importing `visually-hidden.js` — a client module whose
|
|
7
|
+
// every export is a client reference to a Server Component. Internal for the
|
|
8
|
+
// reason `packages/ui/index.js` gives about every module here: the public
|
|
9
|
+
// answer is `VisuallyHidden`.
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The style that hides an element from sight and from nothing else.
|
|
13
|
+
*
|
|
14
|
+
* Shared by `VisuallyHidden` and by the parts that keep a live region of their
|
|
15
|
+
* own beside what they render — the table's sort announcement, the
|
|
16
|
+
* collections', the calendar's and the range calendar's, and the segmented
|
|
17
|
+
* fields'. Frozen, because every one of them holds the same object.
|
|
18
|
+
*/
|
|
19
|
+
export const visuallyHiddenStyle: {|
|
|
20
|
+
readonly border: number,
|
|
21
|
+
readonly clip: string,
|
|
22
|
+
readonly clipPath: string,
|
|
23
|
+
readonly height: number,
|
|
24
|
+
readonly margin: number,
|
|
25
|
+
readonly overflow: string,
|
|
26
|
+
readonly padding: number,
|
|
27
|
+
readonly position: string,
|
|
28
|
+
readonly whiteSpace: string,
|
|
29
|
+
readonly width: number,
|
|
30
|
+
|} = Object.freeze({
|
|
31
|
+
border: 0,
|
|
32
|
+
clip: "rect(0, 0, 0, 0)",
|
|
33
|
+
clipPath: "inset(50%)",
|
|
34
|
+
height: 1,
|
|
35
|
+
margin: -1,
|
|
36
|
+
overflow: "hidden",
|
|
37
|
+
padding: 0,
|
|
38
|
+
position: "absolute",
|
|
39
|
+
whiteSpace: "nowrap",
|
|
40
|
+
width: 1,
|
|
41
|
+
});
|
package/menu.js
CHANGED
|
@@ -180,7 +180,7 @@ const MenuRadioContext: React.Context<MenuRadioState | null> = createContext(nul
|
|
|
180
180
|
* whatever layout the caller wrote, and a wrapper would put a `<div>` between
|
|
181
181
|
* them that the caller then has to style around.
|
|
182
182
|
*/
|
|
183
|
-
|
|
183
|
+
component MenuRoot(
|
|
184
184
|
children: React.Node,
|
|
185
185
|
defaultOpen?: boolean = false,
|
|
186
186
|
open?: boolean,
|
|
@@ -200,7 +200,7 @@ export component MenuRoot(
|
|
|
200
200
|
* parent — and that difference is what `ArrowLeft`, `Escape` and "choosing an
|
|
201
201
|
* item closes everything" are all defined in terms of.
|
|
202
202
|
*/
|
|
203
|
-
|
|
203
|
+
component MenuSub(
|
|
204
204
|
children: React.Node,
|
|
205
205
|
defaultOpen?: boolean = false,
|
|
206
206
|
open?: boolean,
|
|
@@ -218,7 +218,7 @@ export component MenuSub(
|
|
|
218
218
|
}
|
|
219
219
|
|
|
220
220
|
/** The button that opens the menu. */
|
|
221
|
-
|
|
221
|
+
component MenuTrigger(children: React.Node, render?: RenderProp, ...rest: Rest) {
|
|
222
222
|
const menu = useMenu("Menu.Trigger");
|
|
223
223
|
useTriggerRegistration(menu);
|
|
224
224
|
const props = withProps(withoutComposed(rest, ["onClick", "onKeyDown", "ref"]), {
|
|
@@ -269,7 +269,7 @@ export component MenuTrigger(children: React.Node, render?: RenderProp, ...rest:
|
|
|
269
269
|
* and only the container can answer it. Items still get their own `Enter` and
|
|
270
270
|
* `Space` from being buttons.
|
|
271
271
|
*/
|
|
272
|
-
|
|
272
|
+
component MenuBody(
|
|
273
273
|
children: renders* (
|
|
274
274
|
| MenuItem
|
|
275
275
|
| MenuCheckboxItem
|
|
@@ -323,7 +323,6 @@ export component MenuBody(
|
|
|
323
323
|
// `open` is menu-tree metadata; no ref value is read during render.
|
|
324
324
|
// uf-lint-disable-next-line react-compiler/refs
|
|
325
325
|
open: menu.open,
|
|
326
|
-
// uf-lint-disable-next-line react-compiler/refs
|
|
327
326
|
overlayRef: bodyRef,
|
|
328
327
|
side: placement,
|
|
329
328
|
sideOffset,
|
|
@@ -551,7 +550,7 @@ hook useMenuItem(
|
|
|
551
550
|
* tree. That combination is what shadcn's copy step is usually reached for, and
|
|
552
551
|
* what this package offers instead of it.
|
|
553
552
|
*/
|
|
554
|
-
|
|
553
|
+
component MenuItem(
|
|
555
554
|
children: React.Node,
|
|
556
555
|
disabled?: boolean = false,
|
|
557
556
|
closeOnSelect?: boolean = true,
|
|
@@ -593,7 +592,7 @@ export component MenuItem(
|
|
|
593
592
|
* summarises other checkboxes — `checkbox.js` has it, and a menu item is a
|
|
594
593
|
* command rather than a summary of a table's rows.
|
|
595
594
|
*/
|
|
596
|
-
|
|
595
|
+
component MenuCheckboxItem(
|
|
597
596
|
children: React.Node,
|
|
598
597
|
checked?: boolean,
|
|
599
598
|
defaultChecked?: boolean = false,
|
|
@@ -640,7 +639,7 @@ export component MenuCheckboxItem(
|
|
|
640
639
|
* a state the group starts in and never an event it reports, because no gesture
|
|
641
640
|
* inside it unchooses an answer.
|
|
642
641
|
*/
|
|
643
|
-
|
|
642
|
+
component MenuRadioGroup(
|
|
644
643
|
children: renders* (MenuRadioItem | MenuLabel | MenuSeparator),
|
|
645
644
|
defaultValue?: string | null = null,
|
|
646
645
|
value?: string | null,
|
|
@@ -688,7 +687,7 @@ export component MenuRadioGroup(
|
|
|
688
687
|
* what a sort order or a zoom level in a native menu does; `closeOnSelect` is
|
|
689
688
|
* the way to say otherwise for a choice that ends the visit.
|
|
690
689
|
*/
|
|
691
|
-
|
|
690
|
+
component MenuRadioItem(
|
|
692
691
|
children: React.Node,
|
|
693
692
|
value: string,
|
|
694
693
|
disabled?: boolean = false,
|
|
@@ -728,7 +727,7 @@ export component MenuRadioItem(
|
|
|
728
727
|
* is why it reads the list context of the menu around it and the menu context
|
|
729
728
|
* of the one below it.
|
|
730
729
|
*/
|
|
731
|
-
|
|
730
|
+
component MenuSubTrigger(children: React.Node, render?: RenderProp, ...rest: Rest) {
|
|
732
731
|
const menu = useMenu("Menu.SubTrigger");
|
|
733
732
|
const list = useContext(MenuListContext);
|
|
734
733
|
const setActiveId = list?.setActiveId;
|
|
@@ -785,7 +784,7 @@ export component MenuSubTrigger(children: React.Node, render?: RenderProp, ...re
|
|
|
785
784
|
* moving through the menu is told the group changed. It is not focusable and
|
|
786
785
|
* the arrow keys pass straight over it.
|
|
787
786
|
*/
|
|
788
|
-
|
|
787
|
+
component MenuSeparator(render?: RenderProp, ...rest: Rest) {
|
|
789
788
|
const props = withProps(rest, { "aria-orientation": "horizontal", role: "separator" });
|
|
790
789
|
if (render != null) {
|
|
791
790
|
return render(props);
|
|
@@ -801,7 +800,7 @@ export component MenuSeparator(render?: RenderProp, ...rest: Rest) {
|
|
|
801
800
|
* that is not in the document makes a screen reader announce *nothing*, which
|
|
802
801
|
* is worse than an unnamed group.
|
|
803
802
|
*/
|
|
804
|
-
|
|
803
|
+
component MenuGroup(children: React.Node, render?: RenderProp, ...rest: Rest) {
|
|
805
804
|
const base = useId();
|
|
806
805
|
const [labelled, setLabelled] = useState(false);
|
|
807
806
|
|
|
@@ -826,7 +825,7 @@ export component MenuGroup(children: React.Node, render?: RenderProp, ...rest: R
|
|
|
826
825
|
* as ordinary content would have a reader hear the heading once as the group's
|
|
827
826
|
* name and again as a stray line of text between the items.
|
|
828
827
|
*/
|
|
829
|
-
|
|
828
|
+
component MenuLabel(children: React.Node, render?: RenderProp, ...rest: Rest) {
|
|
830
829
|
const group = useContext(MenuGroupContext);
|
|
831
830
|
const register = group?.registerLabel;
|
|
832
831
|
|
|
@@ -844,3 +843,27 @@ export component MenuLabel(children: React.Node, render?: RenderProp, ...rest: R
|
|
|
844
843
|
}
|
|
845
844
|
return <div {...props} />;
|
|
846
845
|
}
|
|
846
|
+
|
|
847
|
+
/**
|
|
848
|
+
* The parts, under the names the `Menu` namespace gives them.
|
|
849
|
+
*
|
|
850
|
+
* `index.js` re-exports this module whole — `export * as Menu from "./menu.js"` —
|
|
851
|
+
* so a caller writes `<Menu.Root>`, and the namespace is the prefix. Each
|
|
852
|
+
* part is still *declared* as `MenuRoot`, so React DevTools, a component
|
|
853
|
+
* stack and an error name the part a reader can find rather than one of forty
|
|
854
|
+
* `Root`s.
|
|
855
|
+
*/
|
|
856
|
+
export {
|
|
857
|
+
MenuRoot as Root,
|
|
858
|
+
MenuTrigger as Trigger,
|
|
859
|
+
MenuBody as Body,
|
|
860
|
+
MenuItem as Item,
|
|
861
|
+
MenuCheckboxItem as CheckboxItem,
|
|
862
|
+
MenuRadioGroup as RadioGroup,
|
|
863
|
+
MenuRadioItem as RadioItem,
|
|
864
|
+
MenuSeparator as Separator,
|
|
865
|
+
MenuGroup as Group,
|
|
866
|
+
MenuLabel as Label,
|
|
867
|
+
MenuSub as Sub,
|
|
868
|
+
MenuSubTrigger as SubTrigger,
|
|
869
|
+
};
|
package/menubar.js
CHANGED
|
@@ -128,7 +128,7 @@ hook useMenubar(part: string): MenubarState {
|
|
|
128
128
|
* a page with an application menubar and a formatting toolbar has two, and
|
|
129
129
|
* "menu bar" twice tells a reader nothing about which is which.
|
|
130
130
|
*/
|
|
131
|
-
|
|
131
|
+
component MenubarRoot(children: renders* MenubarMenu, render?: RenderProp, ...rest: Rest) {
|
|
132
132
|
const barRef = useRef<HTMLElement | null>(null);
|
|
133
133
|
const [open, setOpenValue] = useState<string | null>(null);
|
|
134
134
|
const [active, setActive] = useState<string | null>(null);
|
|
@@ -208,7 +208,7 @@ export component MenubarRoot(children: renders* MenubarMenu, render?: RenderProp
|
|
|
208
208
|
* Renders no element of its own, for the reason `Menu.Root` gives: a trigger and
|
|
209
209
|
* its body are siblings in whatever layout the caller wrote.
|
|
210
210
|
*/
|
|
211
|
-
|
|
211
|
+
component MenubarMenu(children: React.Node, value: string) {
|
|
212
212
|
const bar = useMenubar("Menubar.Menu");
|
|
213
213
|
const setOpen = bar.setOpen;
|
|
214
214
|
const onOpenChange = useCallback(
|
|
@@ -237,7 +237,7 @@ export component MenubarMenu(children: React.Node, value: string) {
|
|
|
237
237
|
* menubar — a reader is told "File, menu item, has popup, 1 of 3" — and it is
|
|
238
238
|
* what makes the bar's arrow keys agree with what they were told is in it.
|
|
239
239
|
*/
|
|
240
|
-
|
|
240
|
+
component MenubarTrigger(children: React.Node, render?: RenderProp, ...rest: Rest) {
|
|
241
241
|
const bar = useMenubar("Menubar.Trigger");
|
|
242
242
|
const menu = useMenu("Menubar.Trigger");
|
|
243
243
|
const value = useContext(MenubarMenuContext);
|
|
@@ -278,8 +278,6 @@ export component MenubarTrigger(children: React.Node, render?: RenderProp, ...re
|
|
|
278
278
|
menu.pendingFocusRef.current = end;
|
|
279
279
|
bar.setOpen(value);
|
|
280
280
|
}),
|
|
281
|
-
// React calls callback refs during commit; menu focus restoration reads it later.
|
|
282
|
-
// uf-lint-disable-next-line react-compiler/refs
|
|
283
281
|
ref: composeRefs(rest.ref, (element: HTMLElement | null) => {
|
|
284
282
|
// uf-lint-disable-next-line react-compiler/immutability
|
|
285
283
|
menu.triggerRef.current = element;
|
|
@@ -293,3 +291,32 @@ export component MenubarTrigger(children: React.Node, render?: RenderProp, ...re
|
|
|
293
291
|
}
|
|
294
292
|
return <button {...props} type="button" />;
|
|
295
293
|
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* The parts, under the names the `Menubar` namespace gives them.
|
|
297
|
+
*
|
|
298
|
+
* `index.js` re-exports this module whole — `export * as Menubar from "./menubar.js"` —
|
|
299
|
+
* so a caller writes `<Menubar.Root>`, and the namespace is the prefix. Each
|
|
300
|
+
* part is still *declared* as `MenubarRoot`, so React DevTools, a component
|
|
301
|
+
* stack and an error name the part a reader can find rather than one of forty
|
|
302
|
+
* `Root`s.
|
|
303
|
+
*/
|
|
304
|
+
export { MenubarRoot as Root, MenubarMenu as Menu, MenubarTrigger as Trigger };
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* The parts `Menubar` shares with `Menu`, re-exported so the `Menubar`
|
|
308
|
+
* namespace is complete on its own: `<Menubar.Body>` is `Menu`'s part,
|
|
309
|
+
* not a copy of it.
|
|
310
|
+
*/
|
|
311
|
+
export {
|
|
312
|
+
Body,
|
|
313
|
+
Item,
|
|
314
|
+
CheckboxItem,
|
|
315
|
+
RadioGroup,
|
|
316
|
+
RadioItem,
|
|
317
|
+
Separator,
|
|
318
|
+
Group,
|
|
319
|
+
Label,
|
|
320
|
+
Sub,
|
|
321
|
+
SubTrigger,
|
|
322
|
+
} from "./menu.js";
|
package/navigation-menu.js
CHANGED
|
@@ -106,7 +106,7 @@ hook useNavigationMenuItem(part: string): NavigationMenuItemState {
|
|
|
106
106
|
* trigger writing itself into a ref — is a registry that has to be kept in step
|
|
107
107
|
* with a document that already knows the answer.
|
|
108
108
|
*/
|
|
109
|
-
|
|
109
|
+
component NavigationMenuRoot(
|
|
110
110
|
children: renders* NavigationMenuList,
|
|
111
111
|
defaultValue?: string | null = null,
|
|
112
112
|
value?: string | null,
|
|
@@ -145,13 +145,13 @@ export component NavigationMenuRoot(
|
|
|
145
145
|
}
|
|
146
146
|
|
|
147
147
|
/** The list of entries. A `<ul>`, because a reader is told how many there are. */
|
|
148
|
-
|
|
148
|
+
component NavigationMenuList(children: renders* NavigationMenuItem, ...rest: Rest) {
|
|
149
149
|
useNavigationMenu("NavigationMenu.List");
|
|
150
150
|
return <ul {...rest}>{children}</ul>;
|
|
151
151
|
}
|
|
152
152
|
|
|
153
153
|
/** One entry: a link on its own, or a button and the group it opens. */
|
|
154
|
-
|
|
154
|
+
component NavigationMenuItem(value: string, children: React.Node, ...rest: Rest) {
|
|
155
155
|
const menu = useNavigationMenu("NavigationMenu.Item");
|
|
156
156
|
const base = useId();
|
|
157
157
|
const [present, setPresent] = useState(false);
|
|
@@ -179,7 +179,7 @@ export component NavigationMenuItem(value: string, children: React.Node, ...rest
|
|
|
179
179
|
}
|
|
180
180
|
|
|
181
181
|
/** The button that opens an entry's group. Not a `menuitem`; see the header. */
|
|
182
|
-
|
|
182
|
+
component NavigationMenuTrigger(children: React.Node, ...rest: Rest) {
|
|
183
183
|
const item = useNavigationMenuItem("NavigationMenu.Trigger");
|
|
184
184
|
const passed = withoutComposed(rest, ["onClick"]);
|
|
185
185
|
|
|
@@ -203,7 +203,7 @@ export component NavigationMenuTrigger(children: React.Node, ...rest: Rest) {
|
|
|
203
203
|
* Named after its trigger, so a reader who lands in it by `Tab` is told which
|
|
204
204
|
* entry they are inside rather than hearing an unnamed list of four links.
|
|
205
205
|
*/
|
|
206
|
-
|
|
206
|
+
component NavigationMenuBody(children: renders* NavigationMenuLink, ...rest: Rest) {
|
|
207
207
|
const item = useNavigationMenuItem("NavigationMenu.Body");
|
|
208
208
|
// Registered only while the group is actually in the document, which for this
|
|
209
209
|
// component means only while it is open — see the module header for why a
|
|
@@ -237,7 +237,7 @@ export component NavigationMenuBody(children: renders* NavigationMenuLink, ...re
|
|
|
237
237
|
* behind them so that coming back — or a router that never unmounted the page —
|
|
238
238
|
* does not leave it hanging open.
|
|
239
239
|
*/
|
|
240
|
-
|
|
240
|
+
component NavigationMenuLink(children: React.Node, ...rest: Rest) {
|
|
241
241
|
const item = useNavigationMenuItem("NavigationMenu.Link");
|
|
242
242
|
const passed = withoutComposed(rest, ["onClick"]);
|
|
243
243
|
|
|
@@ -249,3 +249,21 @@ export component NavigationMenuLink(children: React.Node, ...rest: Rest) {
|
|
|
249
249
|
</li>
|
|
250
250
|
);
|
|
251
251
|
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* The parts, under the names the `NavigationMenu` namespace gives them.
|
|
255
|
+
*
|
|
256
|
+
* `index.js` re-exports this module whole — `export * as NavigationMenu from "./navigation-menu.js"` —
|
|
257
|
+
* so a caller writes `<NavigationMenu.Root>`, and the namespace is the prefix. Each
|
|
258
|
+
* part is still *declared* as `NavigationMenuRoot`, so React DevTools, a component
|
|
259
|
+
* stack and an error name the part a reader can find rather than one of forty
|
|
260
|
+
* `Root`s.
|
|
261
|
+
*/
|
|
262
|
+
export {
|
|
263
|
+
NavigationMenuRoot as Root,
|
|
264
|
+
NavigationMenuList as List,
|
|
265
|
+
NavigationMenuItem as Item,
|
|
266
|
+
NavigationMenuTrigger as Trigger,
|
|
267
|
+
NavigationMenuBody as Body,
|
|
268
|
+
NavigationMenuLink as Link,
|
|
269
|
+
};
|
package/number-field.js
CHANGED
|
@@ -82,7 +82,7 @@ hook useNumber(): NumberState {
|
|
|
82
82
|
return state;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
|
|
85
|
+
component NumberFieldRoot(
|
|
86
86
|
children: React.Node,
|
|
87
87
|
value?: number | null,
|
|
88
88
|
defaultValue?: number | null = null,
|
|
@@ -189,7 +189,7 @@ export component NumberFieldRoot(
|
|
|
189
189
|
}
|
|
190
190
|
|
|
191
191
|
/** Field.Control may render this input to supply the label, description and error ids. */
|
|
192
|
-
|
|
192
|
+
component NumberFieldInput(render?: RenderProp, ...rest: Rest) {
|
|
193
193
|
const state = useNumber();
|
|
194
194
|
const props = withProps(rest, {
|
|
195
195
|
type: "text",
|
|
@@ -225,11 +225,7 @@ export component NumberFieldInput(render?: RenderProp, ...rest: Rest) {
|
|
|
225
225
|
return render != null ? render(props) : <input {...props} />;
|
|
226
226
|
}
|
|
227
227
|
|
|
228
|
-
|
|
229
|
-
children?: React.Node = "+",
|
|
230
|
-
render?: RenderProp,
|
|
231
|
-
...rest: Rest
|
|
232
|
-
) {
|
|
228
|
+
component NumberFieldIncrement(children?: React.Node = "+", render?: RenderProp, ...rest: Rest) {
|
|
233
229
|
const state = useNumber();
|
|
234
230
|
const props = withProps(rest, {
|
|
235
231
|
children,
|
|
@@ -243,11 +239,7 @@ export component NumberFieldIncrement(
|
|
|
243
239
|
});
|
|
244
240
|
return render != null ? render(props) : <button {...props} />;
|
|
245
241
|
}
|
|
246
|
-
|
|
247
|
-
children?: React.Node = "−",
|
|
248
|
-
render?: RenderProp,
|
|
249
|
-
...rest: Rest
|
|
250
|
-
) {
|
|
242
|
+
component NumberFieldDecrement(children?: React.Node = "−", render?: RenderProp, ...rest: Rest) {
|
|
251
243
|
const state = useNumber();
|
|
252
244
|
const props = withProps(rest, {
|
|
253
245
|
children,
|
|
@@ -261,3 +253,19 @@ export component NumberFieldDecrement(
|
|
|
261
253
|
});
|
|
262
254
|
return render != null ? render(props) : <button {...props} />;
|
|
263
255
|
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* The parts, under the names the `NumberField` namespace gives them.
|
|
259
|
+
*
|
|
260
|
+
* `index.js` re-exports this module whole — `export * as NumberField from "./number-field.js"` —
|
|
261
|
+
* so a caller writes `<NumberField.Root>`, and the namespace is the prefix. Each
|
|
262
|
+
* part is still *declared* as `NumberFieldRoot`, so React DevTools, a component
|
|
263
|
+
* stack and an error name the part a reader can find rather than one of forty
|
|
264
|
+
* `Root`s.
|
|
265
|
+
*/
|
|
266
|
+
export {
|
|
267
|
+
NumberFieldRoot as Root,
|
|
268
|
+
NumberFieldInput as Input,
|
|
269
|
+
NumberFieldIncrement as Increment,
|
|
270
|
+
NumberFieldDecrement as Decrement,
|
|
271
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniflowed/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
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",
|
|
@@ -19,10 +19,10 @@
|
|
|
19
19
|
"!*.test.js"
|
|
20
20
|
],
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@uniflowed/core": "0.
|
|
23
|
-
"@uniflowed/hooks": "0.
|
|
24
|
-
"@uniflowed/react": "0.
|
|
25
|
-
"@uniflowed/state": "0.
|
|
22
|
+
"@uniflowed/core": "0.3.0",
|
|
23
|
+
"@uniflowed/hooks": "0.3.0",
|
|
24
|
+
"@uniflowed/react": "0.3.0",
|
|
25
|
+
"@uniflowed/state": "0.3.0"
|
|
26
26
|
},
|
|
27
27
|
"peerDependencies": {
|
|
28
28
|
"react": ">=19"
|
package/pagination.js
CHANGED
|
@@ -56,7 +56,7 @@ import { withProps, withoutComposed } from "./internal/merge-props.js";
|
|
|
56
56
|
* links are on screen, and a component that showed five links out of
|
|
57
57
|
* twenty-five would otherwise announce "page 4 of 5".
|
|
58
58
|
*/
|
|
59
|
-
|
|
59
|
+
component PaginationRoot(
|
|
60
60
|
children: React.Node,
|
|
61
61
|
label?: string = "Pagination",
|
|
62
62
|
page?: number | null = null,
|
|
@@ -91,7 +91,7 @@ export component PaginationRoot(
|
|
|
91
91
|
* A real list, so a reader is told how many there are before walking them and
|
|
92
92
|
* can skip the whole thing in one keystroke.
|
|
93
93
|
*/
|
|
94
|
-
|
|
94
|
+
component PaginationContent(
|
|
95
95
|
children: renders* (PaginationItem | PaginationPrevious | PaginationNext),
|
|
96
96
|
render?: RenderProp,
|
|
97
97
|
...rest: Rest
|
|
@@ -109,7 +109,7 @@ export component PaginationContent(
|
|
|
109
109
|
* The `<li>` is structural and takes nothing; everything a caller passes goes
|
|
110
110
|
* on the `<a>`, which is what they style and what a reader activates.
|
|
111
111
|
*/
|
|
112
|
-
|
|
112
|
+
component PaginationItem(
|
|
113
113
|
children: React.Node,
|
|
114
114
|
current?: boolean = false,
|
|
115
115
|
disabled?: boolean = false,
|
|
@@ -131,7 +131,7 @@ export component PaginationItem(
|
|
|
131
131
|
* is not something a reader can act on. Pass `label` to translate it; the
|
|
132
132
|
* glyph stays whatever the caller rendered.
|
|
133
133
|
*/
|
|
134
|
-
|
|
134
|
+
component PaginationPrevious(
|
|
135
135
|
children?: React.Node,
|
|
136
136
|
label?: string = "Previous page",
|
|
137
137
|
disabled?: boolean = false,
|
|
@@ -146,7 +146,7 @@ export component PaginationPrevious(
|
|
|
146
146
|
}
|
|
147
147
|
|
|
148
148
|
/** The link to the page after this one. See `Pagination.Previous`. */
|
|
149
|
-
|
|
149
|
+
component PaginationNext(
|
|
150
150
|
children?: React.Node,
|
|
151
151
|
label?: string = "Next page",
|
|
152
152
|
disabled?: boolean = false,
|
|
@@ -207,3 +207,20 @@ component PageLink(
|
|
|
207
207
|
function defaultAnnouncement(page: number, pageCount: number): string {
|
|
208
208
|
return `Page ${String(page)} of ${String(pageCount)}.`;
|
|
209
209
|
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* The parts, under the names the `Pagination` namespace gives them.
|
|
213
|
+
*
|
|
214
|
+
* `index.js` re-exports this module whole — `export * as Pagination from "./pagination.js"` —
|
|
215
|
+
* so a caller writes `<Pagination.Root>`, and the namespace is the prefix. Each
|
|
216
|
+
* part is still *declared* as `PaginationRoot`, so React DevTools, a component
|
|
217
|
+
* stack and an error name the part a reader can find rather than one of forty
|
|
218
|
+
* `Root`s.
|
|
219
|
+
*/
|
|
220
|
+
export {
|
|
221
|
+
PaginationRoot as Root,
|
|
222
|
+
PaginationContent as Content,
|
|
223
|
+
PaginationItem as Item,
|
|
224
|
+
PaginationPrevious as Previous,
|
|
225
|
+
PaginationNext as Next,
|
|
226
|
+
};
|