@uniflowed/ui 0.0.0-alpha.18 → 0.0.0-alpha.37
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 +61 -54
- package/alert-dialog.js +43 -17
- package/alert.js +142 -0
- package/avatar.js +276 -0
- package/breadcrumb.js +138 -0
- package/checkbox.js +45 -39
- package/collapsible.js +41 -36
- package/context-menu.js +32 -24
- package/dialog.js +143 -119
- package/drawer.js +121 -89
- package/field.js +71 -37
- package/hover-card.js +23 -27
- package/index.js +567 -31
- package/input-otp.js +8 -8
- package/interactions.js +2163 -0
- package/internal/merge-props.js +85 -1
- package/menu.js +236 -213
- package/menubar.js +91 -89
- package/package.json +8 -40
- package/pagination.js +29 -17
- package/popover.js +59 -55
- package/progress.js +21 -16
- package/radio-group.js +79 -75
- package/separator.js +97 -0
- package/sheet.js +41 -17
- package/sidebar.js +10 -10
- package/skeleton.js +159 -0
- package/slider.js +89 -89
- package/switch.js +37 -31
- package/table.js +90 -67
- package/tabs.js +100 -91
- package/toast.js +23 -55
- package/toggle-group.js +52 -49
- package/toggle.js +38 -25
- package/tooltip.js +26 -37
package/menubar.js
CHANGED
|
@@ -66,8 +66,13 @@ import {
|
|
|
66
66
|
useState,
|
|
67
67
|
} from "@uniflowed/react";
|
|
68
68
|
|
|
69
|
-
import type { Rest } from "./internal/merge-props.js";
|
|
70
|
-
import {
|
|
69
|
+
import type { PartEvent, RenderProp, Rest } from "./internal/merge-props.js";
|
|
70
|
+
import {
|
|
71
|
+
composeHandlers,
|
|
72
|
+
composeRefs,
|
|
73
|
+
withProps,
|
|
74
|
+
withoutComposed,
|
|
75
|
+
} from "./internal/merge-props.js";
|
|
71
76
|
import type { RovingSet } from "./internal/roving-focus.js";
|
|
72
77
|
import {
|
|
73
78
|
directionOf,
|
|
@@ -123,7 +128,7 @@ hook useMenubar(part: string): MenubarState {
|
|
|
123
128
|
* a page with an application menubar and a formatting toolbar has two, and
|
|
124
129
|
* "menu bar" twice tells a reader nothing about which is which.
|
|
125
130
|
*/
|
|
126
|
-
export component MenubarRoot(children: renders* MenubarMenu, ...rest: Rest) {
|
|
131
|
+
export component MenubarRoot(children: renders* MenubarMenu, render?: RenderProp, ...rest: Rest) {
|
|
127
132
|
const barRef = useRef<HTMLElement | null>(null);
|
|
128
133
|
const [open, setOpenValue] = useState<string | null>(null);
|
|
129
134
|
const [active, setActive] = useState<string | null>(null);
|
|
@@ -142,57 +147,55 @@ export component MenubarRoot(children: renders* MenubarMenu, ...rest: Rest) {
|
|
|
142
147
|
() => ({ open, setOpen, active, setActive, firstId }),
|
|
143
148
|
[open, setOpen, active, firstId],
|
|
144
149
|
);
|
|
145
|
-
const
|
|
150
|
+
const props = withProps(withoutComposed(rest, ["onKeyDown", "ref"]), {
|
|
151
|
+
"aria-orientation": "horizontal",
|
|
152
|
+
children,
|
|
153
|
+
onKeyDown: composeHandlers(rest.onKeyDown, (event: PartEvent) => {
|
|
154
|
+
const bar: $FlowFixMe = event.currentTarget;
|
|
155
|
+
const movement = movementFor(event.key, "horizontal", directionOf(bar));
|
|
156
|
+
if (movement == null) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const triggers = itemsOf(bar, TRIGGERS.item, TRIGGERS.owner);
|
|
160
|
+
// With a menu open, focus is on one of *its* items rather than on a
|
|
161
|
+
// trigger, so "where am I in the bar" is the expanded trigger. This
|
|
162
|
+
// is the whole of walking File → Edit → View without pressing Escape.
|
|
163
|
+
const focused = indexOfActive(triggers, bar.ownerDocument?.activeElement);
|
|
164
|
+
const at =
|
|
165
|
+
focused >= 0
|
|
166
|
+
? focused
|
|
167
|
+
: triggers.findIndex((each) => each.getAttribute("aria-expanded") === "true");
|
|
168
|
+
const next = moveTo(triggers, at, movement, TRIGGERS.wrap, TRIGGERS.skipDisabled);
|
|
169
|
+
if (next == null) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
// Claimed before focus moves, or the browser scrolls the page under
|
|
173
|
+
// the trigger that has just taken it; `moveOnKey` says the same.
|
|
174
|
+
event.preventDefault();
|
|
175
|
+
event.stopPropagation();
|
|
176
|
+
const value = next.getAttribute("data-uf-menubar-value");
|
|
177
|
+
if (open != null && value != null) {
|
|
178
|
+
// Swap which menu is showing. Focus lands on the new menu's first
|
|
179
|
+
// item through `Menu.Body`'s own opening effect, so nothing here
|
|
180
|
+
// moves it: focusing the trigger as well would be two focus moves
|
|
181
|
+
// in one commit and the reader would see the second.
|
|
182
|
+
setOpen(value);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
next.focus();
|
|
186
|
+
if (value != null) {
|
|
187
|
+
setActive(value);
|
|
188
|
+
}
|
|
189
|
+
}),
|
|
190
|
+
ref: composeRefs(rest.ref, (element: HTMLElement | null) => {
|
|
191
|
+
barRef.current = element;
|
|
192
|
+
}),
|
|
193
|
+
role: "menubar",
|
|
194
|
+
});
|
|
146
195
|
|
|
147
196
|
return (
|
|
148
197
|
<MenubarContext.Provider value={state}>
|
|
149
|
-
<div
|
|
150
|
-
{...passed}
|
|
151
|
-
aria-orientation="horizontal"
|
|
152
|
-
onKeyDown={composeHandlers(rest.onKeyDown, (event) => {
|
|
153
|
-
const bar: $FlowFixMe = event.currentTarget;
|
|
154
|
-
const movement = movementFor(event.key, "horizontal", directionOf(bar));
|
|
155
|
-
if (movement == null) {
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
158
|
-
const triggers = itemsOf(bar, TRIGGERS.item, TRIGGERS.owner);
|
|
159
|
-
// With a menu open, focus is on one of *its* items rather than on a
|
|
160
|
-
// trigger, so "where am I in the bar" is the expanded trigger. This
|
|
161
|
-
// is the whole of walking File → Edit → View without pressing Escape.
|
|
162
|
-
const focused = indexOfActive(triggers, bar.ownerDocument?.activeElement);
|
|
163
|
-
const at =
|
|
164
|
-
focused >= 0
|
|
165
|
-
? focused
|
|
166
|
-
: triggers.findIndex((each) => each.getAttribute("aria-expanded") === "true");
|
|
167
|
-
const next = moveTo(triggers, at, movement, TRIGGERS.wrap, TRIGGERS.skipDisabled);
|
|
168
|
-
if (next == null) {
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
|
-
// Claimed before focus moves, or the browser scrolls the page under
|
|
172
|
-
// the trigger that has just taken it; `moveOnKey` says the same.
|
|
173
|
-
event.preventDefault();
|
|
174
|
-
event.stopPropagation();
|
|
175
|
-
const value = next.getAttribute("data-uf-menubar-value");
|
|
176
|
-
if (open != null && value != null) {
|
|
177
|
-
// Swap which menu is showing. Focus lands on the new menu's first
|
|
178
|
-
// item through `Menu.Body`'s own opening effect, so nothing here
|
|
179
|
-
// moves it: focusing the trigger as well would be two focus moves
|
|
180
|
-
// in one commit and the reader would see the second.
|
|
181
|
-
setOpen(value);
|
|
182
|
-
return;
|
|
183
|
-
}
|
|
184
|
-
next.focus();
|
|
185
|
-
if (value != null) {
|
|
186
|
-
setActive(value);
|
|
187
|
-
}
|
|
188
|
-
})}
|
|
189
|
-
ref={composeRefs(rest.ref, (element) => {
|
|
190
|
-
barRef.current = element;
|
|
191
|
-
})}
|
|
192
|
-
role="menubar"
|
|
193
|
-
>
|
|
194
|
-
{children}
|
|
195
|
-
</div>
|
|
198
|
+
{render == null ? <div {...props} /> : render(props)}
|
|
196
199
|
</MenubarContext.Provider>
|
|
197
200
|
);
|
|
198
201
|
}
|
|
@@ -232,54 +235,53 @@ export component MenubarMenu(children: React.Node, value: string) {
|
|
|
232
235
|
* menubar — a reader is told "File, menu item, has popup, 1 of 3" — and it is
|
|
233
236
|
* what makes the bar's arrow keys agree with what they were told is in it.
|
|
234
237
|
*/
|
|
235
|
-
export component MenubarTrigger(children: React.Node, ...rest: Rest) {
|
|
238
|
+
export component MenubarTrigger(children: React.Node, render?: RenderProp, ...rest: Rest) {
|
|
236
239
|
const bar = useMenubar("Menubar.Trigger");
|
|
237
240
|
const menu = useMenu("Menubar.Trigger");
|
|
238
241
|
const value = useContext(MenubarMenuContext);
|
|
239
242
|
if (value == null) {
|
|
240
243
|
throw new Error("Menubar.Trigger must be rendered inside a Menubar.Menu");
|
|
241
244
|
}
|
|
242
|
-
const passed = withoutComposed(rest, ["onClick", "onFocus", "onKeyDown", "ref"]);
|
|
243
245
|
const id = `${menu.base}-trigger`;
|
|
244
246
|
// The bar's single tab stop. Before anything has been focused or opened it
|
|
245
247
|
// belongs to the first trigger, which is a fact about the document rather
|
|
246
248
|
// than about this component — `useFirstItem` reads it in the bar.
|
|
247
249
|
const stop = bar.active == null ? bar.firstId === id : bar.active === value;
|
|
248
250
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
251
|
+
const props = withProps(withoutComposed(rest, ["onClick", "onFocus", "onKeyDown", "ref"]), {
|
|
252
|
+
"aria-controls": menu.open ? `${menu.base}-body` : undefined,
|
|
253
|
+
"aria-expanded": menu.open ? "true" : "false",
|
|
254
|
+
"aria-haspopup": "menu",
|
|
255
|
+
children,
|
|
256
|
+
// How the bar's keyboard turns a trigger element back into the menu it
|
|
257
|
+
// opens; the module header says why this is an attribute and the rest of
|
|
258
|
+
// the bar's arithmetic is not.
|
|
259
|
+
"data-uf-menubar-value": value,
|
|
260
|
+
id,
|
|
261
|
+
onClick: composeHandlers(rest.onClick, () => bar.setOpen(menu.open ? null : value)),
|
|
262
|
+
onFocus: composeHandlers(rest.onFocus, () => bar.setActive(value)),
|
|
263
|
+
onKeyDown: composeHandlers(rest.onKeyDown, (event: PartEvent) => {
|
|
264
|
+
const end = match (event.key) {
|
|
265
|
+
"ArrowDown" => "first",
|
|
266
|
+
"ArrowUp" => "last",
|
|
267
|
+
_ => null,
|
|
268
|
+
};
|
|
269
|
+
if (end == null) {
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
event.preventDefault();
|
|
273
|
+
menu.pendingFocus.current = end;
|
|
274
|
+
bar.setOpen(value);
|
|
275
|
+
}),
|
|
276
|
+
ref: composeRefs(rest.ref, (element: HTMLElement | null) => {
|
|
277
|
+
menu.triggerRef.current = element;
|
|
278
|
+
}),
|
|
279
|
+
role: "menuitem",
|
|
280
|
+
tabIndex: stop ? 0 : -1,
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
if (render != null) {
|
|
284
|
+
return render(props);
|
|
285
|
+
}
|
|
286
|
+
return <button {...props} type="button" />;
|
|
285
287
|
}
|
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.37",
|
|
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",
|
|
@@ -11,50 +11,18 @@
|
|
|
11
11
|
"directory": "packages/ui"
|
|
12
12
|
},
|
|
13
13
|
"exports": {
|
|
14
|
-
".": "./index.js"
|
|
15
|
-
"./accordion": "./accordion.js",
|
|
16
|
-
"./alert-dialog": "./alert-dialog.js",
|
|
17
|
-
"./calendar": "./calendar.js",
|
|
18
|
-
"./carousel": "./carousel.js",
|
|
19
|
-
"./checkbox": "./checkbox.js",
|
|
20
|
-
"./collapsible": "./collapsible.js",
|
|
21
|
-
"./combobox": "./combobox.js",
|
|
22
|
-
"./context-menu": "./context-menu.js",
|
|
23
|
-
"./date-picker": "./date-picker.js",
|
|
24
|
-
"./dialog": "./dialog.js",
|
|
25
|
-
"./drawer": "./drawer.js",
|
|
26
|
-
"./field": "./field.js",
|
|
27
|
-
"./hover-card": "./hover-card.js",
|
|
28
|
-
"./input-otp": "./input-otp.js",
|
|
29
|
-
"./menu": "./menu.js",
|
|
30
|
-
"./menubar": "./menubar.js",
|
|
31
|
-
"./navigation-menu": "./navigation-menu.js",
|
|
32
|
-
"./pagination": "./pagination.js",
|
|
33
|
-
"./popover": "./popover.js",
|
|
34
|
-
"./progress": "./progress.js",
|
|
35
|
-
"./radio-group": "./radio-group.js",
|
|
36
|
-
"./resizable": "./resizable.js",
|
|
37
|
-
"./scroll-area": "./scroll-area.js",
|
|
38
|
-
"./select": "./select.js",
|
|
39
|
-
"./sheet": "./sheet.js",
|
|
40
|
-
"./sidebar": "./sidebar.js",
|
|
41
|
-
"./slider": "./slider.js",
|
|
42
|
-
"./switch": "./switch.js",
|
|
43
|
-
"./table": "./table.js",
|
|
44
|
-
"./tabs": "./tabs.js",
|
|
45
|
-
"./toast": "./toast.js",
|
|
46
|
-
"./toggle": "./toggle.js",
|
|
47
|
-
"./toggle-group": "./toggle-group.js",
|
|
48
|
-
"./tooltip": "./tooltip.js"
|
|
14
|
+
".": "./index.js"
|
|
49
15
|
},
|
|
50
16
|
"files": [
|
|
51
17
|
"*.js",
|
|
52
|
-
"internal"
|
|
18
|
+
"internal",
|
|
19
|
+
"!*.test.js"
|
|
53
20
|
],
|
|
54
21
|
"dependencies": {
|
|
55
|
-
"@uniflowed/core": "0.0.0-alpha.
|
|
56
|
-
"@uniflowed/hooks": "0.0.0-alpha.
|
|
57
|
-
"@uniflowed/react": "0.0.0-alpha.
|
|
22
|
+
"@uniflowed/core": "0.0.0-alpha.37",
|
|
23
|
+
"@uniflowed/hooks": "0.0.0-alpha.37",
|
|
24
|
+
"@uniflowed/react": "0.0.0-alpha.37",
|
|
25
|
+
"@uniflowed/state": "0.0.0-alpha.37"
|
|
58
26
|
},
|
|
59
27
|
"peerDependencies": {
|
|
60
28
|
"react": ">=19"
|
package/pagination.js
CHANGED
|
@@ -44,8 +44,8 @@
|
|
|
44
44
|
|
|
45
45
|
import * as React from "@uniflowed/react";
|
|
46
46
|
|
|
47
|
-
import type { Rest } from "./internal/merge-props.js";
|
|
48
|
-
import { withoutComposed } from "./internal/merge-props.js";
|
|
47
|
+
import type { RenderProp, Rest } from "./internal/merge-props.js";
|
|
48
|
+
import { withProps, withoutComposed } from "./internal/merge-props.js";
|
|
49
49
|
|
|
50
50
|
/**
|
|
51
51
|
* The pagination, as a named landmark, and the region that announces it.
|
|
@@ -62,16 +62,16 @@ export component PaginationRoot(
|
|
|
62
62
|
page?: number | null = null,
|
|
63
63
|
pageCount?: number | null = null,
|
|
64
64
|
announcePage?: (page: number, pageCount: number) => string,
|
|
65
|
+
render?: RenderProp,
|
|
65
66
|
...rest: Rest
|
|
66
67
|
) {
|
|
67
68
|
const message =
|
|
68
69
|
page == null || pageCount == null ? "" : (announcePage ?? defaultAnnouncement)(page, pageCount);
|
|
70
|
+
const props = withProps(rest, { "aria-label": label, children });
|
|
69
71
|
|
|
70
72
|
return (
|
|
71
73
|
<>
|
|
72
|
-
<nav {...
|
|
73
|
-
{children}
|
|
74
|
-
</nav>
|
|
74
|
+
{render == null ? <nav {...props} /> : render(withProps(props, { role: "navigation" }))}
|
|
75
75
|
{/*
|
|
76
76
|
Beside the navigation rather than inside it, so a reader walking the
|
|
77
77
|
landmark hears the links and not a sentence about them — and mounted
|
|
@@ -93,9 +93,14 @@ export component PaginationRoot(
|
|
|
93
93
|
*/
|
|
94
94
|
export component PaginationContent(
|
|
95
95
|
children: renders* (PaginationItem | PaginationPrevious | PaginationNext),
|
|
96
|
+
render?: RenderProp,
|
|
96
97
|
...rest: Rest
|
|
97
98
|
) {
|
|
98
|
-
|
|
99
|
+
const props = withProps(rest, { children });
|
|
100
|
+
if (render != null) {
|
|
101
|
+
return render(withProps(props, { role: "list" }));
|
|
102
|
+
}
|
|
103
|
+
return <ul {...props} />;
|
|
99
104
|
}
|
|
100
105
|
|
|
101
106
|
/**
|
|
@@ -108,10 +113,11 @@ export component PaginationItem(
|
|
|
108
113
|
children: React.Node,
|
|
109
114
|
current?: boolean = false,
|
|
110
115
|
disabled?: boolean = false,
|
|
116
|
+
render?: RenderProp,
|
|
111
117
|
...rest: Rest
|
|
112
118
|
) {
|
|
113
119
|
return (
|
|
114
|
-
<PageLink current={current} disabled={disabled} rest={rest}>
|
|
120
|
+
<PageLink current={current} disabled={disabled} render={render} rest={rest}>
|
|
115
121
|
{children}
|
|
116
122
|
</PageLink>
|
|
117
123
|
);
|
|
@@ -129,10 +135,11 @@ export component PaginationPrevious(
|
|
|
129
135
|
children?: React.Node,
|
|
130
136
|
label?: string = "Previous page",
|
|
131
137
|
disabled?: boolean = false,
|
|
138
|
+
render?: RenderProp,
|
|
132
139
|
...rest: Rest
|
|
133
140
|
) {
|
|
134
141
|
return (
|
|
135
|
-
<PageLink disabled={disabled} label={label} rest={rest}>
|
|
142
|
+
<PageLink disabled={disabled} label={label} render={render} rest={rest}>
|
|
136
143
|
{children}
|
|
137
144
|
</PageLink>
|
|
138
145
|
);
|
|
@@ -143,10 +150,11 @@ export component PaginationNext(
|
|
|
143
150
|
children?: React.Node,
|
|
144
151
|
label?: string = "Next page",
|
|
145
152
|
disabled?: boolean = false,
|
|
153
|
+
render?: RenderProp,
|
|
146
154
|
...rest: Rest
|
|
147
155
|
) {
|
|
148
156
|
return (
|
|
149
|
-
<PageLink disabled={disabled} label={label} rest={rest}>
|
|
157
|
+
<PageLink disabled={disabled} label={label} render={render} rest={rest}>
|
|
150
158
|
{children}
|
|
151
159
|
</PageLink>
|
|
152
160
|
);
|
|
@@ -174,19 +182,23 @@ component PageLink(
|
|
|
174
182
|
current?: boolean = false,
|
|
175
183
|
disabled?: boolean = false,
|
|
176
184
|
label?: string,
|
|
185
|
+
render?: RenderProp,
|
|
177
186
|
) {
|
|
178
187
|
const passed = withoutComposed(rest, disabled ? ["href"] : []);
|
|
188
|
+
const props = withProps(passed, {
|
|
189
|
+
"aria-current": current ? "page" : undefined,
|
|
190
|
+
"aria-disabled": disabled ? "true" : undefined,
|
|
191
|
+
"aria-label": label,
|
|
192
|
+
children,
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
if (render != null) {
|
|
196
|
+
return <li>{render(withProps(props, { role: disabled ? undefined : "link" }))}</li>;
|
|
197
|
+
}
|
|
179
198
|
|
|
180
199
|
return (
|
|
181
200
|
<li>
|
|
182
|
-
<a
|
|
183
|
-
{...passed}
|
|
184
|
-
aria-current={current ? "page" : undefined}
|
|
185
|
-
aria-disabled={disabled ? "true" : undefined}
|
|
186
|
-
aria-label={label}
|
|
187
|
-
>
|
|
188
|
-
{children}
|
|
189
|
-
</a>
|
|
201
|
+
<a {...props} />
|
|
190
202
|
</li>
|
|
191
203
|
);
|
|
192
204
|
}
|
package/popover.js
CHANGED
|
@@ -59,8 +59,13 @@ import {
|
|
|
59
59
|
import { useStableCallback } from "@uniflowed/hooks/lifecycle";
|
|
60
60
|
|
|
61
61
|
import type { Align, LogicalSide } from "./internal/anchor.js";
|
|
62
|
-
import type { Rest } from "./internal/merge-props.js";
|
|
63
|
-
import {
|
|
62
|
+
import type { PartEvent, RenderProp, Rest } from "./internal/merge-props.js";
|
|
63
|
+
import {
|
|
64
|
+
composeHandlers,
|
|
65
|
+
composeRefs,
|
|
66
|
+
withProps,
|
|
67
|
+
withoutComposed,
|
|
68
|
+
} from "./internal/merge-props.js";
|
|
64
69
|
import { focusable } from "./internal/focus.js";
|
|
65
70
|
import { useAnchor } from "./internal/anchor.js";
|
|
66
71
|
import { useControlled } from "./internal/controlled-state.js";
|
|
@@ -145,30 +150,29 @@ export component PopoverRoot(
|
|
|
145
150
|
* trigger is not "outside" for exactly this reason — closing there and letting
|
|
146
151
|
* this click reopen made the press a no-op that flickered.
|
|
147
152
|
*/
|
|
148
|
-
export component PopoverTrigger(children: React.Node, ...rest: Rest) {
|
|
153
|
+
export component PopoverTrigger(children: React.Node, render?: RenderProp, ...rest: Rest) {
|
|
149
154
|
const popover = usePopover("Popover.Trigger");
|
|
150
155
|
const passed = withoutComposed(rest, ["onClick", "ref"]);
|
|
151
156
|
usePresence(popover.registerTrigger);
|
|
157
|
+
const props = withProps(passed, {
|
|
158
|
+
// Only while it is open: an `aria-controls` naming an element that is not
|
|
159
|
+
// in the document tells a reader there is somewhere to go and then has
|
|
160
|
+
// nowhere to send them.
|
|
161
|
+
"aria-controls": popover.open ? `${popover.base}-body` : undefined,
|
|
162
|
+
"aria-expanded": popover.open ? "true" : "false",
|
|
163
|
+
"aria-haspopup": "dialog",
|
|
164
|
+
children,
|
|
165
|
+
id: `${popover.base}-trigger`,
|
|
166
|
+
onClick: composeHandlers(rest.onClick, () => popover.setOpen(!popover.open)),
|
|
167
|
+
ref: composeRefs(rest.ref, (element: HTMLElement | null) => {
|
|
168
|
+
popover.triggerRef.current = element;
|
|
169
|
+
}),
|
|
170
|
+
});
|
|
152
171
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
// in the document tells a reader there is somewhere to go and then has
|
|
158
|
-
// nowhere to send them.
|
|
159
|
-
aria-controls={popover.open ? `${popover.base}-body` : undefined}
|
|
160
|
-
aria-expanded={popover.open ? "true" : "false"}
|
|
161
|
-
aria-haspopup="dialog"
|
|
162
|
-
id={`${popover.base}-trigger`}
|
|
163
|
-
onClick={composeHandlers(rest.onClick, () => popover.setOpen(!popover.open))}
|
|
164
|
-
ref={composeRefs(rest.ref, (element) => {
|
|
165
|
-
popover.triggerRef.current = element;
|
|
166
|
-
})}
|
|
167
|
-
type="button"
|
|
168
|
-
>
|
|
169
|
-
{children}
|
|
170
|
-
</button>
|
|
171
|
-
);
|
|
172
|
+
if (render != null) {
|
|
173
|
+
return render(props);
|
|
174
|
+
}
|
|
175
|
+
return <button {...props} type="button" />;
|
|
172
176
|
}
|
|
173
177
|
|
|
174
178
|
/**
|
|
@@ -196,6 +200,7 @@ export component PopoverBody(
|
|
|
196
200
|
* not for the button that steps back a month.
|
|
197
201
|
*/
|
|
198
202
|
initialFocus?: { current: HTMLElement | null },
|
|
203
|
+
render?: RenderProp,
|
|
199
204
|
side?: LogicalSide = "bottom",
|
|
200
205
|
sideOffset?: number = 0,
|
|
201
206
|
...rest: Rest
|
|
@@ -303,37 +308,36 @@ export component PopoverBody(
|
|
|
303
308
|
// leave the popover announced as its button rather than as itself.
|
|
304
309
|
const named = rest["aria-label"] != null || rest["aria-labelledby"] != null;
|
|
305
310
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
);
|
|
311
|
+
const props = withProps(passed, {
|
|
312
|
+
"aria-labelledby": named || !popover.triggered ? undefined : `${popover.base}-trigger`,
|
|
313
|
+
children,
|
|
314
|
+
"data-align": anchored.align,
|
|
315
|
+
"data-side": anchored.side,
|
|
316
|
+
"data-state": "open",
|
|
317
|
+
id: `${popover.base}-body`,
|
|
318
|
+
onKeyDown: composeHandlers(rest.onKeyDown, (event: PartEvent) => {
|
|
319
|
+
if (event.key !== "Escape") {
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
event.preventDefault();
|
|
323
|
+
// This popover, not the dialog around it. Two overlays nest in the
|
|
324
|
+
// DOM, so without this one Escape closed both.
|
|
325
|
+
event.stopPropagation();
|
|
326
|
+
close();
|
|
327
|
+
}),
|
|
328
|
+
ref: composeRefs(rest.ref, (element: HTMLElement | null) => {
|
|
329
|
+
bodyRef.current = element;
|
|
330
|
+
}),
|
|
331
|
+
// No `aria-modal`. The page behind a popover is still available, and
|
|
332
|
+
// saying otherwise is the one lie a screen reader cannot see through.
|
|
333
|
+
role: "dialog",
|
|
334
|
+
// So the popover can hold focus itself when it contains nothing focusable,
|
|
335
|
+
// and so Escape has somewhere to be heard.
|
|
336
|
+
tabIndex: -1,
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
if (render != null) {
|
|
340
|
+
return render(props);
|
|
341
|
+
}
|
|
342
|
+
return <div {...props} />;
|
|
339
343
|
}
|
package/progress.js
CHANGED
|
@@ -38,7 +38,8 @@
|
|
|
38
38
|
|
|
39
39
|
import * as React from "@uniflowed/react";
|
|
40
40
|
|
|
41
|
-
import type { Rest } from "./internal/merge-props.js";
|
|
41
|
+
import type { RenderProp, Rest } from "./internal/merge-props.js";
|
|
42
|
+
import { withProps } from "./internal/merge-props.js";
|
|
42
43
|
import { clamp } from "./internal/range.js";
|
|
43
44
|
|
|
44
45
|
/**
|
|
@@ -57,6 +58,9 @@ import { clamp } from "./internal/range.js";
|
|
|
57
58
|
* `Slider`, whose value may be its own, a progress bar's value arrived as a
|
|
58
59
|
* prop, so the caller already has everything they need to size a bar with and
|
|
59
60
|
* a helpful custom property would only be their own arithmetic handed back.
|
|
61
|
+
*
|
|
62
|
+
* `render` changes the element and not the accessibility contract: the
|
|
63
|
+
* progressbar role and value attributes are the props handed to the caller.
|
|
60
64
|
*/
|
|
61
65
|
export component Progress(
|
|
62
66
|
value?: number | null = null,
|
|
@@ -64,23 +68,24 @@ export component Progress(
|
|
|
64
68
|
max?: number = 100,
|
|
65
69
|
valueText?: string,
|
|
66
70
|
children?: React.Node,
|
|
71
|
+
render?: RenderProp,
|
|
67
72
|
...rest: Rest
|
|
68
73
|
) {
|
|
69
74
|
const known = value == null ? null : clamp(value, min, max);
|
|
75
|
+
const props = withProps(rest, {
|
|
76
|
+
"aria-valuemax": max,
|
|
77
|
+
"aria-valuemin": min,
|
|
78
|
+
// Omitted, not zeroed. `aria-valuenow="0"` tells a reader that nothing
|
|
79
|
+
// has happened; leaving it out tells them the amount is unknown, which
|
|
80
|
+
// is the true one and the one a spinner means.
|
|
81
|
+
"aria-valuenow": known ?? undefined,
|
|
82
|
+
"aria-valuetext": valueText,
|
|
83
|
+
children,
|
|
84
|
+
role: "progressbar",
|
|
85
|
+
});
|
|
70
86
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
aria-valuemin={min}
|
|
76
|
-
// Omitted, not zeroed. `aria-valuenow="0"` tells a reader that nothing
|
|
77
|
-
// has happened; leaving it out tells them the amount is unknown, which
|
|
78
|
-
// is the true one and the one a spinner means.
|
|
79
|
-
aria-valuenow={known ?? undefined}
|
|
80
|
-
aria-valuetext={valueText}
|
|
81
|
-
role="progressbar"
|
|
82
|
-
>
|
|
83
|
-
{children}
|
|
84
|
-
</div>
|
|
85
|
-
);
|
|
87
|
+
if (render != null) {
|
|
88
|
+
return render(props);
|
|
89
|
+
}
|
|
90
|
+
return <div {...props} />;
|
|
86
91
|
}
|