@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/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,33 @@ 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
|
-
</div>
|
|
270
|
-
);
|
|
248
|
+
const props = withProps(passed, {
|
|
249
|
+
children,
|
|
250
|
+
onPointerDown: composeHandlers(rest.onPointerDown, (event: $FlowFixMe) => {
|
|
251
|
+
if (slider.disabled) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
// Otherwise the press selects the page's text on the way past, which
|
|
255
|
+
// makes a drag paint everything blue.
|
|
256
|
+
event.preventDefault();
|
|
257
|
+
event.currentTarget?.setPointerCapture?.(event.pointerId);
|
|
258
|
+
moveTo(event, null);
|
|
259
|
+
}),
|
|
260
|
+
onPointerMove: composeHandlers(rest.onPointerMove, (event: $FlowFixMe) => {
|
|
261
|
+
if (dragging.current != null) {
|
|
262
|
+
moveTo(event, dragging.current);
|
|
263
|
+
}
|
|
264
|
+
}),
|
|
265
|
+
onPointerUp: composeHandlers(rest.onPointerUp, (event: $FlowFixMe) => {
|
|
266
|
+
dragging.current = null;
|
|
267
|
+
event.currentTarget?.releasePointerCapture?.(event.pointerId);
|
|
268
|
+
}),
|
|
269
|
+
ref: composeRefs(rest.ref, (element: HTMLElement | null) => {
|
|
270
|
+
slider.trackRef.current = element;
|
|
271
|
+
}),
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
return render == null ? <div {...props} /> : render(props);
|
|
271
275
|
}
|
|
272
276
|
|
|
273
277
|
/**
|
|
@@ -281,24 +285,22 @@ export component SliderTrack(children: React.Node, ...rest: Rest) {
|
|
|
281
285
|
* is told the value by the thumb and telling them again here would be telling
|
|
282
286
|
* them twice.
|
|
283
287
|
*/
|
|
284
|
-
export component SliderRange(...rest: Rest) {
|
|
288
|
+
export component SliderRange(render?: RenderProp, ...rest: Rest) {
|
|
285
289
|
const slider = useSlider("Slider.Range");
|
|
286
290
|
const passed = withoutComposed(rest, ["style"]);
|
|
287
291
|
const ends = [...slider.values].sort((first, second) => first - second);
|
|
288
292
|
const start = slider.values.length > 1 ? (ends[0] ?? slider.min) : slider.min;
|
|
289
293
|
const end = ends[ends.length - 1] ?? slider.min;
|
|
294
|
+
const props = withProps(passed, {
|
|
295
|
+
"aria-hidden": "true",
|
|
296
|
+
style: {
|
|
297
|
+
...(rest.style as $FlowFixMe),
|
|
298
|
+
"--uf-slider-start": fraction(start, slider.min, slider.max),
|
|
299
|
+
"--uf-slider-end": fraction(end, slider.min, slider.max),
|
|
300
|
+
},
|
|
301
|
+
});
|
|
290
302
|
|
|
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
|
-
);
|
|
303
|
+
return render == null ? <div {...props} /> : render(props);
|
|
302
304
|
}
|
|
303
305
|
|
|
304
306
|
/**
|
|
@@ -315,55 +317,53 @@ export component SliderRange(...rest: Rest) {
|
|
|
315
317
|
* "Maximum" told apart is the whole difference between a control a reader can
|
|
316
318
|
* operate and two identical "slider"s.
|
|
317
319
|
*/
|
|
318
|
-
export component SliderThumb(index?: number = 0, ...rest: Rest) {
|
|
320
|
+
export component SliderThumb(index?: number = 0, render?: RenderProp, ...rest: Rest) {
|
|
319
321
|
const slider = useSlider("Slider.Thumb");
|
|
320
322
|
const passed = withoutComposed(rest, ["onKeyDown", "style"]);
|
|
321
323
|
const value = slider.values[index] ?? slider.min;
|
|
322
324
|
const [lower, upper] = boundsOf(slider.values, index, slider.min, slider.max);
|
|
325
|
+
const props = withProps(passed, {
|
|
326
|
+
"aria-disabled": slider.disabled ? "true" : undefined,
|
|
327
|
+
"aria-orientation": slider.orientation,
|
|
328
|
+
// The neighbour's value, not the slider's end. A reader told they may
|
|
329
|
+
// set this thumb to 90 while the control refuses at 60 has been told
|
|
330
|
+
// something the control disagrees with.
|
|
331
|
+
"aria-valuemax": upper,
|
|
332
|
+
"aria-valuemin": lower,
|
|
333
|
+
"aria-valuenow": value,
|
|
334
|
+
"aria-valuetext": slider.valueText?.(value, index),
|
|
335
|
+
onKeyDown: composeHandlers(rest.onKeyDown, (event: $FlowFixMe) => {
|
|
336
|
+
if (slider.disabled) {
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
const reversed = isReversed(event.currentTarget, slider.orientation);
|
|
340
|
+
const move = stepFor(event.key, slider.step, slider.largeStep, reversed);
|
|
341
|
+
if (move != null) {
|
|
342
|
+
// Before moving: the arrow keys scroll the page, and a slider that
|
|
343
|
+
// moves the page under the reader as it moves the value is a control
|
|
344
|
+
// they cannot watch.
|
|
345
|
+
event.preventDefault();
|
|
346
|
+
slider.setAt(index, value + move);
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
if (event.key === "Home" || event.key === "End") {
|
|
350
|
+
event.preventDefault();
|
|
351
|
+
// This thumb's own ends, which for the lower thumb of a range is its
|
|
352
|
+
// neighbour rather than the slider's maximum.
|
|
353
|
+
slider.setAt(index, event.key === "Home" ? lower : upper);
|
|
354
|
+
}
|
|
355
|
+
}),
|
|
356
|
+
role: "slider",
|
|
357
|
+
style: {
|
|
358
|
+
...(rest.style as $FlowFixMe),
|
|
359
|
+
"--uf-slider-fraction": fraction(value, slider.min, slider.max),
|
|
360
|
+
},
|
|
361
|
+
// In the tab sequence, and out of it while disabled — the browser does
|
|
362
|
+
// this for a real control and there is no real control here to do it.
|
|
363
|
+
tabIndex: slider.disabled ? -1 : 0,
|
|
364
|
+
});
|
|
323
365
|
|
|
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
|
-
);
|
|
366
|
+
return render == null ? <span {...props} /> : render(props);
|
|
367
367
|
}
|
|
368
368
|
|
|
369
369
|
/**
|
package/switch.js
CHANGED
|
@@ -29,47 +29,53 @@
|
|
|
29
29
|
|
|
30
30
|
import * as React from "@uniflowed/react";
|
|
31
31
|
|
|
32
|
-
import type { Rest } from "./internal/merge-props.js";
|
|
33
|
-
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";
|
|
34
34
|
import { useControlled } from "./internal/controlled-state.js";
|
|
35
35
|
|
|
36
|
-
/**
|
|
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
|
+
*/
|
|
37
44
|
export component Switch(
|
|
38
45
|
checked?: boolean,
|
|
39
46
|
defaultChecked?: boolean = false,
|
|
40
47
|
onCheckedChange?: (checked: boolean) => void,
|
|
41
48
|
disabled?: boolean = false,
|
|
42
49
|
children?: React.Node,
|
|
50
|
+
render?: RenderProp,
|
|
43
51
|
...rest: Rest
|
|
44
52
|
) {
|
|
45
53
|
const [on, setOn] = useControlled(checked, defaultChecked, onCheckedChange);
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
disabled={disabled}
|
|
53
|
-
onClick={composeHandlers(rest.onClick, () => {
|
|
54
|
-
if (!disabled) {
|
|
55
|
-
setOn(!on);
|
|
56
|
-
}
|
|
57
|
-
})}
|
|
58
|
-
onKeyDown={composeHandlers(rest.onKeyDown, (event) => {
|
|
59
|
-
if (disabled || (event.key !== " " && event.key !== "Enter")) {
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
// Preventing the default is not decoration. It stops `Space` scrolling
|
|
63
|
-
// the page — which is what makes a hand-written toggle feel broken even
|
|
64
|
-
// when it works — and it stops the browser's own click from arriving
|
|
65
|
-
// after this handler and toggling the switch a second time.
|
|
66
|
-
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) {
|
|
67
60
|
setOn(!on);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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" />;
|
|
75
81
|
}
|
package/table.js
CHANGED
|
@@ -68,8 +68,13 @@ import { createContext, useContext, useEffect, useId, useMemo, useState } from "
|
|
|
68
68
|
import { useStableCallback } from "@uniflowed/hooks/lifecycle";
|
|
69
69
|
|
|
70
70
|
import { Checkbox } from "./checkbox.js";
|
|
71
|
-
import type { Rest } from "./internal/merge-props.js";
|
|
72
|
-
import {
|
|
71
|
+
import type { RenderProp, Rest } from "./internal/merge-props.js";
|
|
72
|
+
import {
|
|
73
|
+
composeHandlers,
|
|
74
|
+
composeRefs,
|
|
75
|
+
withProps,
|
|
76
|
+
withoutComposed,
|
|
77
|
+
} from "./internal/merge-props.js";
|
|
73
78
|
import { useControlled } from "./internal/controlled-state.js";
|
|
74
79
|
|
|
75
80
|
/** Which column a table is sorted by, and which way. */
|
|
@@ -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,23 +190,15 @@ 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
|
|
@@ -221,7 +219,7 @@ export component TableRoot(
|
|
|
221
219
|
* what a screen reader reads when a reader lands on it. A heading above the
|
|
222
220
|
* table looks the same and is not the table's name.
|
|
223
221
|
*/
|
|
224
|
-
export component TableCaption(children: React.Node, ...rest: Rest) {
|
|
222
|
+
export component TableCaption(children: React.Node, render?: RenderProp, ...rest: Rest) {
|
|
225
223
|
const table = useTable("Table.Caption");
|
|
226
224
|
const register = table.registerCaption;
|
|
227
225
|
|
|
@@ -230,11 +228,11 @@ export component TableCaption(children: React.Node, ...rest: Rest) {
|
|
|
230
228
|
return () => register(false);
|
|
231
229
|
}, [register]);
|
|
232
230
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
231
|
+
const props = withProps(rest, { children, id: table.captionId });
|
|
232
|
+
if (render != null) {
|
|
233
|
+
return render(withProps(props, { role: "caption" }));
|
|
234
|
+
}
|
|
235
|
+
return <caption {...props} />;
|
|
238
236
|
}
|
|
239
237
|
|
|
240
238
|
/**
|
|
@@ -243,7 +241,7 @@ export component TableCaption(children: React.Node, ...rest: Rest) {
|
|
|
243
241
|
* It tells the root that it exists, because `aria-rowcount` and every row's
|
|
244
242
|
* `aria-rowindex` count header rows and a table without one counts differently.
|
|
245
243
|
*/
|
|
246
|
-
export component TableHeader(children: React.Node, ...rest: Rest) {
|
|
244
|
+
export component TableHeader(children: React.Node, render?: RenderProp, ...rest: Rest) {
|
|
247
245
|
const table = useTable("Table.Header");
|
|
248
246
|
const register = table.registerHeader;
|
|
249
247
|
|
|
@@ -252,18 +250,21 @@ export component TableHeader(children: React.Node, ...rest: Rest) {
|
|
|
252
250
|
return () => register(false);
|
|
253
251
|
}, [register]);
|
|
254
252
|
|
|
253
|
+
const props = withProps(rest, { children });
|
|
254
|
+
|
|
255
255
|
return (
|
|
256
256
|
<HeaderContext.Provider value={true}>
|
|
257
|
-
<thead {...
|
|
257
|
+
{render == null ? <thead {...props} /> : render(withProps(props, { role: "rowgroup" }))}
|
|
258
258
|
</HeaderContext.Provider>
|
|
259
259
|
);
|
|
260
260
|
}
|
|
261
261
|
|
|
262
262
|
/** The data rows. */
|
|
263
|
-
export component TableBody(children: React.Node, ...rest: Rest) {
|
|
263
|
+
export component TableBody(children: React.Node, render?: RenderProp, ...rest: Rest) {
|
|
264
|
+
const props = withProps(rest, { children });
|
|
264
265
|
return (
|
|
265
266
|
<HeaderContext.Provider value={false}>
|
|
266
|
-
<tbody {...
|
|
267
|
+
{render == null ? <tbody {...props} /> : render(withProps(props, { role: "rowgroup" }))}
|
|
267
268
|
</HeaderContext.Provider>
|
|
268
269
|
);
|
|
269
270
|
}
|
|
@@ -284,7 +285,12 @@ export component TableBody(children: React.Node, ...rest: Rest) {
|
|
|
284
285
|
* adding them anyway is a second source of truth that can disagree with the
|
|
285
286
|
* document.
|
|
286
287
|
*/
|
|
287
|
-
export component TableRow(
|
|
288
|
+
export component TableRow(
|
|
289
|
+
children: React.Node,
|
|
290
|
+
index?: number | null = null,
|
|
291
|
+
render?: RenderProp,
|
|
292
|
+
...rest: Rest
|
|
293
|
+
) {
|
|
288
294
|
const table = useTable("Table.Row");
|
|
289
295
|
const header = useContext(HeaderContext);
|
|
290
296
|
const counted = table.rowCount != null;
|
|
@@ -300,11 +306,11 @@ export component TableRow(children: React.Node, index?: number | null = null, ..
|
|
|
300
306
|
rowIndex = table.headerRows + table.rowOffset + (index ?? 0) + 1;
|
|
301
307
|
}
|
|
302
308
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
309
|
+
const props = withProps(rest, { "aria-rowindex": rowIndex, children });
|
|
310
|
+
if (render != null) {
|
|
311
|
+
return render(withProps(props, { role: "row" }));
|
|
312
|
+
}
|
|
313
|
+
return <tr {...props} />;
|
|
308
314
|
}
|
|
309
315
|
|
|
310
316
|
/**
|
|
@@ -320,7 +326,12 @@ export component TableRow(children: React.Node, index?: number | null = null, ..
|
|
|
320
326
|
* Not `"none"` on the others: eleven headers each announcing "not sorted" is
|
|
321
327
|
* eleven announcements of nothing, on every pass through the table.
|
|
322
328
|
*/
|
|
323
|
-
export component TableHead(
|
|
329
|
+
export component TableHead(
|
|
330
|
+
children: React.Node,
|
|
331
|
+
column?: string | null = null,
|
|
332
|
+
render?: RenderProp,
|
|
333
|
+
...rest: Rest
|
|
334
|
+
) {
|
|
324
335
|
const table = useTable("Table.Head");
|
|
325
336
|
const passed = withoutComposed(rest, column == null ? [] : ["onClick", "ref"]);
|
|
326
337
|
const sorted = column != null && table.sort?.column === column;
|
|
@@ -341,41 +352,49 @@ export component TableHead(children: React.Node, column?: string | null = null,
|
|
|
341
352
|
});
|
|
342
353
|
|
|
343
354
|
if (column == null) {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
355
|
+
const props = withProps(rest, { children, scope: "col" });
|
|
356
|
+
if (render != null) {
|
|
357
|
+
return render(withProps(props, { role: "columnheader" }));
|
|
358
|
+
}
|
|
359
|
+
return <th {...props} />;
|
|
349
360
|
}
|
|
350
361
|
|
|
351
|
-
|
|
352
|
-
<
|
|
353
|
-
{
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
362
|
+
const button = (
|
|
363
|
+
<button
|
|
364
|
+
onClick={composeHandlers(rest.onClick, () => {
|
|
365
|
+
// Two states, not three. A sort that cycles back to "unsorted" gives
|
|
366
|
+
// a reader a third press whose result is a table in an order nobody
|
|
367
|
+
// asked for.
|
|
368
|
+
table.setSort({
|
|
369
|
+
column,
|
|
370
|
+
direction: sorted && table.sort?.direction === "ascending" ? "descending" : "ascending",
|
|
371
|
+
});
|
|
372
|
+
})}
|
|
373
|
+
type="button"
|
|
357
374
|
>
|
|
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>
|
|
375
|
+
{children}
|
|
376
|
+
</button>
|
|
373
377
|
);
|
|
378
|
+
const props = withProps(passed, {
|
|
379
|
+
"aria-sort": sorted ? table.sort?.direction : undefined,
|
|
380
|
+
children: button,
|
|
381
|
+
ref: composeRefs(rest.ref, setElement),
|
|
382
|
+
scope: "col",
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
if (render != null) {
|
|
386
|
+
return render(withProps(props, { role: "columnheader" }));
|
|
387
|
+
}
|
|
388
|
+
return <th {...props} />;
|
|
374
389
|
}
|
|
375
390
|
|
|
376
391
|
/** One cell. */
|
|
377
|
-
export component TableCell(children: React.Node, ...rest: Rest) {
|
|
378
|
-
|
|
392
|
+
export component TableCell(children: React.Node, render?: RenderProp, ...rest: Rest) {
|
|
393
|
+
const props = withProps(rest, { children });
|
|
394
|
+
if (render != null) {
|
|
395
|
+
return render(withProps(props, { role: "cell" }));
|
|
396
|
+
}
|
|
397
|
+
return <td {...props} />;
|
|
379
398
|
}
|
|
380
399
|
|
|
381
400
|
/**
|
|
@@ -386,12 +405,12 @@ export component TableCell(children: React.Node, ...rest: Rest) {
|
|
|
386
405
|
* moves down the year column. A table of records usually has one and almost
|
|
387
406
|
* never marks it.
|
|
388
407
|
*/
|
|
389
|
-
export component TableRowHeader(children: React.Node, ...rest: Rest) {
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
408
|
+
export component TableRowHeader(children: React.Node, render?: RenderProp, ...rest: Rest) {
|
|
409
|
+
const props = withProps(rest, { children, scope: "row" });
|
|
410
|
+
if (render != null) {
|
|
411
|
+
return render(withProps(props, { role: "rowheader" }));
|
|
412
|
+
}
|
|
413
|
+
return <th {...props} />;
|
|
395
414
|
}
|
|
396
415
|
|
|
397
416
|
/**
|
|
@@ -430,6 +449,7 @@ export component TableSelectAll(
|
|
|
430
449
|
label?: string = "Select all rows",
|
|
431
450
|
className?: string,
|
|
432
451
|
disabled?: boolean = false,
|
|
452
|
+
render?: RenderProp,
|
|
433
453
|
) {
|
|
434
454
|
return (
|
|
435
455
|
<Checkbox
|
|
@@ -439,6 +459,7 @@ export component TableSelectAll(
|
|
|
439
459
|
disabled={disabled}
|
|
440
460
|
indeterminate={checked === "mixed"}
|
|
441
461
|
onCheckedChange={onCheckedChange}
|
|
462
|
+
render={render}
|
|
442
463
|
/>
|
|
443
464
|
);
|
|
444
465
|
}
|
|
@@ -461,6 +482,7 @@ export component TableRowSelect(
|
|
|
461
482
|
onCheckedChange: (checked: boolean) => void,
|
|
462
483
|
className?: string,
|
|
463
484
|
disabled?: boolean = false,
|
|
485
|
+
render?: RenderProp,
|
|
464
486
|
) {
|
|
465
487
|
return (
|
|
466
488
|
<Checkbox
|
|
@@ -469,6 +491,7 @@ export component TableRowSelect(
|
|
|
469
491
|
className={className}
|
|
470
492
|
disabled={disabled}
|
|
471
493
|
onCheckedChange={onCheckedChange}
|
|
494
|
+
render={render}
|
|
472
495
|
/>
|
|
473
496
|
);
|
|
474
497
|
}
|