@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/drawer.js
CHANGED
|
@@ -53,11 +53,12 @@ import { createContext, useContext, useEffect, useMemo, useRef, useState } from
|
|
|
53
53
|
import { usePrefersReducedMotion } from "@uniflowed/hooks/browser";
|
|
54
54
|
|
|
55
55
|
import type { Edge } from "./sheet.js";
|
|
56
|
-
import type { Rest } from "./internal/merge-props.js";
|
|
56
|
+
import type { RenderProp, Rest } from "./internal/merge-props.js";
|
|
57
57
|
import {
|
|
58
58
|
composeHandlers,
|
|
59
59
|
composeRefs,
|
|
60
60
|
forwarded,
|
|
61
|
+
withProps,
|
|
61
62
|
withoutComposed,
|
|
62
63
|
} from "./internal/merge-props.js";
|
|
63
64
|
import {
|
|
@@ -172,13 +173,17 @@ export component DrawerRoot(
|
|
|
172
173
|
}
|
|
173
174
|
|
|
174
175
|
/** What opens it, and what focus comes back to when it closes. */
|
|
175
|
-
export component DrawerTrigger(children: React.Node, ...rest: Rest) {
|
|
176
|
-
return
|
|
176
|
+
export component DrawerTrigger(children: React.Node, render?: RenderProp, ...rest: Rest) {
|
|
177
|
+
return (
|
|
178
|
+
<SheetTrigger {...forwarded(rest)} render={render}>
|
|
179
|
+
{children}
|
|
180
|
+
</SheetTrigger>
|
|
181
|
+
);
|
|
177
182
|
}
|
|
178
183
|
|
|
179
184
|
/** The backdrop. It carries the edge, the same as a sheet's. */
|
|
180
|
-
export component DrawerOverlay(...rest: Rest) {
|
|
181
|
-
return <SheetOverlay {...forwarded(rest)} />;
|
|
185
|
+
export component DrawerOverlay(render?: RenderProp, ...rest: Rest) {
|
|
186
|
+
return <SheetOverlay {...forwarded(rest)} render={render} />;
|
|
182
187
|
}
|
|
183
188
|
|
|
184
189
|
/**
|
|
@@ -189,7 +194,7 @@ export component DrawerOverlay(...rest: Rest) {
|
|
|
189
194
|
* no drag to provide an alternative to — and a drawer with a handle and no
|
|
190
195
|
* `Drawer.Close` has a gesture that is the only way out.
|
|
191
196
|
*/
|
|
192
|
-
export component DrawerBody(children: React.Node, ...rest: Rest) {
|
|
197
|
+
export component DrawerBody(children: React.Node, render?: RenderProp, ...rest: Rest) {
|
|
193
198
|
const drawer = useDrawer("Drawer.Body");
|
|
194
199
|
const { bodyRef, snapIndex, snapPoints } = drawer;
|
|
195
200
|
const reducedMotion = usePrefersReducedMotion();
|
|
@@ -215,6 +220,7 @@ export component DrawerBody(children: React.Node, ...rest: Rest) {
|
|
|
215
220
|
ref={composeRefs(rest.ref, (element: HTMLElement | null) => {
|
|
216
221
|
bodyRef.current = element;
|
|
217
222
|
})}
|
|
223
|
+
render={render}
|
|
218
224
|
>
|
|
219
225
|
{children}
|
|
220
226
|
<RequireCloseForTheDrag />
|
|
@@ -254,23 +260,39 @@ component RequireCloseForTheDrag() {
|
|
|
254
260
|
}
|
|
255
261
|
|
|
256
262
|
/** The top of the drawer, where the handle usually goes. */
|
|
257
|
-
export component DrawerHeader(children: React.Node, ...rest: Rest) {
|
|
258
|
-
return
|
|
263
|
+
export component DrawerHeader(children: React.Node, render?: RenderProp, ...rest: Rest) {
|
|
264
|
+
return (
|
|
265
|
+
<SheetHeader {...forwarded(rest)} render={render}>
|
|
266
|
+
{children}
|
|
267
|
+
</SheetHeader>
|
|
268
|
+
);
|
|
259
269
|
}
|
|
260
270
|
|
|
261
271
|
/** The bottom of the drawer, where the actions go. */
|
|
262
|
-
export component DrawerFooter(children: React.Node, ...rest: Rest) {
|
|
263
|
-
return
|
|
272
|
+
export component DrawerFooter(children: React.Node, render?: RenderProp, ...rest: Rest) {
|
|
273
|
+
return (
|
|
274
|
+
<SheetFooter {...forwarded(rest)} render={render}>
|
|
275
|
+
{children}
|
|
276
|
+
</SheetFooter>
|
|
277
|
+
);
|
|
264
278
|
}
|
|
265
279
|
|
|
266
280
|
/** The drawer's accessible name. */
|
|
267
|
-
export component DrawerTitle(children: React.Node, ...rest: Rest) {
|
|
268
|
-
return
|
|
281
|
+
export component DrawerTitle(children: React.Node, render?: RenderProp, ...rest: Rest) {
|
|
282
|
+
return (
|
|
283
|
+
<SheetTitle {...forwarded(rest)} render={render}>
|
|
284
|
+
{children}
|
|
285
|
+
</SheetTitle>
|
|
286
|
+
);
|
|
269
287
|
}
|
|
270
288
|
|
|
271
289
|
/** What the drawer is for, announced after its name. */
|
|
272
|
-
export component DrawerDescription(children: React.Node, ...rest: Rest) {
|
|
273
|
-
return
|
|
290
|
+
export component DrawerDescription(children: React.Node, render?: RenderProp, ...rest: Rest) {
|
|
291
|
+
return (
|
|
292
|
+
<SheetDescription {...forwarded(rest)} render={render}>
|
|
293
|
+
{children}
|
|
294
|
+
</SheetDescription>
|
|
295
|
+
);
|
|
274
296
|
}
|
|
275
297
|
|
|
276
298
|
/**
|
|
@@ -279,7 +301,7 @@ export component DrawerDescription(children: React.Node, ...rest: Rest) {
|
|
|
279
301
|
*
|
|
280
302
|
* It registers itself so `Drawer.Body` can tell whether the gesture has one.
|
|
281
303
|
*/
|
|
282
|
-
export component DrawerClose(children: React.Node, ...rest: Rest) {
|
|
304
|
+
export component DrawerClose(children: React.Node, render?: RenderProp, ...rest: Rest) {
|
|
283
305
|
const drawer = useDrawer("Drawer.Close");
|
|
284
306
|
const closes = drawer.closes;
|
|
285
307
|
|
|
@@ -290,7 +312,11 @@ export component DrawerClose(children: React.Node, ...rest: Rest) {
|
|
|
290
312
|
};
|
|
291
313
|
}, [closes]);
|
|
292
314
|
|
|
293
|
-
return
|
|
315
|
+
return (
|
|
316
|
+
<SheetClose {...forwarded(rest)} render={render}>
|
|
317
|
+
{children}
|
|
318
|
+
</SheetClose>
|
|
319
|
+
);
|
|
294
320
|
}
|
|
295
321
|
|
|
296
322
|
/**
|
|
@@ -305,7 +331,11 @@ export component DrawerClose(children: React.Node, ...rest: Rest) {
|
|
|
305
331
|
* `label` because a slider with no accessible name is announced as "slider",
|
|
306
332
|
* which is the same failure `Resizable.Handle` names.
|
|
307
333
|
*/
|
|
308
|
-
export component DrawerHandle(
|
|
334
|
+
export component DrawerHandle(
|
|
335
|
+
label?: string = "Resize the drawer",
|
|
336
|
+
render?: RenderProp,
|
|
337
|
+
...rest: Rest
|
|
338
|
+
) {
|
|
309
339
|
const drawer = useDrawer("Drawer.Handle");
|
|
310
340
|
const { bodyRef, close, handles, setSnapIndex, side, snapIndex, snapPoints } = drawer;
|
|
311
341
|
const passed = withoutComposed(rest, [
|
|
@@ -360,78 +390,80 @@ export component DrawerHandle(label?: string = "Resize the drawer", ...rest: Res
|
|
|
360
390
|
return side === "top" || side === "left" ? from - now : now - from;
|
|
361
391
|
};
|
|
362
392
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
393
|
+
const props = withProps(passed, {
|
|
394
|
+
"aria-label": label,
|
|
395
|
+
// The axis the drag runs along, which is the axis the snap points are
|
|
396
|
+
// measured on: a bottom sheet grows upwards, so its slider is vertical.
|
|
397
|
+
"aria-orientation": vertical ? "vertical" : "horizontal",
|
|
398
|
+
"aria-valuemax": last,
|
|
399
|
+
"aria-valuemin": 0,
|
|
400
|
+
"aria-valuenow": snapIndex,
|
|
401
|
+
// The number a reader can act on. `aria-valuenow` is an index into a list
|
|
402
|
+
// nobody outside this component has seen, and "2" says nothing.
|
|
403
|
+
"aria-valuetext": `${String(Math.round((snapPoints[snapIndex] ?? 1) * 100))}%`,
|
|
404
|
+
"data-dragging": dragging ? "true" : undefined,
|
|
405
|
+
onKeyDown: composeHandlers(rest.onKeyDown, (event: $FlowFixMe) => {
|
|
406
|
+
if (event.key === "Home" || event.key === "End") {
|
|
407
|
+
event.preventDefault();
|
|
408
|
+
setSnapIndex(event.key === "Home" ? 0 : last);
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
const towardsOpen = OPENS_WITH[side];
|
|
412
|
+
const towardsClosed = CLOSES_WITH[side];
|
|
413
|
+
if (event.key === towardsOpen) {
|
|
414
|
+
event.preventDefault();
|
|
415
|
+
step(true);
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
if (event.key === towardsClosed) {
|
|
419
|
+
event.preventDefault();
|
|
420
|
+
step(false);
|
|
421
|
+
}
|
|
422
|
+
}),
|
|
423
|
+
onPointerDown: composeHandlers(rest.onPointerDown, (event: $FlowFixMe) => {
|
|
424
|
+
dragFrom.current = vertical ? event.clientY : event.clientX;
|
|
425
|
+
setDragging(true);
|
|
426
|
+
// So the drag survives the pointer leaving the handle, which it does
|
|
427
|
+
// immediately: the handle moves with the drawer.
|
|
428
|
+
event.currentTarget?.setPointerCapture?.(event.pointerId);
|
|
429
|
+
}),
|
|
430
|
+
onPointerMove: composeHandlers(rest.onPointerMove, (event: $FlowFixMe) => {
|
|
431
|
+
const body = bodyRef.current;
|
|
432
|
+
if (dragFrom.current == null || body == null) {
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
// Only away from the edge: dragging a drawer *past* fully open would
|
|
436
|
+
// otherwise lift it off the edge it is attached to.
|
|
437
|
+
body.style.setProperty("--uf-drawer-drag", `${String(Math.max(0, travelled(event)))}px`);
|
|
438
|
+
}),
|
|
439
|
+
onPointerUp: composeHandlers(rest.onPointerUp, (event: $FlowFixMe) => {
|
|
440
|
+
const body = bodyRef.current;
|
|
441
|
+
const moved = travelled(event);
|
|
442
|
+
dragFrom.current = null;
|
|
443
|
+
setDragging(false);
|
|
444
|
+
body?.style.setProperty("--uf-drawer-drag", "0px");
|
|
445
|
+
if (body == null) {
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
const box = body.getBoundingClientRect();
|
|
449
|
+
const size = vertical ? box.height : box.width;
|
|
450
|
+
// A zero-sized box — a document that computes no layout — must not turn
|
|
451
|
+
// every release into a dismissal.
|
|
452
|
+
if (size <= 0 || Math.abs(moved) < size * DRAG_THRESHOLD) {
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
step(moved < 0);
|
|
456
|
+
}),
|
|
457
|
+
role: "slider",
|
|
458
|
+
// A drag handle that is not in the tab sequence is the WCAG 2.1.1
|
|
459
|
+
// failure this part exists to avoid.
|
|
460
|
+
tabIndex: 0,
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
if (render != null) {
|
|
464
|
+
return render(props);
|
|
465
|
+
}
|
|
466
|
+
return <div {...props} />;
|
|
435
467
|
}
|
|
436
468
|
|
|
437
469
|
/**
|
package/field.js
CHANGED
|
@@ -96,7 +96,7 @@
|
|
|
96
96
|
import * as React from "@uniflowed/react";
|
|
97
97
|
import { createContext, useContext, useEffect, useId, useMemo, useState } from "@uniflowed/react";
|
|
98
98
|
|
|
99
|
-
import type { Rest } from "./internal/merge-props.js";
|
|
99
|
+
import type { RenderProp, Rest } from "./internal/merge-props.js";
|
|
100
100
|
import { withProps } from "./internal/merge-props.js";
|
|
101
101
|
|
|
102
102
|
/**
|
|
@@ -129,6 +129,7 @@ export type FieldSource = {|
|
|
|
129
129
|
readonly invalid: boolean,
|
|
130
130
|
readonly required: boolean,
|
|
131
131
|
readonly disabled: boolean,
|
|
132
|
+
readonly busy: boolean,
|
|
132
133
|
/** What is wrong, or null while the field is valid. */
|
|
133
134
|
readonly message: string | null,
|
|
134
135
|
readonly control: Rest,
|
|
@@ -141,14 +142,17 @@ type FieldState = {|
|
|
|
141
142
|
readonly controlId: string,
|
|
142
143
|
readonly labelId: string,
|
|
143
144
|
readonly descriptionId: string,
|
|
145
|
+
readonly statusId: string,
|
|
144
146
|
readonly errorId: string,
|
|
145
147
|
readonly invalid: boolean,
|
|
146
148
|
readonly required: boolean,
|
|
149
|
+
readonly busy: boolean,
|
|
147
150
|
readonly group: boolean,
|
|
148
151
|
readonly message: string | null,
|
|
149
152
|
readonly control: Rest,
|
|
150
153
|
readonly describedBy: string | void,
|
|
151
154
|
readonly registerDescription: (present: boolean) => void,
|
|
155
|
+
readonly registerStatus: (present: boolean) => void,
|
|
152
156
|
readonly registerError: (present: boolean) => void,
|
|
153
157
|
|};
|
|
154
158
|
|
|
@@ -187,26 +191,32 @@ export component FieldRoot(
|
|
|
187
191
|
children: React.Node,
|
|
188
192
|
invalid?: boolean = false,
|
|
189
193
|
required?: boolean = false,
|
|
194
|
+
busy?: boolean = false,
|
|
190
195
|
field?: FieldSource,
|
|
191
196
|
group?: boolean = false,
|
|
197
|
+
render?: RenderProp,
|
|
192
198
|
...rest: Rest
|
|
193
199
|
) {
|
|
194
200
|
const base = useId();
|
|
195
201
|
const [hasDescription, setHasDescription] = useState(false);
|
|
202
|
+
const [hasStatus, setHasStatus] = useState(false);
|
|
196
203
|
const [hasError, setHasError] = useState(false);
|
|
197
204
|
const sourceInvalid = field?.invalid ?? false;
|
|
198
205
|
const sourceRequired = field?.required ?? false;
|
|
206
|
+
const sourceBusy = field?.busy ?? false;
|
|
199
207
|
const message = field?.message ?? null;
|
|
200
208
|
const control = field?.control ?? NO_CONTROL;
|
|
201
209
|
|
|
202
210
|
const state = useMemo(() => {
|
|
203
211
|
const descriptionId = `${base}-description`;
|
|
212
|
+
const statusId = `${base}-status`;
|
|
204
213
|
const errorId = `${base}-error`;
|
|
205
214
|
const wrong = invalid || sourceInvalid;
|
|
206
215
|
// Only ids that are in the document. `aria-describedby` naming a missing
|
|
207
216
|
// element makes a screen reader announce nothing rather than skipping it.
|
|
208
217
|
const described = [
|
|
209
218
|
hasDescription ? descriptionId : null,
|
|
219
|
+
hasStatus ? statusId : null,
|
|
210
220
|
wrong && hasError ? errorId : null,
|
|
211
221
|
].filter(Boolean);
|
|
212
222
|
|
|
@@ -214,14 +224,17 @@ export component FieldRoot(
|
|
|
214
224
|
controlId: `${base}-control`,
|
|
215
225
|
labelId: `${base}-label`,
|
|
216
226
|
descriptionId,
|
|
227
|
+
statusId,
|
|
217
228
|
errorId,
|
|
218
229
|
invalid: wrong,
|
|
219
230
|
required: required || sourceRequired,
|
|
231
|
+
busy: busy || sourceBusy,
|
|
220
232
|
group,
|
|
221
233
|
message,
|
|
222
234
|
control,
|
|
223
235
|
describedBy: described.length === 0 ? undefined : described.join(" "),
|
|
224
236
|
registerDescription: setHasDescription,
|
|
237
|
+
registerStatus: setHasStatus,
|
|
225
238
|
registerError: setHasError,
|
|
226
239
|
};
|
|
227
240
|
}, [
|
|
@@ -230,27 +243,31 @@ export component FieldRoot(
|
|
|
230
243
|
sourceInvalid,
|
|
231
244
|
required,
|
|
232
245
|
sourceRequired,
|
|
246
|
+
busy,
|
|
247
|
+
sourceBusy,
|
|
233
248
|
group,
|
|
234
249
|
message,
|
|
235
250
|
control,
|
|
236
251
|
hasDescription,
|
|
252
|
+
hasStatus,
|
|
237
253
|
hasError,
|
|
238
254
|
]);
|
|
239
255
|
|
|
256
|
+
const props = withProps(rest, {
|
|
257
|
+
// A group names itself, describes itself and reports its own validity,
|
|
258
|
+
// because there is no one control inside it to carry any of the three.
|
|
259
|
+
// See the module header.
|
|
260
|
+
"aria-busy": group && state.busy ? "true" : undefined,
|
|
261
|
+
"aria-describedby": group ? state.describedBy : undefined,
|
|
262
|
+
"aria-invalid": group && state.invalid ? "true" : undefined,
|
|
263
|
+
"aria-labelledby": group ? state.labelId : undefined,
|
|
264
|
+
children,
|
|
265
|
+
role: group ? "group" : undefined,
|
|
266
|
+
});
|
|
267
|
+
|
|
240
268
|
return (
|
|
241
269
|
<FieldContext.Provider value={state}>
|
|
242
|
-
<div
|
|
243
|
-
{...rest}
|
|
244
|
-
// A group names itself, describes itself and reports its own validity,
|
|
245
|
-
// because there is no one control inside it to carry any of the three.
|
|
246
|
-
// See the module header.
|
|
247
|
-
aria-describedby={group ? state.describedBy : undefined}
|
|
248
|
-
aria-invalid={group && state.invalid ? "true" : undefined}
|
|
249
|
-
aria-labelledby={group ? state.labelId : undefined}
|
|
250
|
-
role={group ? "group" : undefined}
|
|
251
|
-
>
|
|
252
|
-
{children}
|
|
253
|
-
</div>
|
|
270
|
+
{render != null ? render(props) : <div {...props} />}
|
|
254
271
|
</FieldContext.Provider>
|
|
255
272
|
);
|
|
256
273
|
}
|
|
@@ -262,22 +279,16 @@ export component FieldRoot(
|
|
|
262
279
|
* `<label for>` naming something that is not a form control is ignored by every
|
|
263
280
|
* browser, and ignored silently. The module header says more.
|
|
264
281
|
*/
|
|
265
|
-
export component FieldLabel(children: React.Node, ...rest: Rest) {
|
|
282
|
+
export component FieldLabel(children: React.Node, render?: RenderProp, ...rest: Rest) {
|
|
266
283
|
const field = useField("Field.Label");
|
|
267
284
|
// `rest` first: a caller `id` here would break the relationship the control
|
|
268
285
|
// points at, and it would break it silently.
|
|
269
286
|
if (field.group) {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
{children}
|
|
273
|
-
</span>
|
|
274
|
-
);
|
|
287
|
+
const props = withProps(rest, { children, id: field.labelId });
|
|
288
|
+
return render != null ? render(props) : <span {...props} />;
|
|
275
289
|
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
{children}
|
|
279
|
-
</label>
|
|
280
|
-
);
|
|
290
|
+
const props = withProps(rest, { children, htmlFor: field.controlId, id: field.labelId });
|
|
291
|
+
return render != null ? render(props) : <label {...props} />;
|
|
281
292
|
}
|
|
282
293
|
|
|
283
294
|
/**
|
|
@@ -289,7 +300,7 @@ export component FieldLabel(children: React.Node, ...rest: Rest) {
|
|
|
289
300
|
* `ref` and handlers *and* the field's attributes from one spread instead of
|
|
290
301
|
* two that overwrite each other.
|
|
291
302
|
*/
|
|
292
|
-
export component FieldControl(render:
|
|
303
|
+
export component FieldControl(render: RenderProp) {
|
|
293
304
|
const field = useField("Field.Control");
|
|
294
305
|
// A group already carries the name, the description and the validity, and a
|
|
295
306
|
// control repeating them makes a reader hear the error once for the set and
|
|
@@ -297,6 +308,7 @@ export component FieldControl(render: (props: Rest) => React.Node) {
|
|
|
297
308
|
const own: Rest = field.group
|
|
298
309
|
? { "aria-required": field.required ? "true" : undefined }
|
|
299
310
|
: {
|
|
311
|
+
"aria-busy": field.busy ? "true" : undefined,
|
|
300
312
|
id: field.controlId,
|
|
301
313
|
"aria-labelledby": field.labelId,
|
|
302
314
|
"aria-describedby": field.describedBy,
|
|
@@ -307,7 +319,7 @@ export component FieldControl(render: (props: Rest) => React.Node) {
|
|
|
307
319
|
}
|
|
308
320
|
|
|
309
321
|
/** Help text, which the control points at while it is rendered. */
|
|
310
|
-
export component FieldDescription(children: React.Node, ...rest: Rest) {
|
|
322
|
+
export component FieldDescription(children: React.Node, render?: RenderProp, ...rest: Rest) {
|
|
311
323
|
const field = useField("Field.Description");
|
|
312
324
|
const register = field.registerDescription;
|
|
313
325
|
useEffect(() => {
|
|
@@ -315,11 +327,32 @@ export component FieldDescription(children: React.Node, ...rest: Rest) {
|
|
|
315
327
|
return () => register(false);
|
|
316
328
|
}, [register]);
|
|
317
329
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
330
|
+
const props = withProps(rest, { children, id: field.descriptionId });
|
|
331
|
+
return render != null ? render(props) : <p {...props} />;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Non-error feedback for the field.
|
|
336
|
+
*
|
|
337
|
+
* A polite live region has to exist before the text changes. So unlike
|
|
338
|
+
* `Field.Error`, this part stays in the document even while it is empty; the
|
|
339
|
+
* control points at it only while the caller rendered the part.
|
|
340
|
+
*/
|
|
341
|
+
export component FieldStatus(children?: React.Node, render?: RenderProp, ...rest: Rest) {
|
|
342
|
+
const field = useField("Field.Status");
|
|
343
|
+
const register = field.registerStatus;
|
|
344
|
+
useEffect(() => {
|
|
345
|
+
register(true);
|
|
346
|
+
return () => register(false);
|
|
347
|
+
}, [register]);
|
|
348
|
+
|
|
349
|
+
const props = withProps(rest, {
|
|
350
|
+
"aria-live": "polite",
|
|
351
|
+
children: children ?? "",
|
|
352
|
+
id: field.statusId,
|
|
353
|
+
role: "status",
|
|
354
|
+
});
|
|
355
|
+
return render != null ? render(props) : <p {...props} />;
|
|
323
356
|
}
|
|
324
357
|
|
|
325
358
|
/**
|
|
@@ -334,7 +367,7 @@ export component FieldDescription(children: React.Node, ...rest: Rest) {
|
|
|
334
367
|
* store does not need the caller to reach back into `formState.errors` for a
|
|
335
368
|
* string the source is already carrying.
|
|
336
369
|
*/
|
|
337
|
-
export component FieldError(children?: React.Node, ...rest: Rest) {
|
|
370
|
+
export component FieldError(children?: React.Node, render?: RenderProp, ...rest: Rest) {
|
|
338
371
|
const field = useField("Field.Error");
|
|
339
372
|
const register = field.registerError;
|
|
340
373
|
useEffect(() => {
|
|
@@ -345,9 +378,10 @@ export component FieldError(children?: React.Node, ...rest: Rest) {
|
|
|
345
378
|
if (!field.invalid) {
|
|
346
379
|
return null;
|
|
347
380
|
}
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
);
|
|
381
|
+
const props = withProps(rest, {
|
|
382
|
+
children: children ?? field.message,
|
|
383
|
+
id: field.errorId,
|
|
384
|
+
role: "alert",
|
|
385
|
+
});
|
|
386
|
+
return render != null ? render(props) : <p {...props} />;
|
|
353
387
|
}
|
package/hover-card.js
CHANGED
|
@@ -45,7 +45,7 @@ import { useStableCallback } from "@uniflowed/hooks/lifecycle";
|
|
|
45
45
|
|
|
46
46
|
import type { Align, LogicalSide } from "./internal/anchor.js";
|
|
47
47
|
import type { HoverIntent } from "./internal/hover-intent.js";
|
|
48
|
-
import type { Rest } from "./internal/merge-props.js";
|
|
48
|
+
import type { RenderProp, Rest } from "./internal/merge-props.js";
|
|
49
49
|
import { composeRefs, withProps, withoutComposed } from "./internal/merge-props.js";
|
|
50
50
|
import {
|
|
51
51
|
DEFAULT_CLOSE_DELAY,
|
|
@@ -127,11 +127,7 @@ export component HoverCardRoot(
|
|
|
127
127
|
* reachable by keyboard, and `useFocusableTrigger` refuses anything else —
|
|
128
128
|
* a hover card on a `<span>` is one a keyboard reader can never see.
|
|
129
129
|
*/
|
|
130
|
-
export component HoverCardTrigger(
|
|
131
|
-
children?: React.Node,
|
|
132
|
-
render?: (props: Rest) => React.Node,
|
|
133
|
-
...rest: Rest
|
|
134
|
-
) {
|
|
130
|
+
export component HoverCardTrigger(children?: React.Node, render?: RenderProp, ...rest: Rest) {
|
|
135
131
|
const card = useHoverCard("HoverCard.Trigger");
|
|
136
132
|
const { closeDelay, dismissed, intent, openDelay, triggerRef } = card;
|
|
137
133
|
useFocusableTrigger(triggerRef, "HoverCard.Trigger");
|
|
@@ -187,15 +183,12 @@ export component HoverCardTrigger(
|
|
|
187
183
|
triggerRef.current = element;
|
|
188
184
|
});
|
|
189
185
|
|
|
186
|
+
const props = withProps(withoutComposed(rest, ["ref"]), { children, ref: attach });
|
|
187
|
+
|
|
190
188
|
if (render != null) {
|
|
191
|
-
return render(
|
|
189
|
+
return render(props);
|
|
192
190
|
}
|
|
193
|
-
|
|
194
|
-
return (
|
|
195
|
-
<button {...withoutComposed(rest, ["ref"])} ref={attach} type="button">
|
|
196
|
-
{children}
|
|
197
|
-
</button>
|
|
198
|
-
);
|
|
191
|
+
return <button {...props} type="button" />;
|
|
199
192
|
}
|
|
200
193
|
|
|
201
194
|
/**
|
|
@@ -213,6 +206,7 @@ export component HoverCardBody(
|
|
|
213
206
|
alignOffset?: number = 0,
|
|
214
207
|
avoidCollisions?: boolean = true,
|
|
215
208
|
collisionPadding?: number = 0,
|
|
209
|
+
render?: RenderProp,
|
|
216
210
|
side?: LogicalSide = "bottom",
|
|
217
211
|
sideOffset?: number = 0,
|
|
218
212
|
...rest: Rest
|
|
@@ -317,18 +311,20 @@ export component HoverCardBody(
|
|
|
317
311
|
return null;
|
|
318
312
|
}
|
|
319
313
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
314
|
+
const props = withProps(withoutComposed(rest, ["ref"]), {
|
|
315
|
+
children,
|
|
316
|
+
"data-align": anchored.align,
|
|
317
|
+
"data-side": anchored.side,
|
|
318
|
+
"data-state": "open",
|
|
319
|
+
id: `${card.base}-body`,
|
|
320
|
+
ref: composeRefs(rest.ref, (element: HTMLElement | null) => {
|
|
321
|
+
bodyRef.current = element;
|
|
322
|
+
}),
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
if (render != null) {
|
|
326
|
+
return render(props);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return <div {...props} />;
|
|
334
330
|
}
|