@uniflowed/ui 0.0.0-alpha.9 → 0.2.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 +84 -57
- package/alert-dialog.js +284 -0
- package/alert.js +142 -0
- package/avatar.js +280 -0
- package/breadcrumb.js +138 -0
- package/calendar.js +587 -0
- package/carousel.js +410 -0
- package/checkbox.js +215 -31
- package/collapsible.js +72 -48
- package/color-picker.js +172 -0
- package/combobox.js +216 -39
- package/context-menu.js +215 -0
- package/date-field.js +9 -0
- package/date-picker.js +357 -0
- package/date-range-picker.js +120 -0
- package/dialog.js +243 -178
- package/drag-drop.js +125 -0
- package/drawer.js +504 -0
- package/field.js +260 -43
- package/grid-list.js +8 -0
- package/hover-card.js +52 -52
- package/i18n-provider.js +89 -0
- package/index.js +1177 -31
- package/input-otp.js +218 -0
- package/interactions.js +2327 -0
- package/internal/anchor.js +71 -6
- package/internal/collection.js +562 -0
- package/internal/date-grid.js +260 -0
- package/internal/date-range.js +26 -0
- package/internal/disclosure.js +201 -0
- package/internal/menu-tree.js +228 -0
- package/internal/merge-props.js +85 -1
- package/internal/roving-focus.js +15 -4
- package/internal/segmented-field.js +317 -0
- package/internal/selection.js +171 -0
- package/internal/visually-hidden-style.js +41 -0
- package/list-box.js +13 -0
- package/menu.js +553 -361
- package/menubar.js +295 -0
- package/number-field.js +263 -0
- package/package.json +8 -28
- package/pagination.js +34 -22
- package/popover.js +116 -75
- package/progress.js +21 -16
- package/radio-group.js +81 -75
- package/range-calendar.js +79 -0
- package/resizable.js +155 -9
- package/scroll-area.js +283 -0
- package/select.js +83 -37
- package/separator.js +97 -0
- package/sheet.js +189 -0
- package/sidebar.js +320 -0
- package/skeleton.js +163 -0
- package/slider.js +95 -89
- package/switch.js +42 -34
- package/table.js +100 -71
- package/tabs.js +100 -91
- package/tag-group.js +8 -0
- package/time-field.js +8 -0
- package/toast.js +36 -66
- package/toggle-group.js +53 -49
- package/toggle.js +41 -27
- package/tooltip.js +48 -55
- package/tree.js +8 -0
- package/visually-hidden.js +259 -0
package/slider.js
CHANGED
|
@@ -76,8 +76,13 @@ import * as React from "@uniflowed/react";
|
|
|
76
76
|
import { createContext, useContext, useMemo, useRef } from "@uniflowed/react";
|
|
77
77
|
import { useStableCallback } from "@uniflowed/hooks/lifecycle";
|
|
78
78
|
|
|
79
|
-
import type { Rest } from "./internal/merge-props.js";
|
|
80
|
-
import {
|
|
79
|
+
import type { RenderProp, Rest } from "./internal/merge-props.js";
|
|
80
|
+
import {
|
|
81
|
+
composeHandlers,
|
|
82
|
+
composeRefs,
|
|
83
|
+
withProps,
|
|
84
|
+
withoutComposed,
|
|
85
|
+
} from "./internal/merge-props.js";
|
|
81
86
|
import type { Orientation } from "./internal/roving-focus.js";
|
|
82
87
|
import { clamp, fraction, isReversed, snap } from "./internal/range.js";
|
|
83
88
|
import { useControlled } from "./internal/controlled-state.js";
|
|
@@ -128,6 +133,7 @@ export component SliderRoot(
|
|
|
128
133
|
orientation?: Orientation = "horizontal",
|
|
129
134
|
disabled?: boolean = false,
|
|
130
135
|
valueText?: (value: number, index: number) => string,
|
|
136
|
+
render?: RenderProp,
|
|
131
137
|
...rest: Rest
|
|
132
138
|
) {
|
|
133
139
|
const [values, setValues] = useControlled(value, defaultValue, onValueChange);
|
|
@@ -190,7 +196,7 @@ export component SliderRoot(
|
|
|
190
196
|
|
|
191
197
|
return (
|
|
192
198
|
<SliderContext.Provider value={state}>
|
|
193
|
-
<div {...rest}>{children}</div>
|
|
199
|
+
{render == null ? <div {...rest}>{children}</div> : render(withProps(rest, { children }))}
|
|
194
200
|
</SliderContext.Provider>
|
|
195
201
|
);
|
|
196
202
|
}
|
|
@@ -203,7 +209,7 @@ export component SliderRoot(
|
|
|
203
209
|
* wanders off the track — which every drag does — keeps arriving here instead
|
|
204
210
|
* of being lost to whatever it wandered over.
|
|
205
211
|
*/
|
|
206
|
-
export component SliderTrack(children: React.Node, ...rest: Rest) {
|
|
212
|
+
export component SliderTrack(children: React.Node, render?: RenderProp, ...rest: Rest) {
|
|
207
213
|
const slider = useSlider("Slider.Track");
|
|
208
214
|
const passed = withoutComposed(rest, ["onPointerDown", "onPointerMove", "onPointerUp", "ref"]);
|
|
209
215
|
const dragging = useRef<number | null>(null);
|
|
@@ -239,35 +245,39 @@ export component SliderTrack(children: React.Node, ...rest: Rest) {
|
|
|
239
245
|
slider.setAt(at, target);
|
|
240
246
|
};
|
|
241
247
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
248
|
+
const props = withProps(passed, {
|
|
249
|
+
children,
|
|
250
|
+
// Pointer handlers keep the active thumb in a ref between events.
|
|
251
|
+
// uf-lint-disable-next-line react-compiler/refs
|
|
252
|
+
onPointerDown: composeHandlers(rest.onPointerDown, (event: $FlowFixMe) => {
|
|
253
|
+
if (slider.disabled) {
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
// Otherwise the press selects the page's text on the way past, which
|
|
257
|
+
// makes a drag paint everything blue.
|
|
258
|
+
event.preventDefault();
|
|
259
|
+
event.currentTarget?.setPointerCapture?.(event.pointerId);
|
|
260
|
+
moveTo(event, null);
|
|
261
|
+
}),
|
|
262
|
+
// uf-lint-disable-next-line react-compiler/refs
|
|
263
|
+
onPointerMove: composeHandlers(rest.onPointerMove, (event: $FlowFixMe) => {
|
|
264
|
+
if (dragging.current != null) {
|
|
265
|
+
moveTo(event, dragging.current);
|
|
266
|
+
}
|
|
267
|
+
}),
|
|
268
|
+
// uf-lint-disable-next-line react-compiler/refs
|
|
269
|
+
onPointerUp: composeHandlers(rest.onPointerUp, (event: $FlowFixMe) => {
|
|
270
|
+
dragging.current = null;
|
|
271
|
+
event.currentTarget?.releasePointerCapture?.(event.pointerId);
|
|
272
|
+
}),
|
|
273
|
+
ref: composeRefs(rest.ref, (element: HTMLElement | null) => {
|
|
274
|
+
// React calls callback refs during commit; pointer handlers read the track later.
|
|
275
|
+
// uf-lint-disable-next-line react-compiler/immutability
|
|
276
|
+
slider.trackRef.current = element;
|
|
277
|
+
}),
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
return render == null ? <div {...props} /> : render(props);
|
|
271
281
|
}
|
|
272
282
|
|
|
273
283
|
/**
|
|
@@ -281,24 +291,22 @@ export component SliderTrack(children: React.Node, ...rest: Rest) {
|
|
|
281
291
|
* is told the value by the thumb and telling them again here would be telling
|
|
282
292
|
* them twice.
|
|
283
293
|
*/
|
|
284
|
-
export component SliderRange(...rest: Rest) {
|
|
294
|
+
export component SliderRange(render?: RenderProp, ...rest: Rest) {
|
|
285
295
|
const slider = useSlider("Slider.Range");
|
|
286
296
|
const passed = withoutComposed(rest, ["style"]);
|
|
287
297
|
const ends = [...slider.values].sort((first, second) => first - second);
|
|
288
298
|
const start = slider.values.length > 1 ? (ends[0] ?? slider.min) : slider.min;
|
|
289
299
|
const end = ends[ends.length - 1] ?? slider.min;
|
|
300
|
+
const props = withProps(passed, {
|
|
301
|
+
"aria-hidden": "true",
|
|
302
|
+
style: {
|
|
303
|
+
...(rest.style as $FlowFixMe),
|
|
304
|
+
"--uf-slider-start": fraction(start, slider.min, slider.max),
|
|
305
|
+
"--uf-slider-end": fraction(end, slider.min, slider.max),
|
|
306
|
+
},
|
|
307
|
+
});
|
|
290
308
|
|
|
291
|
-
return (
|
|
292
|
-
<div
|
|
293
|
-
{...passed}
|
|
294
|
-
aria-hidden="true"
|
|
295
|
-
style={{
|
|
296
|
-
...(rest.style as $FlowFixMe),
|
|
297
|
-
"--uf-slider-start": fraction(start, slider.min, slider.max),
|
|
298
|
-
"--uf-slider-end": fraction(end, slider.min, slider.max),
|
|
299
|
-
}}
|
|
300
|
-
/>
|
|
301
|
-
);
|
|
309
|
+
return render == null ? <div {...props} /> : render(props);
|
|
302
310
|
}
|
|
303
311
|
|
|
304
312
|
/**
|
|
@@ -315,55 +323,53 @@ export component SliderRange(...rest: Rest) {
|
|
|
315
323
|
* "Maximum" told apart is the whole difference between a control a reader can
|
|
316
324
|
* operate and two identical "slider"s.
|
|
317
325
|
*/
|
|
318
|
-
export component SliderThumb(index?: number = 0, ...rest: Rest) {
|
|
326
|
+
export component SliderThumb(index?: number = 0, render?: RenderProp, ...rest: Rest) {
|
|
319
327
|
const slider = useSlider("Slider.Thumb");
|
|
320
328
|
const passed = withoutComposed(rest, ["onKeyDown", "style"]);
|
|
321
329
|
const value = slider.values[index] ?? slider.min;
|
|
322
330
|
const [lower, upper] = boundsOf(slider.values, index, slider.min, slider.max);
|
|
331
|
+
const props = withProps(passed, {
|
|
332
|
+
"aria-disabled": slider.disabled ? "true" : undefined,
|
|
333
|
+
"aria-orientation": slider.orientation,
|
|
334
|
+
// The neighbour's value, not the slider's end. A reader told they may
|
|
335
|
+
// set this thumb to 90 while the control refuses at 60 has been told
|
|
336
|
+
// something the control disagrees with.
|
|
337
|
+
"aria-valuemax": upper,
|
|
338
|
+
"aria-valuemin": lower,
|
|
339
|
+
"aria-valuenow": value,
|
|
340
|
+
"aria-valuetext": slider.valueText?.(value, index),
|
|
341
|
+
onKeyDown: composeHandlers(rest.onKeyDown, (event: $FlowFixMe) => {
|
|
342
|
+
if (slider.disabled) {
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
const reversed = isReversed(event.currentTarget, slider.orientation);
|
|
346
|
+
const move = stepFor(event.key, slider.step, slider.largeStep, reversed);
|
|
347
|
+
if (move != null) {
|
|
348
|
+
// Before moving: the arrow keys scroll the page, and a slider that
|
|
349
|
+
// moves the page under the reader as it moves the value is a control
|
|
350
|
+
// they cannot watch.
|
|
351
|
+
event.preventDefault();
|
|
352
|
+
slider.setAt(index, value + move);
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
if (event.key === "Home" || event.key === "End") {
|
|
356
|
+
event.preventDefault();
|
|
357
|
+
// This thumb's own ends, which for the lower thumb of a range is its
|
|
358
|
+
// neighbour rather than the slider's maximum.
|
|
359
|
+
slider.setAt(index, event.key === "Home" ? lower : upper);
|
|
360
|
+
}
|
|
361
|
+
}),
|
|
362
|
+
role: "slider",
|
|
363
|
+
style: {
|
|
364
|
+
...(rest.style as $FlowFixMe),
|
|
365
|
+
"--uf-slider-fraction": fraction(value, slider.min, slider.max),
|
|
366
|
+
},
|
|
367
|
+
// In the tab sequence, and out of it while disabled — the browser does
|
|
368
|
+
// this for a real control and there is no real control here to do it.
|
|
369
|
+
tabIndex: slider.disabled ? -1 : 0,
|
|
370
|
+
});
|
|
323
371
|
|
|
324
|
-
return (
|
|
325
|
-
<span
|
|
326
|
-
{...passed}
|
|
327
|
-
aria-disabled={slider.disabled ? "true" : undefined}
|
|
328
|
-
aria-orientation={slider.orientation}
|
|
329
|
-
// The neighbour's value, not the slider's end. A reader told they may
|
|
330
|
-
// set this thumb to 90 while the control refuses at 60 has been told
|
|
331
|
-
// something the control disagrees with.
|
|
332
|
-
aria-valuemax={upper}
|
|
333
|
-
aria-valuemin={lower}
|
|
334
|
-
aria-valuenow={value}
|
|
335
|
-
aria-valuetext={slider.valueText?.(value, index)}
|
|
336
|
-
onKeyDown={composeHandlers(rest.onKeyDown, (event: $FlowFixMe) => {
|
|
337
|
-
if (slider.disabled) {
|
|
338
|
-
return;
|
|
339
|
-
}
|
|
340
|
-
const reversed = isReversed(event.currentTarget, slider.orientation);
|
|
341
|
-
const move = stepFor(event.key, slider.step, slider.largeStep, reversed);
|
|
342
|
-
if (move != null) {
|
|
343
|
-
// Before moving: the arrow keys scroll the page, and a slider that
|
|
344
|
-
// moves the page under the reader as it moves the value is a control
|
|
345
|
-
// they cannot watch.
|
|
346
|
-
event.preventDefault();
|
|
347
|
-
slider.setAt(index, value + move);
|
|
348
|
-
return;
|
|
349
|
-
}
|
|
350
|
-
if (event.key === "Home" || event.key === "End") {
|
|
351
|
-
event.preventDefault();
|
|
352
|
-
// This thumb's own ends, which for the lower thumb of a range is its
|
|
353
|
-
// neighbour rather than the slider's maximum.
|
|
354
|
-
slider.setAt(index, event.key === "Home" ? lower : upper);
|
|
355
|
-
}
|
|
356
|
-
})}
|
|
357
|
-
role="slider"
|
|
358
|
-
style={{
|
|
359
|
-
...(rest.style as $FlowFixMe),
|
|
360
|
-
"--uf-slider-fraction": fraction(value, slider.min, slider.max),
|
|
361
|
-
}}
|
|
362
|
-
// In the tab sequence, and out of it while disabled — the browser does
|
|
363
|
-
// this for a real control and there is no real control here to do it.
|
|
364
|
-
tabIndex={slider.disabled ? -1 : 0}
|
|
365
|
-
/>
|
|
366
|
-
);
|
|
372
|
+
return render == null ? <span {...props} /> : render(props);
|
|
367
373
|
}
|
|
368
374
|
|
|
369
375
|
/**
|
package/switch.js
CHANGED
|
@@ -19,55 +19,63 @@
|
|
|
19
19
|
//
|
|
20
20
|
// The keyboard follows from the same distinction. `Space` toggles both. `Enter`
|
|
21
21
|
// toggles a *switch*, because a switch is an operation and pressing Enter on
|
|
22
|
-
// something that operates is what a reader expects — while `checkbox.js`
|
|
23
|
-
//
|
|
24
|
-
//
|
|
22
|
+
// something that operates is what a reader expects — while `checkbox.js` turns
|
|
23
|
+
// `Enter` into the submission of the form the checkbox is in, which is what a
|
|
24
|
+
// native `<input type="checkbox">` does with the key and what a reader
|
|
25
|
+
// answering a question on their way to a submit button is asking for. That is
|
|
26
|
+
// the whole reason these are not one file with a flag.
|
|
25
27
|
|
|
26
28
|
"use client";
|
|
27
29
|
|
|
28
30
|
import * as React from "@uniflowed/react";
|
|
29
31
|
|
|
30
|
-
import type { Rest } from "./internal/merge-props.js";
|
|
31
|
-
import { composeHandlers, withoutComposed } from "./internal/merge-props.js";
|
|
32
|
+
import type { PartEvent, RenderProp, Rest } from "./internal/merge-props.js";
|
|
33
|
+
import { composeHandlers, withProps, withoutComposed } from "./internal/merge-props.js";
|
|
32
34
|
import { useControlled } from "./internal/controlled-state.js";
|
|
33
35
|
|
|
34
|
-
/**
|
|
36
|
+
/**
|
|
37
|
+
* A two-state switch: on or off.
|
|
38
|
+
*
|
|
39
|
+
* `render` is the escape hatch, and on a switch it is what a caller with their
|
|
40
|
+
* own `<Pressable>` or a `<div role="switch">` in a design system reaches for.
|
|
41
|
+
* `type="button"` is the one thing it does not hand over, because that is true
|
|
42
|
+
* of the element rather than of the switch.
|
|
43
|
+
*/
|
|
35
44
|
export component Switch(
|
|
36
45
|
checked?: boolean,
|
|
37
46
|
defaultChecked?: boolean = false,
|
|
38
47
|
onCheckedChange?: (checked: boolean) => void,
|
|
39
48
|
disabled?: boolean = false,
|
|
40
49
|
children?: React.Node,
|
|
50
|
+
render?: RenderProp,
|
|
41
51
|
...rest: Rest
|
|
42
52
|
) {
|
|
43
53
|
const [on, setOn] = useControlled(checked, defaultChecked, onCheckedChange);
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
disabled={disabled}
|
|
51
|
-
onClick={composeHandlers(rest.onClick, () => {
|
|
52
|
-
if (!disabled) {
|
|
53
|
-
setOn(!on);
|
|
54
|
-
}
|
|
55
|
-
})}
|
|
56
|
-
onKeyDown={composeHandlers(rest.onKeyDown, (event) => {
|
|
57
|
-
if (disabled || (event.key !== " " && event.key !== "Enter")) {
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
// Preventing the default is not decoration. It stops `Space` scrolling
|
|
61
|
-
// the page — which is what makes a hand-written toggle feel broken even
|
|
62
|
-
// when it works — and it stops the browser's own click from arriving
|
|
63
|
-
// after this handler and toggling the switch a second time.
|
|
64
|
-
event.preventDefault();
|
|
54
|
+
const props = withProps(withoutComposed(rest, ["onClick", "onKeyDown"]), {
|
|
55
|
+
"aria-checked": on ? "true" : "false",
|
|
56
|
+
children,
|
|
57
|
+
disabled,
|
|
58
|
+
onClick: composeHandlers(rest.onClick, () => {
|
|
59
|
+
if (!disabled) {
|
|
65
60
|
setOn(!on);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
61
|
+
}
|
|
62
|
+
}),
|
|
63
|
+
onKeyDown: composeHandlers(rest.onKeyDown, (event: PartEvent) => {
|
|
64
|
+
if (disabled || (event.key !== " " && event.key !== "Enter")) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
// Preventing the default is not decoration. It stops `Space` scrolling
|
|
68
|
+
// the page — which is what makes a hand-written toggle feel broken even
|
|
69
|
+
// when it works — and it stops the browser's own click from arriving
|
|
70
|
+
// after this handler and toggling the switch a second time.
|
|
71
|
+
event.preventDefault();
|
|
72
|
+
setOn(!on);
|
|
73
|
+
}),
|
|
74
|
+
role: "switch",
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
if (render != null) {
|
|
78
|
+
return render(props);
|
|
79
|
+
}
|
|
80
|
+
return <button {...props} type="button" />;
|
|
73
81
|
}
|
package/table.js
CHANGED
|
@@ -57,9 +57,8 @@
|
|
|
57
57
|
// It is rendered by the root rather than offered as a part a caller places,
|
|
58
58
|
// because a sort that announces nothing is the failure this component exists
|
|
59
59
|
// to prevent and a part is a thing somebody forgets. The cost is one extra
|
|
60
|
-
// element
|
|
61
|
-
//
|
|
62
|
-
// status in words is a table more people can use.
|
|
60
|
+
// element after the table, hidden from layout and visible to the screen readers
|
|
61
|
+
// watching it.
|
|
63
62
|
|
|
64
63
|
"use client";
|
|
65
64
|
|
|
@@ -68,9 +67,15 @@ import { createContext, useContext, useEffect, useId, useMemo, useState } from "
|
|
|
68
67
|
import { useStableCallback } from "@uniflowed/hooks/lifecycle";
|
|
69
68
|
|
|
70
69
|
import { Checkbox } from "./checkbox.js";
|
|
71
|
-
import type { Rest } from "./internal/merge-props.js";
|
|
72
|
-
import {
|
|
70
|
+
import type { RenderProp, Rest } from "./internal/merge-props.js";
|
|
71
|
+
import {
|
|
72
|
+
composeHandlers,
|
|
73
|
+
composeRefs,
|
|
74
|
+
withProps,
|
|
75
|
+
withoutComposed,
|
|
76
|
+
} from "./internal/merge-props.js";
|
|
73
77
|
import { useControlled } from "./internal/controlled-state.js";
|
|
78
|
+
import { visuallyHiddenStyle } from "./internal/visually-hidden-style.js";
|
|
74
79
|
|
|
75
80
|
/** Which column a table is sorted by, and which way. */
|
|
76
81
|
export type Sort = {|
|
|
@@ -130,6 +135,7 @@ export component TableRoot(
|
|
|
130
135
|
rowCount?: number | null = null,
|
|
131
136
|
rowOffset?: number = 0,
|
|
132
137
|
announceSort?: (column: string, direction: "ascending" | "descending") => string,
|
|
138
|
+
render?: RenderProp,
|
|
133
139
|
...rest: Rest
|
|
134
140
|
) {
|
|
135
141
|
const base = useId();
|
|
@@ -184,30 +190,28 @@ export component TableRoot(
|
|
|
184
190
|
labels[current.column] ?? current.column,
|
|
185
191
|
current.direction,
|
|
186
192
|
);
|
|
193
|
+
const props = withProps(rest, {
|
|
194
|
+
"aria-labelledby": captioned ? `${base}-caption` : undefined,
|
|
195
|
+
"aria-rowcount": rowCount == null ? undefined : rowCount + headerRows,
|
|
196
|
+
children,
|
|
197
|
+
});
|
|
187
198
|
|
|
188
199
|
return (
|
|
189
200
|
<TableContext.Provider value={state}>
|
|
190
|
-
<table
|
|
191
|
-
{...rest}
|
|
192
|
-
// The caption is a `<table>`'s accessible name in HTML-AAM already;
|
|
193
|
-
// naming it again here is what turns "the host will probably do this"
|
|
194
|
-
// into something this component promises, and it costs nothing because
|
|
195
|
-
// both point at the same words. Only while a caption is rendered, for
|
|
196
|
-
// the reason every part of this package repeats.
|
|
197
|
-
aria-labelledby={captioned ? `${base}-caption` : undefined}
|
|
198
|
-
// Every row in the table, which is the data plus the header — the
|
|
199
|
-
// arithmetic a caller should not have to remember.
|
|
200
|
-
aria-rowcount={rowCount == null ? undefined : rowCount + headerRows}
|
|
201
|
-
>
|
|
202
|
-
{children}
|
|
203
|
-
</table>
|
|
201
|
+
{render == null ? <table {...props} /> : render(withProps(props, { role: "table" }))}
|
|
204
202
|
{/*
|
|
205
203
|
Beside the table rather than inside it, because a `<table>` may only
|
|
206
204
|
contain a caption, column groups and row groups — and mounted from the
|
|
207
205
|
first render, holding nothing, because a live region that appears with
|
|
208
206
|
its text is a live region that says nothing.
|
|
209
207
|
*/}
|
|
210
|
-
<div
|
|
208
|
+
<div
|
|
209
|
+
aria-atomic="true"
|
|
210
|
+
aria-live="polite"
|
|
211
|
+
data-uf-table-status=""
|
|
212
|
+
role="status"
|
|
213
|
+
style={visuallyHiddenStyle}
|
|
214
|
+
>
|
|
211
215
|
{message}
|
|
212
216
|
</div>
|
|
213
217
|
</TableContext.Provider>
|
|
@@ -221,7 +225,7 @@ export component TableRoot(
|
|
|
221
225
|
* what a screen reader reads when a reader lands on it. A heading above the
|
|
222
226
|
* table looks the same and is not the table's name.
|
|
223
227
|
*/
|
|
224
|
-
export component TableCaption(children: React.Node, ...rest: Rest) {
|
|
228
|
+
export component TableCaption(children: React.Node, render?: RenderProp, ...rest: Rest) {
|
|
225
229
|
const table = useTable("Table.Caption");
|
|
226
230
|
const register = table.registerCaption;
|
|
227
231
|
|
|
@@ -230,11 +234,11 @@ export component TableCaption(children: React.Node, ...rest: Rest) {
|
|
|
230
234
|
return () => register(false);
|
|
231
235
|
}, [register]);
|
|
232
236
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
237
|
+
const props = withProps(rest, { children, id: table.captionId });
|
|
238
|
+
if (render != null) {
|
|
239
|
+
return render(withProps(props, { role: "caption" }));
|
|
240
|
+
}
|
|
241
|
+
return <caption {...props} />;
|
|
238
242
|
}
|
|
239
243
|
|
|
240
244
|
/**
|
|
@@ -243,7 +247,7 @@ export component TableCaption(children: React.Node, ...rest: Rest) {
|
|
|
243
247
|
* It tells the root that it exists, because `aria-rowcount` and every row's
|
|
244
248
|
* `aria-rowindex` count header rows and a table without one counts differently.
|
|
245
249
|
*/
|
|
246
|
-
export component TableHeader(children: React.Node, ...rest: Rest) {
|
|
250
|
+
export component TableHeader(children: React.Node, render?: RenderProp, ...rest: Rest) {
|
|
247
251
|
const table = useTable("Table.Header");
|
|
248
252
|
const register = table.registerHeader;
|
|
249
253
|
|
|
@@ -252,18 +256,21 @@ export component TableHeader(children: React.Node, ...rest: Rest) {
|
|
|
252
256
|
return () => register(false);
|
|
253
257
|
}, [register]);
|
|
254
258
|
|
|
259
|
+
const props = withProps(rest, { children });
|
|
260
|
+
|
|
255
261
|
return (
|
|
256
262
|
<HeaderContext.Provider value={true}>
|
|
257
|
-
<thead {...
|
|
263
|
+
{render == null ? <thead {...props} /> : render(withProps(props, { role: "rowgroup" }))}
|
|
258
264
|
</HeaderContext.Provider>
|
|
259
265
|
);
|
|
260
266
|
}
|
|
261
267
|
|
|
262
268
|
/** The data rows. */
|
|
263
|
-
export component TableBody(children: React.Node, ...rest: Rest) {
|
|
269
|
+
export component TableBody(children: React.Node, render?: RenderProp, ...rest: Rest) {
|
|
270
|
+
const props = withProps(rest, { children });
|
|
264
271
|
return (
|
|
265
272
|
<HeaderContext.Provider value={false}>
|
|
266
|
-
<tbody {...
|
|
273
|
+
{render == null ? <tbody {...props} /> : render(withProps(props, { role: "rowgroup" }))}
|
|
267
274
|
</HeaderContext.Provider>
|
|
268
275
|
);
|
|
269
276
|
}
|
|
@@ -284,7 +291,12 @@ export component TableBody(children: React.Node, ...rest: Rest) {
|
|
|
284
291
|
* adding them anyway is a second source of truth that can disagree with the
|
|
285
292
|
* document.
|
|
286
293
|
*/
|
|
287
|
-
export component TableRow(
|
|
294
|
+
export component TableRow(
|
|
295
|
+
children: React.Node,
|
|
296
|
+
index?: number | null = null,
|
|
297
|
+
render?: RenderProp,
|
|
298
|
+
...rest: Rest
|
|
299
|
+
) {
|
|
288
300
|
const table = useTable("Table.Row");
|
|
289
301
|
const header = useContext(HeaderContext);
|
|
290
302
|
const counted = table.rowCount != null;
|
|
@@ -300,11 +312,11 @@ export component TableRow(children: React.Node, index?: number | null = null, ..
|
|
|
300
312
|
rowIndex = table.headerRows + table.rowOffset + (index ?? 0) + 1;
|
|
301
313
|
}
|
|
302
314
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
315
|
+
const props = withProps(rest, { "aria-rowindex": rowIndex, children });
|
|
316
|
+
if (render != null) {
|
|
317
|
+
return render(withProps(props, { role: "row" }));
|
|
318
|
+
}
|
|
319
|
+
return <tr {...props} />;
|
|
308
320
|
}
|
|
309
321
|
|
|
310
322
|
/**
|
|
@@ -320,7 +332,12 @@ export component TableRow(children: React.Node, index?: number | null = null, ..
|
|
|
320
332
|
* Not `"none"` on the others: eleven headers each announcing "not sorted" is
|
|
321
333
|
* eleven announcements of nothing, on every pass through the table.
|
|
322
334
|
*/
|
|
323
|
-
export component TableHead(
|
|
335
|
+
export component TableHead(
|
|
336
|
+
children: React.Node,
|
|
337
|
+
column?: string | null = null,
|
|
338
|
+
render?: RenderProp,
|
|
339
|
+
...rest: Rest
|
|
340
|
+
) {
|
|
324
341
|
const table = useTable("Table.Head");
|
|
325
342
|
const passed = withoutComposed(rest, column == null ? [] : ["onClick", "ref"]);
|
|
326
343
|
const sorted = column != null && table.sort?.column === column;
|
|
@@ -341,41 +358,49 @@ export component TableHead(children: React.Node, column?: string | null = null,
|
|
|
341
358
|
});
|
|
342
359
|
|
|
343
360
|
if (column == null) {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
361
|
+
const props = withProps(rest, { children, scope: "col" });
|
|
362
|
+
if (render != null) {
|
|
363
|
+
return render(withProps(props, { role: "columnheader" }));
|
|
364
|
+
}
|
|
365
|
+
return <th {...props} />;
|
|
349
366
|
}
|
|
350
367
|
|
|
351
|
-
|
|
352
|
-
<
|
|
353
|
-
{
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
368
|
+
const button = (
|
|
369
|
+
<button
|
|
370
|
+
onClick={composeHandlers(rest.onClick, () => {
|
|
371
|
+
// Two states, not three. A sort that cycles back to "unsorted" gives
|
|
372
|
+
// a reader a third press whose result is a table in an order nobody
|
|
373
|
+
// asked for.
|
|
374
|
+
table.setSort({
|
|
375
|
+
column,
|
|
376
|
+
direction: sorted && table.sort?.direction === "ascending" ? "descending" : "ascending",
|
|
377
|
+
});
|
|
378
|
+
})}
|
|
379
|
+
type="button"
|
|
357
380
|
>
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
// Two states, not three. A sort that cycles back to "unsorted" gives
|
|
361
|
-
// a reader a third press whose result is a table in an order nobody
|
|
362
|
-
// asked for.
|
|
363
|
-
table.setSort({
|
|
364
|
-
column,
|
|
365
|
-
direction: sorted && table.sort?.direction === "ascending" ? "descending" : "ascending",
|
|
366
|
-
});
|
|
367
|
-
})}
|
|
368
|
-
type="button"
|
|
369
|
-
>
|
|
370
|
-
{children}
|
|
371
|
-
</button>
|
|
372
|
-
</th>
|
|
381
|
+
{children}
|
|
382
|
+
</button>
|
|
373
383
|
);
|
|
384
|
+
const props = withProps(passed, {
|
|
385
|
+
"aria-sort": sorted ? table.sort?.direction : undefined,
|
|
386
|
+
children: button,
|
|
387
|
+
ref: composeRefs(rest.ref, setElement),
|
|
388
|
+
scope: "col",
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
if (render != null) {
|
|
392
|
+
return render(withProps(props, { role: "columnheader" }));
|
|
393
|
+
}
|
|
394
|
+
return <th {...props} />;
|
|
374
395
|
}
|
|
375
396
|
|
|
376
397
|
/** One cell. */
|
|
377
|
-
export component TableCell(children: React.Node, ...rest: Rest) {
|
|
378
|
-
|
|
398
|
+
export component TableCell(children: React.Node, render?: RenderProp, ...rest: Rest) {
|
|
399
|
+
const props = withProps(rest, { children });
|
|
400
|
+
if (render != null) {
|
|
401
|
+
return render(withProps(props, { role: "cell" }));
|
|
402
|
+
}
|
|
403
|
+
return <td {...props} />;
|
|
379
404
|
}
|
|
380
405
|
|
|
381
406
|
/**
|
|
@@ -386,12 +411,12 @@ export component TableCell(children: React.Node, ...rest: Rest) {
|
|
|
386
411
|
* moves down the year column. A table of records usually has one and almost
|
|
387
412
|
* never marks it.
|
|
388
413
|
*/
|
|
389
|
-
export component TableRowHeader(children: React.Node, ...rest: Rest) {
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
414
|
+
export component TableRowHeader(children: React.Node, render?: RenderProp, ...rest: Rest) {
|
|
415
|
+
const props = withProps(rest, { children, scope: "row" });
|
|
416
|
+
if (render != null) {
|
|
417
|
+
return render(withProps(props, { role: "rowheader" }));
|
|
418
|
+
}
|
|
419
|
+
return <th {...props} />;
|
|
395
420
|
}
|
|
396
421
|
|
|
397
422
|
/**
|
|
@@ -430,6 +455,7 @@ export component TableSelectAll(
|
|
|
430
455
|
label?: string = "Select all rows",
|
|
431
456
|
className?: string,
|
|
432
457
|
disabled?: boolean = false,
|
|
458
|
+
render?: RenderProp,
|
|
433
459
|
) {
|
|
434
460
|
return (
|
|
435
461
|
<Checkbox
|
|
@@ -439,6 +465,7 @@ export component TableSelectAll(
|
|
|
439
465
|
disabled={disabled}
|
|
440
466
|
indeterminate={checked === "mixed"}
|
|
441
467
|
onCheckedChange={onCheckedChange}
|
|
468
|
+
render={render}
|
|
442
469
|
/>
|
|
443
470
|
);
|
|
444
471
|
}
|
|
@@ -461,6 +488,7 @@ export component TableRowSelect(
|
|
|
461
488
|
onCheckedChange: (checked: boolean) => void,
|
|
462
489
|
className?: string,
|
|
463
490
|
disabled?: boolean = false,
|
|
491
|
+
render?: RenderProp,
|
|
464
492
|
) {
|
|
465
493
|
return (
|
|
466
494
|
<Checkbox
|
|
@@ -469,6 +497,7 @@ export component TableRowSelect(
|
|
|
469
497
|
className={className}
|
|
470
498
|
disabled={disabled}
|
|
471
499
|
onCheckedChange={onCheckedChange}
|
|
500
|
+
render={render}
|
|
472
501
|
/>
|
|
473
502
|
);
|
|
474
503
|
}
|