@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/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 { composeHandlers, composeRefs, withoutComposed } from "./internal/merge-props.js";
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 passed = withoutComposed(rest, ["onKeyDown", "ref"]);
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
- return (
250
- <button
251
- {...passed}
252
- aria-controls={menu.open ? `${menu.base}-body` : undefined}
253
- aria-expanded={menu.open ? "true" : "false"}
254
- aria-haspopup="menu"
255
- // How the bar's keyboard turns a trigger element back into the menu it
256
- // opens; the module header says why this is an attribute and the rest of
257
- // the bar's arithmetic is not.
258
- data-uf-menubar-value={value}
259
- id={id}
260
- onClick={composeHandlers(rest.onClick, () => bar.setOpen(menu.open ? null : value))}
261
- onFocus={composeHandlers(rest.onFocus, () => bar.setActive(value))}
262
- onKeyDown={composeHandlers(rest.onKeyDown, (event) => {
263
- const end = match (event.key) {
264
- "ArrowDown" => "first",
265
- "ArrowUp" => "last",
266
- _ => null,
267
- };
268
- if (end == null) {
269
- return;
270
- }
271
- event.preventDefault();
272
- menu.pendingFocus.current = end;
273
- bar.setOpen(value);
274
- })}
275
- ref={composeRefs(rest.ref, (element) => {
276
- menu.triggerRef.current = element;
277
- })}
278
- role="menuitem"
279
- tabIndex={stop ? 0 : -1}
280
- type="button"
281
- >
282
- {children}
283
- </button>
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.18",
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.18",
56
- "@uniflowed/hooks": "0.0.0-alpha.18",
57
- "@uniflowed/react": "0.0.0-alpha.18"
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 {...rest} aria-label={label}>
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
- return <ul {...rest}>{children}</ul>;
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 { composeHandlers, composeRefs, withoutComposed } from "./internal/merge-props.js";
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
- return (
154
- <button
155
- {...passed}
156
- // Only while it is open: an `aria-controls` naming an element that is not
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
- return (
307
- <div
308
- // `passed` first, then this component's semantics; see
309
- // `internal/merge-props.js` for the three bugs that rule is made of.
310
- {...passed}
311
- aria-labelledby={named || !popover.triggered ? undefined : `${popover.base}-trigger`}
312
- data-align={anchored.align}
313
- data-side={anchored.side}
314
- data-state="open"
315
- id={`${popover.base}-body`}
316
- onKeyDown={composeHandlers(rest.onKeyDown, (event) => {
317
- if (event.key !== "Escape") {
318
- return;
319
- }
320
- event.preventDefault();
321
- // This popover, not the dialog around it. Two overlays nest in the
322
- // DOM, so without this one Escape closed both.
323
- event.stopPropagation();
324
- close();
325
- })}
326
- ref={composeRefs(rest.ref, (element) => {
327
- bodyRef.current = element;
328
- })}
329
- // No `aria-modal`. The page behind a popover is still available, and
330
- // saying otherwise is the one lie a screen reader cannot see through.
331
- role="dialog"
332
- // So the popover can hold focus itself when it contains nothing
333
- // focusable, and so Escape has somewhere to be heard.
334
- tabIndex={-1}
335
- >
336
- {children}
337
- </div>
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
- return (
72
- <div
73
- {...rest}
74
- aria-valuemax={max}
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
  }