@uniflowed/ui 0.0.0-alpha.9 → 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 -177
- 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 +1159 -31
- package/input-otp.js +218 -0
- package/interactions.js +2327 -0
- package/internal/anchor.js +71 -6
- 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/menu-tree.js +228 -0
- package/internal/merge-props.js +85 -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 -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 +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 +48 -55
- package/tree.js +8 -0
package/toggle.js
CHANGED
|
@@ -28,8 +28,9 @@
|
|
|
28
28
|
//
|
|
29
29
|
// Because a toggle button is a button, and a button activates on both. That is
|
|
30
30
|
// the same reasoning `switch.js` gives for `Enter`, and the opposite of
|
|
31
|
-
// `checkbox.js`,
|
|
32
|
-
// reader answers on their way to
|
|
31
|
+
// `checkbox.js`, where `Enter` submits the form rather than touching the
|
|
32
|
+
// control — because a checkbox is something a reader answers on their way to
|
|
33
|
+
// submitting a form, and that is what the native one does with the key.
|
|
33
34
|
//
|
|
34
35
|
// # It is `disabled`, not `aria-disabled`
|
|
35
36
|
//
|
|
@@ -43,49 +44,62 @@
|
|
|
43
44
|
|
|
44
45
|
import * as React from "@uniflowed/react";
|
|
45
46
|
|
|
46
|
-
import type { Rest } from "./internal/merge-props.js";
|
|
47
|
-
import { composeHandlers, withoutComposed } from "./internal/merge-props.js";
|
|
47
|
+
import type { PartEvent, RenderProp, Rest } from "./internal/merge-props.js";
|
|
48
|
+
import { composeHandlers, withProps, withoutComposed } from "./internal/merge-props.js";
|
|
48
49
|
import { useControlled } from "./internal/controlled-state.js";
|
|
49
50
|
|
|
50
|
-
/**
|
|
51
|
+
/**
|
|
52
|
+
* A button whose state stays applied: pressed or not.
|
|
53
|
+
*
|
|
54
|
+
* `render` is the escape hatch. A caller-rendered element gets `role="button"`
|
|
55
|
+
* because it may be a link or a `div`; the native button branch keeps the role
|
|
56
|
+
* implicit and only adds `type="button"`, which is true of the element rather
|
|
57
|
+
* than of the toggle behaviour.
|
|
58
|
+
*/
|
|
51
59
|
export component Toggle(
|
|
52
60
|
pressed?: boolean,
|
|
53
61
|
defaultPressed?: boolean = false,
|
|
54
62
|
onPressedChange?: (pressed: boolean) => void,
|
|
55
63
|
disabled?: boolean = false,
|
|
56
64
|
children?: React.Node,
|
|
65
|
+
render?: RenderProp,
|
|
57
66
|
...rest: Rest
|
|
58
67
|
) {
|
|
59
68
|
const [on, setOn] = useControlled(pressed, defaultPressed, onPressedChange);
|
|
60
69
|
const passed = withoutComposed(rest, ["onClick", "onKeyDown"]);
|
|
70
|
+
const semantics = {
|
|
71
|
+
"aria-pressed": on ? "true" : "false",
|
|
72
|
+
children,
|
|
73
|
+
disabled,
|
|
74
|
+
onClick: composeHandlers(rest.onClick, (_event: PartEvent) => {
|
|
75
|
+
if (!disabled) {
|
|
76
|
+
setOn(!on);
|
|
77
|
+
}
|
|
78
|
+
}),
|
|
79
|
+
onKeyDown: composeHandlers(rest.onKeyDown, (event: PartEvent) => {
|
|
80
|
+
if (disabled || (event.key !== " " && event.key !== "Enter")) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
// Preventing the default stops `Space` scrolling the page, and stops
|
|
84
|
+
// the browser's own click arriving after this handler and pressing the
|
|
85
|
+
// button a second time — back to where it started, which reads as the
|
|
86
|
+
// key having done nothing at all.
|
|
87
|
+
event.preventDefault();
|
|
88
|
+
setOn(!on);
|
|
89
|
+
}),
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
if (render != null) {
|
|
93
|
+
return render(withProps(passed, { ...semantics, role: "button" }));
|
|
94
|
+
}
|
|
61
95
|
|
|
62
96
|
return (
|
|
63
97
|
<button
|
|
64
|
-
{...passed}
|
|
98
|
+
{...withProps(passed, semantics)}
|
|
65
99
|
// No `role`: this *is* a button, and `aria-pressed` is what makes it a
|
|
66
100
|
// toggle one. Adding `role="button"` to a `<button>` would be noise, and
|
|
67
101
|
// adding any other role would be a lie about what pressing it does.
|
|
68
|
-
aria-pressed={on ? "true" : "false"}
|
|
69
|
-
disabled={disabled}
|
|
70
|
-
onClick={composeHandlers(rest.onClick, () => {
|
|
71
|
-
if (!disabled) {
|
|
72
|
-
setOn(!on);
|
|
73
|
-
}
|
|
74
|
-
})}
|
|
75
|
-
onKeyDown={composeHandlers(rest.onKeyDown, (event) => {
|
|
76
|
-
if (disabled || (event.key !== " " && event.key !== "Enter")) {
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
// Preventing the default stops `Space` scrolling the page, and stops
|
|
80
|
-
// the browser's own click arriving after this handler and pressing the
|
|
81
|
-
// button a second time — back to where it started, which reads as the
|
|
82
|
-
// key having done nothing at all.
|
|
83
|
-
event.preventDefault();
|
|
84
|
-
setOn(!on);
|
|
85
|
-
})}
|
|
86
102
|
type="button"
|
|
87
|
-
|
|
88
|
-
{children}
|
|
89
|
-
</button>
|
|
103
|
+
/>
|
|
90
104
|
);
|
|
91
105
|
}
|
package/tooltip.js
CHANGED
|
@@ -73,9 +73,9 @@ import { createContext, useContext, useEffect, useId, useMemo, useRef } from "@u
|
|
|
73
73
|
import { useEventListener } from "@uniflowed/hooks/dom";
|
|
74
74
|
import { useStableCallback } from "@uniflowed/hooks/lifecycle";
|
|
75
75
|
|
|
76
|
-
import type { Align,
|
|
76
|
+
import type { Align, LogicalSide } from "./internal/anchor.js";
|
|
77
77
|
import type { DelayGroup, HoverIntent } from "./internal/hover-intent.js";
|
|
78
|
-
import type { Rest } from "./internal/merge-props.js";
|
|
78
|
+
import type { RenderProp, Rest } from "./internal/merge-props.js";
|
|
79
79
|
import { composeRefs, withProps, withoutComposed } from "./internal/merge-props.js";
|
|
80
80
|
import {
|
|
81
81
|
DEFAULT_CLOSE_DELAY,
|
|
@@ -89,7 +89,7 @@ import {
|
|
|
89
89
|
import { useAnchor } from "./internal/anchor.js";
|
|
90
90
|
import { useControlled } from "./internal/controlled-state.js";
|
|
91
91
|
|
|
92
|
-
export type { Align, Side } from "./internal/anchor.js";
|
|
92
|
+
export type { Align, LogicalSide, Side } from "./internal/anchor.js";
|
|
93
93
|
|
|
94
94
|
/** What a `Tooltip.Provider` shares with the tooltips inside it. */
|
|
95
95
|
type TooltipScope = {|
|
|
@@ -126,7 +126,7 @@ type TooltipState = {|
|
|
|
126
126
|
* cannot be closed. It is cleared when the reader leaves the trigger, so
|
|
127
127
|
* coming back opens it again.
|
|
128
128
|
*/
|
|
129
|
-
readonly
|
|
129
|
+
readonly dismissedRef: { current: boolean },
|
|
130
130
|
|};
|
|
131
131
|
|
|
132
132
|
const TooltipContext: React.Context<TooltipState | null> = createContext(null);
|
|
@@ -180,7 +180,7 @@ export component TooltipRoot(
|
|
|
180
180
|
const scope = useContext(TooltipScopeContext);
|
|
181
181
|
const [isOpen, setOpen] = useControlled(open, defaultOpen, onOpenChange);
|
|
182
182
|
const triggerRef = useRef<HTMLElement | null>(null);
|
|
183
|
-
const
|
|
183
|
+
const dismissedRef = useRef(false);
|
|
184
184
|
const intent = useHoverIntent(setOpen);
|
|
185
185
|
const own = openDelay ?? scope?.delayDuration ?? DEFAULT_OPEN_DELAY;
|
|
186
186
|
const group = scope?.group;
|
|
@@ -217,7 +217,7 @@ export component TooltipRoot(
|
|
|
217
217
|
() => ({
|
|
218
218
|
base,
|
|
219
219
|
closeDelay,
|
|
220
|
-
|
|
220
|
+
dismissedRef,
|
|
221
221
|
intent,
|
|
222
222
|
open: isOpen,
|
|
223
223
|
openDelay: () => group?.delayFor(own) ?? own,
|
|
@@ -243,25 +243,21 @@ export component TooltipRoot(
|
|
|
243
243
|
* `render` function that forgets to spread something still gets a working
|
|
244
244
|
* tooltip; see the module header.
|
|
245
245
|
*/
|
|
246
|
-
export component TooltipTrigger(
|
|
247
|
-
children?: React.Node,
|
|
248
|
-
render?: (props: Rest) => React.Node,
|
|
249
|
-
...rest: Rest
|
|
250
|
-
) {
|
|
246
|
+
export component TooltipTrigger(children?: React.Node, render?: RenderProp, ...rest: Rest) {
|
|
251
247
|
const tooltip = useTooltip("Tooltip.Trigger");
|
|
252
|
-
const { closeDelay,
|
|
248
|
+
const { closeDelay, dismissedRef, intent, openDelay, setOpen, triggerRef } = tooltip;
|
|
253
249
|
useFocusableTrigger(triggerRef, "Tooltip.Trigger");
|
|
254
250
|
|
|
255
251
|
// Whether the pointer put focus here. A click focuses the button, and
|
|
256
252
|
// reopening the tooltip the click just dismissed would put it back over the
|
|
257
253
|
// thing the reader pressed — so a focus that arrived with a press opens
|
|
258
254
|
// nothing, and the next one, which is the keyboard's, does.
|
|
259
|
-
const
|
|
255
|
+
const pressedRef = useRef(false);
|
|
260
256
|
|
|
261
257
|
useEventListener(triggerRef, "pointerenter", (event: $FlowFixMe) => {
|
|
262
258
|
// A tap is not a hover. See the module header: opening here is what eats
|
|
263
259
|
// the tap the control was there to receive.
|
|
264
|
-
if (event.pointerType === "touch" ||
|
|
260
|
+
if (event.pointerType === "touch" || dismissedRef.current) {
|
|
265
261
|
return;
|
|
266
262
|
}
|
|
267
263
|
intent.openAfter(openDelay());
|
|
@@ -269,57 +265,50 @@ export component TooltipTrigger(
|
|
|
269
265
|
useEventListener(triggerRef, "pointerleave", () => {
|
|
270
266
|
// Leaving is what makes a dismissal stop applying: coming back is a fresh
|
|
271
267
|
// gesture and deserves a fresh answer.
|
|
272
|
-
|
|
268
|
+
dismissedRef.current = false;
|
|
273
269
|
intent.closeAfter(closeDelay);
|
|
274
270
|
});
|
|
275
271
|
useEventListener(triggerRef, "pointerdown", () => {
|
|
276
|
-
|
|
272
|
+
pressedRef.current = true;
|
|
277
273
|
intent.cancel();
|
|
278
274
|
setOpen(false);
|
|
279
275
|
});
|
|
280
276
|
useEventListener(triggerRef, "focusin", () => {
|
|
281
|
-
if (
|
|
282
|
-
|
|
277
|
+
if (pressedRef.current) {
|
|
278
|
+
pressedRef.current = false;
|
|
283
279
|
return;
|
|
284
280
|
}
|
|
285
|
-
if (
|
|
281
|
+
if (dismissedRef.current) {
|
|
286
282
|
return;
|
|
287
283
|
}
|
|
288
284
|
// No delay: the reader has already said what they want by arriving here.
|
|
289
285
|
intent.openAfter(0);
|
|
290
286
|
});
|
|
291
287
|
useEventListener(triggerRef, "focusout", () => {
|
|
292
|
-
|
|
293
|
-
|
|
288
|
+
pressedRef.current = false;
|
|
289
|
+
dismissedRef.current = false;
|
|
294
290
|
intent.closeAfter(0);
|
|
295
291
|
});
|
|
296
292
|
|
|
297
293
|
// Annotated because this one is not written inside a `ref={...}`, and there
|
|
298
294
|
// is nothing else here for Flow to infer the element's type from.
|
|
295
|
+
// React calls callback refs during commit; pointer and focus handlers read it later.
|
|
296
|
+
// uf-lint-disable-next-line react-compiler/refs
|
|
299
297
|
const attach = composeRefs(rest.ref, (element: HTMLElement | null) => {
|
|
300
298
|
triggerRef.current = element;
|
|
301
299
|
});
|
|
302
300
|
// Only while it is there. `aria-describedby` pointing at an element that has
|
|
303
301
|
// been removed is the dangling reference this package keeps coming back to.
|
|
304
|
-
const
|
|
302
|
+
const props = withProps(withoutComposed(rest, ["ref"]), {
|
|
305
303
|
"aria-describedby": tooltip.open ? `${tooltip.base}-body` : undefined,
|
|
304
|
+
children,
|
|
306
305
|
ref: attach,
|
|
307
|
-
};
|
|
306
|
+
});
|
|
308
307
|
|
|
309
308
|
if (render != null) {
|
|
310
|
-
return render(
|
|
309
|
+
return render(props);
|
|
311
310
|
}
|
|
312
|
-
|
|
313
|
-
return (
|
|
314
|
-
<button
|
|
315
|
-
{...withoutComposed(rest, ["ref"])}
|
|
316
|
-
aria-describedby={ours["aria-describedby"]}
|
|
317
|
-
ref={attach}
|
|
318
|
-
type="button"
|
|
319
|
-
>
|
|
320
|
-
{children}
|
|
321
|
-
</button>
|
|
322
|
-
);
|
|
311
|
+
return <button {...props} type="button" />;
|
|
323
312
|
}
|
|
324
313
|
|
|
325
314
|
/**
|
|
@@ -337,19 +326,20 @@ export component TooltipBody(
|
|
|
337
326
|
alignOffset?: number = 0,
|
|
338
327
|
avoidCollisions?: boolean = true,
|
|
339
328
|
collisionPadding?: number = 0,
|
|
340
|
-
|
|
329
|
+
render?: RenderProp,
|
|
330
|
+
side?: LogicalSide = "top",
|
|
341
331
|
sideOffset?: number = 0,
|
|
342
332
|
...rest: Rest
|
|
343
333
|
) {
|
|
344
334
|
const tooltip = useTooltip("Tooltip.Body");
|
|
345
|
-
const { closeDelay, intent, open, triggerRef } = tooltip;
|
|
335
|
+
const { closeDelay, dismissedRef, intent, open, triggerRef } = tooltip;
|
|
346
336
|
const bodyRef = useRef<HTMLElement | null>(null);
|
|
347
337
|
const close = useStableCallback(() => {
|
|
348
338
|
// `Escape` dismisses it *and* keeps it dismissed while the reader is still
|
|
349
339
|
// on the trigger. Without the flag the pointer that is still resting there
|
|
350
340
|
// — or, for a hover card, the focus it hands back — reopens it at once,
|
|
351
341
|
// and the key does nothing a reader can see.
|
|
352
|
-
|
|
342
|
+
dismissedRef.current = true;
|
|
353
343
|
intent.cancel();
|
|
354
344
|
tooltip.setOpen(false);
|
|
355
345
|
});
|
|
@@ -391,21 +381,24 @@ export component TooltipBody(
|
|
|
391
381
|
return null;
|
|
392
382
|
}
|
|
393
383
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
)
|
|
384
|
+
const props = withProps(withoutComposed(rest, ["ref"]), {
|
|
385
|
+
children,
|
|
386
|
+
"data-align": anchored.align,
|
|
387
|
+
"data-side": anchored.side,
|
|
388
|
+
"data-state": "open",
|
|
389
|
+
id: `${tooltip.base}-body`,
|
|
390
|
+
// React calls callback refs during commit; placement effects read it later.
|
|
391
|
+
// uf-lint-disable-next-line react-compiler/refs
|
|
392
|
+
ref: composeRefs(rest.ref, (element) => {
|
|
393
|
+
bodyRef.current = element;
|
|
394
|
+
}),
|
|
395
|
+
role: "tooltip",
|
|
396
|
+
// No `tabIndex`. A tooltip the keyboard can land in is a stop the reader
|
|
397
|
+
// did not ask for and cannot leave the way they expect.
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
if (render != null) {
|
|
401
|
+
return render(props);
|
|
402
|
+
}
|
|
403
|
+
return <div {...props} />;
|
|
411
404
|
}
|
package/tree.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
"use client";
|
|
3
|
+
import * as React from "@uniflowed/react";
|
|
4
|
+
import { CollectionRoot } from "./internal/collection.js";
|
|
5
|
+
import type { CollectionProps } from "./internal/collection.js";
|
|
6
|
+
export component Tree(...props: CollectionProps) {
|
|
7
|
+
return <CollectionRoot options={props} kind="tree" />;
|
|
8
|
+
}
|