@uniflowed/ui 0.0.0-alpha.8 → 0.1.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 +560 -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 +235 -198
- 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 +334 -0
- package/i18n-provider.js +89 -0
- package/index.js +1254 -32
- package/input-otp.js +218 -0
- package/interactions.js +2327 -0
- package/internal/anchor.js +565 -0
- package/internal/collection.js +395 -0
- package/internal/date-grid.js +260 -0
- package/internal/date-range.js +26 -0
- package/internal/disclosure.js +201 -0
- package/internal/focus.js +64 -0
- package/internal/hover-intent.js +259 -0
- package/internal/menu-tree.js +228 -0
- package/internal/merge-props.js +117 -1
- package/internal/roving-focus.js +15 -4
- package/internal/segmented-field.js +316 -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 -25
- package/pagination.js +34 -22
- package/popover.js +367 -0
- package/progress.js +21 -16
- package/radio-group.js +81 -75
- package/range-calendar.js +78 -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 +112 -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 +404 -0
- package/tree.js +8 -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,8 +67,13 @@ 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";
|
|
74
78
|
|
|
75
79
|
/** Which column a table is sorted by, and which way. */
|
|
@@ -100,6 +104,19 @@ const TableContext: React.Context<TableState | null> = createContext(null);
|
|
|
100
104
|
/** Whether the rows below are header rows, which decides `th` versus `td`. */
|
|
101
105
|
const HeaderContext: React.Context<boolean> = createContext(false);
|
|
102
106
|
|
|
107
|
+
const VISUALLY_HIDDEN_STYLE = Object.freeze({
|
|
108
|
+
border: 0,
|
|
109
|
+
clip: "rect(0, 0, 0, 0)",
|
|
110
|
+
clipPath: "inset(50%)",
|
|
111
|
+
height: 1,
|
|
112
|
+
margin: -1,
|
|
113
|
+
overflow: "hidden",
|
|
114
|
+
padding: 0,
|
|
115
|
+
position: "absolute",
|
|
116
|
+
whiteSpace: "nowrap",
|
|
117
|
+
width: 1,
|
|
118
|
+
});
|
|
119
|
+
|
|
103
120
|
hook useTable(part: string): TableState {
|
|
104
121
|
const state = useContext(TableContext);
|
|
105
122
|
if (state == null) {
|
|
@@ -130,6 +147,7 @@ export component TableRoot(
|
|
|
130
147
|
rowCount?: number | null = null,
|
|
131
148
|
rowOffset?: number = 0,
|
|
132
149
|
announceSort?: (column: string, direction: "ascending" | "descending") => string,
|
|
150
|
+
render?: RenderProp,
|
|
133
151
|
...rest: Rest
|
|
134
152
|
) {
|
|
135
153
|
const base = useId();
|
|
@@ -184,30 +202,28 @@ export component TableRoot(
|
|
|
184
202
|
labels[current.column] ?? current.column,
|
|
185
203
|
current.direction,
|
|
186
204
|
);
|
|
205
|
+
const props = withProps(rest, {
|
|
206
|
+
"aria-labelledby": captioned ? `${base}-caption` : undefined,
|
|
207
|
+
"aria-rowcount": rowCount == null ? undefined : rowCount + headerRows,
|
|
208
|
+
children,
|
|
209
|
+
});
|
|
187
210
|
|
|
188
211
|
return (
|
|
189
212
|
<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>
|
|
213
|
+
{render == null ? <table {...props} /> : render(withProps(props, { role: "table" }))}
|
|
204
214
|
{/*
|
|
205
215
|
Beside the table rather than inside it, because a `<table>` may only
|
|
206
216
|
contain a caption, column groups and row groups — and mounted from the
|
|
207
217
|
first render, holding nothing, because a live region that appears with
|
|
208
218
|
its text is a live region that says nothing.
|
|
209
219
|
*/}
|
|
210
|
-
<div
|
|
220
|
+
<div
|
|
221
|
+
aria-atomic="true"
|
|
222
|
+
aria-live="polite"
|
|
223
|
+
data-uf-table-status=""
|
|
224
|
+
role="status"
|
|
225
|
+
style={VISUALLY_HIDDEN_STYLE}
|
|
226
|
+
>
|
|
211
227
|
{message}
|
|
212
228
|
</div>
|
|
213
229
|
</TableContext.Provider>
|
|
@@ -221,7 +237,7 @@ export component TableRoot(
|
|
|
221
237
|
* what a screen reader reads when a reader lands on it. A heading above the
|
|
222
238
|
* table looks the same and is not the table's name.
|
|
223
239
|
*/
|
|
224
|
-
export component TableCaption(children: React.Node, ...rest: Rest) {
|
|
240
|
+
export component TableCaption(children: React.Node, render?: RenderProp, ...rest: Rest) {
|
|
225
241
|
const table = useTable("Table.Caption");
|
|
226
242
|
const register = table.registerCaption;
|
|
227
243
|
|
|
@@ -230,11 +246,11 @@ export component TableCaption(children: React.Node, ...rest: Rest) {
|
|
|
230
246
|
return () => register(false);
|
|
231
247
|
}, [register]);
|
|
232
248
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
249
|
+
const props = withProps(rest, { children, id: table.captionId });
|
|
250
|
+
if (render != null) {
|
|
251
|
+
return render(withProps(props, { role: "caption" }));
|
|
252
|
+
}
|
|
253
|
+
return <caption {...props} />;
|
|
238
254
|
}
|
|
239
255
|
|
|
240
256
|
/**
|
|
@@ -243,7 +259,7 @@ export component TableCaption(children: React.Node, ...rest: Rest) {
|
|
|
243
259
|
* It tells the root that it exists, because `aria-rowcount` and every row's
|
|
244
260
|
* `aria-rowindex` count header rows and a table without one counts differently.
|
|
245
261
|
*/
|
|
246
|
-
export component TableHeader(children: React.Node, ...rest: Rest) {
|
|
262
|
+
export component TableHeader(children: React.Node, render?: RenderProp, ...rest: Rest) {
|
|
247
263
|
const table = useTable("Table.Header");
|
|
248
264
|
const register = table.registerHeader;
|
|
249
265
|
|
|
@@ -252,18 +268,21 @@ export component TableHeader(children: React.Node, ...rest: Rest) {
|
|
|
252
268
|
return () => register(false);
|
|
253
269
|
}, [register]);
|
|
254
270
|
|
|
271
|
+
const props = withProps(rest, { children });
|
|
272
|
+
|
|
255
273
|
return (
|
|
256
274
|
<HeaderContext.Provider value={true}>
|
|
257
|
-
<thead {...
|
|
275
|
+
{render == null ? <thead {...props} /> : render(withProps(props, { role: "rowgroup" }))}
|
|
258
276
|
</HeaderContext.Provider>
|
|
259
277
|
);
|
|
260
278
|
}
|
|
261
279
|
|
|
262
280
|
/** The data rows. */
|
|
263
|
-
export component TableBody(children: React.Node, ...rest: Rest) {
|
|
281
|
+
export component TableBody(children: React.Node, render?: RenderProp, ...rest: Rest) {
|
|
282
|
+
const props = withProps(rest, { children });
|
|
264
283
|
return (
|
|
265
284
|
<HeaderContext.Provider value={false}>
|
|
266
|
-
<tbody {...
|
|
285
|
+
{render == null ? <tbody {...props} /> : render(withProps(props, { role: "rowgroup" }))}
|
|
267
286
|
</HeaderContext.Provider>
|
|
268
287
|
);
|
|
269
288
|
}
|
|
@@ -284,7 +303,12 @@ export component TableBody(children: React.Node, ...rest: Rest) {
|
|
|
284
303
|
* adding them anyway is a second source of truth that can disagree with the
|
|
285
304
|
* document.
|
|
286
305
|
*/
|
|
287
|
-
export component TableRow(
|
|
306
|
+
export component TableRow(
|
|
307
|
+
children: React.Node,
|
|
308
|
+
index?: number | null = null,
|
|
309
|
+
render?: RenderProp,
|
|
310
|
+
...rest: Rest
|
|
311
|
+
) {
|
|
288
312
|
const table = useTable("Table.Row");
|
|
289
313
|
const header = useContext(HeaderContext);
|
|
290
314
|
const counted = table.rowCount != null;
|
|
@@ -300,11 +324,11 @@ export component TableRow(children: React.Node, index?: number | null = null, ..
|
|
|
300
324
|
rowIndex = table.headerRows + table.rowOffset + (index ?? 0) + 1;
|
|
301
325
|
}
|
|
302
326
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
327
|
+
const props = withProps(rest, { "aria-rowindex": rowIndex, children });
|
|
328
|
+
if (render != null) {
|
|
329
|
+
return render(withProps(props, { role: "row" }));
|
|
330
|
+
}
|
|
331
|
+
return <tr {...props} />;
|
|
308
332
|
}
|
|
309
333
|
|
|
310
334
|
/**
|
|
@@ -320,7 +344,12 @@ export component TableRow(children: React.Node, index?: number | null = null, ..
|
|
|
320
344
|
* Not `"none"` on the others: eleven headers each announcing "not sorted" is
|
|
321
345
|
* eleven announcements of nothing, on every pass through the table.
|
|
322
346
|
*/
|
|
323
|
-
export component TableHead(
|
|
347
|
+
export component TableHead(
|
|
348
|
+
children: React.Node,
|
|
349
|
+
column?: string | null = null,
|
|
350
|
+
render?: RenderProp,
|
|
351
|
+
...rest: Rest
|
|
352
|
+
) {
|
|
324
353
|
const table = useTable("Table.Head");
|
|
325
354
|
const passed = withoutComposed(rest, column == null ? [] : ["onClick", "ref"]);
|
|
326
355
|
const sorted = column != null && table.sort?.column === column;
|
|
@@ -341,41 +370,49 @@ export component TableHead(children: React.Node, column?: string | null = null,
|
|
|
341
370
|
});
|
|
342
371
|
|
|
343
372
|
if (column == null) {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
373
|
+
const props = withProps(rest, { children, scope: "col" });
|
|
374
|
+
if (render != null) {
|
|
375
|
+
return render(withProps(props, { role: "columnheader" }));
|
|
376
|
+
}
|
|
377
|
+
return <th {...props} />;
|
|
349
378
|
}
|
|
350
379
|
|
|
351
|
-
|
|
352
|
-
<
|
|
353
|
-
{
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
380
|
+
const button = (
|
|
381
|
+
<button
|
|
382
|
+
onClick={composeHandlers(rest.onClick, () => {
|
|
383
|
+
// Two states, not three. A sort that cycles back to "unsorted" gives
|
|
384
|
+
// a reader a third press whose result is a table in an order nobody
|
|
385
|
+
// asked for.
|
|
386
|
+
table.setSort({
|
|
387
|
+
column,
|
|
388
|
+
direction: sorted && table.sort?.direction === "ascending" ? "descending" : "ascending",
|
|
389
|
+
});
|
|
390
|
+
})}
|
|
391
|
+
type="button"
|
|
357
392
|
>
|
|
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>
|
|
393
|
+
{children}
|
|
394
|
+
</button>
|
|
373
395
|
);
|
|
396
|
+
const props = withProps(passed, {
|
|
397
|
+
"aria-sort": sorted ? table.sort?.direction : undefined,
|
|
398
|
+
children: button,
|
|
399
|
+
ref: composeRefs(rest.ref, setElement),
|
|
400
|
+
scope: "col",
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
if (render != null) {
|
|
404
|
+
return render(withProps(props, { role: "columnheader" }));
|
|
405
|
+
}
|
|
406
|
+
return <th {...props} />;
|
|
374
407
|
}
|
|
375
408
|
|
|
376
409
|
/** One cell. */
|
|
377
|
-
export component TableCell(children: React.Node, ...rest: Rest) {
|
|
378
|
-
|
|
410
|
+
export component TableCell(children: React.Node, render?: RenderProp, ...rest: Rest) {
|
|
411
|
+
const props = withProps(rest, { children });
|
|
412
|
+
if (render != null) {
|
|
413
|
+
return render(withProps(props, { role: "cell" }));
|
|
414
|
+
}
|
|
415
|
+
return <td {...props} />;
|
|
379
416
|
}
|
|
380
417
|
|
|
381
418
|
/**
|
|
@@ -386,12 +423,12 @@ export component TableCell(children: React.Node, ...rest: Rest) {
|
|
|
386
423
|
* moves down the year column. A table of records usually has one and almost
|
|
387
424
|
* never marks it.
|
|
388
425
|
*/
|
|
389
|
-
export component TableRowHeader(children: React.Node, ...rest: Rest) {
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
426
|
+
export component TableRowHeader(children: React.Node, render?: RenderProp, ...rest: Rest) {
|
|
427
|
+
const props = withProps(rest, { children, scope: "row" });
|
|
428
|
+
if (render != null) {
|
|
429
|
+
return render(withProps(props, { role: "rowheader" }));
|
|
430
|
+
}
|
|
431
|
+
return <th {...props} />;
|
|
395
432
|
}
|
|
396
433
|
|
|
397
434
|
/**
|
|
@@ -430,6 +467,7 @@ export component TableSelectAll(
|
|
|
430
467
|
label?: string = "Select all rows",
|
|
431
468
|
className?: string,
|
|
432
469
|
disabled?: boolean = false,
|
|
470
|
+
render?: RenderProp,
|
|
433
471
|
) {
|
|
434
472
|
return (
|
|
435
473
|
<Checkbox
|
|
@@ -439,6 +477,7 @@ export component TableSelectAll(
|
|
|
439
477
|
disabled={disabled}
|
|
440
478
|
indeterminate={checked === "mixed"}
|
|
441
479
|
onCheckedChange={onCheckedChange}
|
|
480
|
+
render={render}
|
|
442
481
|
/>
|
|
443
482
|
);
|
|
444
483
|
}
|
|
@@ -461,6 +500,7 @@ export component TableRowSelect(
|
|
|
461
500
|
onCheckedChange: (checked: boolean) => void,
|
|
462
501
|
className?: string,
|
|
463
502
|
disabled?: boolean = false,
|
|
503
|
+
render?: RenderProp,
|
|
464
504
|
) {
|
|
465
505
|
return (
|
|
466
506
|
<Checkbox
|
|
@@ -469,6 +509,7 @@ export component TableRowSelect(
|
|
|
469
509
|
className={className}
|
|
470
510
|
disabled={disabled}
|
|
471
511
|
onCheckedChange={onCheckedChange}
|
|
512
|
+
render={render}
|
|
472
513
|
/>
|
|
473
514
|
);
|
|
474
515
|
}
|