@uniflowed/ui 0.0.0-alpha.9 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/accordion.js +84 -57
- package/alert-dialog.js +284 -0
- package/alert.js +142 -0
- package/avatar.js +280 -0
- package/breadcrumb.js +138 -0
- package/calendar.js +587 -0
- package/carousel.js +410 -0
- package/checkbox.js +215 -31
- package/collapsible.js +72 -48
- package/color-picker.js +172 -0
- package/combobox.js +216 -39
- package/context-menu.js +215 -0
- package/date-field.js +9 -0
- package/date-picker.js +357 -0
- package/date-range-picker.js +120 -0
- package/dialog.js +243 -178
- package/drag-drop.js +125 -0
- package/drawer.js +504 -0
- package/field.js +260 -43
- package/grid-list.js +8 -0
- package/hover-card.js +52 -52
- package/i18n-provider.js +89 -0
- package/index.js +1177 -31
- package/input-otp.js +218 -0
- package/interactions.js +2327 -0
- package/internal/anchor.js +71 -6
- package/internal/collection.js +562 -0
- package/internal/date-grid.js +260 -0
- package/internal/date-range.js +26 -0
- package/internal/disclosure.js +201 -0
- package/internal/menu-tree.js +228 -0
- package/internal/merge-props.js +85 -1
- package/internal/roving-focus.js +15 -4
- package/internal/segmented-field.js +317 -0
- package/internal/selection.js +171 -0
- package/internal/visually-hidden-style.js +41 -0
- package/list-box.js +13 -0
- package/menu.js +553 -361
- package/menubar.js +295 -0
- package/number-field.js +263 -0
- package/package.json +8 -28
- package/pagination.js +34 -22
- package/popover.js +116 -75
- package/progress.js +21 -16
- package/radio-group.js +81 -75
- package/range-calendar.js +79 -0
- package/resizable.js +155 -9
- package/scroll-area.js +283 -0
- package/select.js +83 -37
- package/separator.js +97 -0
- package/sheet.js +189 -0
- package/sidebar.js +320 -0
- package/skeleton.js +163 -0
- package/slider.js +95 -89
- package/switch.js +42 -34
- package/table.js +100 -71
- package/tabs.js +100 -91
- package/tag-group.js +8 -0
- package/time-field.js +8 -0
- package/toast.js +36 -66
- package/toggle-group.js +53 -49
- package/toggle.js +41 -27
- package/tooltip.js +48 -55
- package/tree.js +8 -0
- package/visually-hidden.js +259 -0
package/collapsible.js
CHANGED
|
@@ -23,25 +23,43 @@
|
|
|
23
23
|
// `internal/disclosure.js` explains what is done instead, and why React needs a
|
|
24
24
|
// hook to say it.
|
|
25
25
|
//
|
|
26
|
-
// #
|
|
26
|
+
// # The height, for a stylesheet that animates it
|
|
27
27
|
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
//
|
|
28
|
+
// `measure` puts the height the content *would* have on
|
|
29
|
+
// `Collapsible.Content` as `--uf-collapsible-height`, correct while the panel
|
|
30
|
+
// is still closed — which is the only moment it is any use, because
|
|
31
|
+
// `height: 0 → var(--uf-collapsible-height)` is a transition that has to know
|
|
32
|
+
// its destination before it starts. `internal/disclosure.js` holds the
|
|
33
|
+
// measuring pass and says why the obvious ways of asking all answer zero, and
|
|
34
|
+
// why the prop is opt-in rather than always on.
|
|
35
|
+
//
|
|
36
|
+
// [data-collapsible-content] {
|
|
37
|
+
// overflow: hidden;
|
|
38
|
+
// transition: height 150ms;
|
|
39
|
+
// height: 0;
|
|
40
|
+
// }
|
|
41
|
+
// [data-collapsible-content]:not([hidden]) {
|
|
42
|
+
// height: var(--uf-collapsible-height);
|
|
43
|
+
// }
|
|
44
|
+
//
|
|
45
|
+
// The selector is the caller's — a class, a `data-*` of their own, whatever
|
|
46
|
+
// they already style with. This package emits the number and no styles at all,
|
|
47
|
+
// which is the same division `internal/anchor.js` keeps for a popover: a
|
|
48
|
+
// stylesheet can say *how* to move, and only the component can say how far.
|
|
36
49
|
|
|
37
50
|
"use client";
|
|
38
51
|
|
|
39
52
|
import * as React from "@uniflowed/react";
|
|
40
53
|
import { createContext, useContext, useId, useMemo, useRef, useState } from "@uniflowed/react";
|
|
41
54
|
|
|
42
|
-
import type { Rest } from "./internal/merge-props.js";
|
|
43
|
-
import {
|
|
44
|
-
|
|
55
|
+
import type { RenderProp, Rest } from "./internal/merge-props.js";
|
|
56
|
+
import {
|
|
57
|
+
composeHandlers,
|
|
58
|
+
composeRefs,
|
|
59
|
+
withProps,
|
|
60
|
+
withoutComposed,
|
|
61
|
+
} from "./internal/merge-props.js";
|
|
62
|
+
import { useMeasuredHeight, usePresence, useUntilFound } from "./internal/disclosure.js";
|
|
45
63
|
import { useControlled } from "./internal/controlled-state.js";
|
|
46
64
|
|
|
47
65
|
type CollapsibleState = {|
|
|
@@ -51,6 +69,8 @@ type CollapsibleState = {|
|
|
|
51
69
|
/** Whether a `Collapsible.Content` is rendered, so the trigger names one that exists. */
|
|
52
70
|
readonly present: boolean,
|
|
53
71
|
readonly registerContent: (present: boolean) => void,
|
|
72
|
+
/** Whether the content carries its measured height; see the module header. */
|
|
73
|
+
readonly measure: boolean,
|
|
54
74
|
|};
|
|
55
75
|
|
|
56
76
|
const CollapsibleContext: React.Context<CollapsibleState | null> = createContext(null);
|
|
@@ -76,47 +96,47 @@ export component CollapsibleRoot(
|
|
|
76
96
|
defaultOpen?: boolean = false,
|
|
77
97
|
open?: boolean,
|
|
78
98
|
onOpenChange?: (open: boolean) => void,
|
|
99
|
+
measure?: boolean = false,
|
|
79
100
|
) {
|
|
80
101
|
const contentId = `${useId()}-content`;
|
|
81
102
|
const [isOpen, setOpen] = useControlled(open, defaultOpen, onOpenChange);
|
|
82
103
|
const [present, setPresent] = useState(false);
|
|
83
104
|
|
|
84
105
|
const state = useMemo(
|
|
85
|
-
() => ({ contentId, open: isOpen, setOpen, present, registerContent: setPresent }),
|
|
86
|
-
[contentId, isOpen, setOpen, present],
|
|
106
|
+
() => ({ contentId, open: isOpen, setOpen, present, registerContent: setPresent, measure }),
|
|
107
|
+
[contentId, isOpen, setOpen, present, measure],
|
|
87
108
|
);
|
|
88
109
|
|
|
89
110
|
return <CollapsibleContext.Provider value={state}>{children}</CollapsibleContext.Provider>;
|
|
90
111
|
}
|
|
91
112
|
|
|
92
|
-
/** The
|
|
113
|
+
/** The control that shows and hides the content. */
|
|
93
114
|
export component CollapsibleTrigger(
|
|
94
115
|
children: React.Node,
|
|
95
116
|
disabled?: boolean = false,
|
|
117
|
+
render?: RenderProp,
|
|
96
118
|
...rest: Rest
|
|
97
119
|
) {
|
|
98
120
|
const collapsible = useCollapsible("Collapsible.Trigger");
|
|
99
|
-
const
|
|
121
|
+
const props = withProps(withoutComposed(rest, ["onClick"]), {
|
|
122
|
+
// Named only while the content is in the document. A caller who renders
|
|
123
|
+
// the content conditionally — or not at all until data arrives — would
|
|
124
|
+
// otherwise have this trigger pointing at nothing.
|
|
125
|
+
"aria-controls": collapsible.present ? collapsible.contentId : undefined,
|
|
126
|
+
"aria-expanded": collapsible.open ? "true" : "false",
|
|
127
|
+
children,
|
|
128
|
+
disabled,
|
|
129
|
+
onClick: composeHandlers(rest.onClick, () => {
|
|
130
|
+
if (!disabled) {
|
|
131
|
+
collapsible.setOpen(!collapsible.open);
|
|
132
|
+
}
|
|
133
|
+
}),
|
|
134
|
+
});
|
|
100
135
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
// the content conditionally — or not at all until data arrives — would
|
|
106
|
-
// otherwise have this button pointing at nothing.
|
|
107
|
-
aria-controls={collapsible.present ? collapsible.contentId : undefined}
|
|
108
|
-
aria-expanded={collapsible.open ? "true" : "false"}
|
|
109
|
-
disabled={disabled}
|
|
110
|
-
onClick={composeHandlers(rest.onClick, () => {
|
|
111
|
-
if (!disabled) {
|
|
112
|
-
collapsible.setOpen(!collapsible.open);
|
|
113
|
-
}
|
|
114
|
-
})}
|
|
115
|
-
type="button"
|
|
116
|
-
>
|
|
117
|
-
{children}
|
|
118
|
-
</button>
|
|
119
|
-
);
|
|
136
|
+
if (render != null) {
|
|
137
|
+
return render(props);
|
|
138
|
+
}
|
|
139
|
+
return <button {...props} type="button" />;
|
|
120
140
|
}
|
|
121
141
|
|
|
122
142
|
/**
|
|
@@ -126,22 +146,26 @@ export component CollapsibleTrigger(
|
|
|
126
146
|
* the module header, and `internal/disclosure.js` for what `hidden` is upgraded
|
|
127
147
|
* to and why that takes an effect.
|
|
128
148
|
*/
|
|
129
|
-
export component CollapsibleContent(children: React.Node, ...rest: Rest) {
|
|
149
|
+
export component CollapsibleContent(children: React.Node, render?: RenderProp, ...rest: Rest) {
|
|
130
150
|
const collapsible = useCollapsible("Collapsible.Content");
|
|
131
151
|
const contentRef = useRef<HTMLElement | null>(null);
|
|
132
152
|
usePresence(collapsible.registerContent);
|
|
133
153
|
useUntilFound(contentRef, collapsible.open);
|
|
154
|
+
useMeasuredHeight(contentRef, collapsible.measure);
|
|
134
155
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
)
|
|
156
|
+
const props = withProps(withoutComposed(rest, ["ref"]), {
|
|
157
|
+
children,
|
|
158
|
+
hidden: !collapsible.open,
|
|
159
|
+
id: collapsible.contentId,
|
|
160
|
+
// React calls callback refs during commit; this node is only read by effects.
|
|
161
|
+
// uf-lint-disable-next-line react-compiler/refs
|
|
162
|
+
ref: composeRefs(rest.ref, (element: HTMLElement | null) => {
|
|
163
|
+
contentRef.current = element;
|
|
164
|
+
}),
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
if (render != null) {
|
|
168
|
+
return render(props);
|
|
169
|
+
}
|
|
170
|
+
return <div {...props} />;
|
|
147
171
|
}
|
package/color-picker.js
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
"use client";
|
|
3
|
+
import * as React from "@uniflowed/react";
|
|
4
|
+
import { createContext, useContext, useState } from "@uniflowed/react";
|
|
5
|
+
import { useControlled } from "./internal/controlled-state.js";
|
|
6
|
+
import type { RenderProp, Rest } from "./internal/merge-props.js";
|
|
7
|
+
import { composeHandlers, withProps } from "./internal/merge-props.js";
|
|
8
|
+
import { directionOf } from "./internal/roving-focus.js";
|
|
9
|
+
|
|
10
|
+
/** Canonical #rrggbb or #rrggbbaa, with CSS's short hex forms accepted at the boundary. */
|
|
11
|
+
export function parseColor(value: string): string | null {
|
|
12
|
+
let hex = value.trim().toLowerCase();
|
|
13
|
+
if (!/^#(?:[0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/.test(hex)) return null;
|
|
14
|
+
if (hex.length <= 5)
|
|
15
|
+
hex =
|
|
16
|
+
"#" +
|
|
17
|
+
Array.from(hex.slice(1))
|
|
18
|
+
.map((char) => char + char)
|
|
19
|
+
.join("");
|
|
20
|
+
return hex;
|
|
21
|
+
}
|
|
22
|
+
type ColorState = { value: string, disabled: boolean, set: (value: string) => void };
|
|
23
|
+
const ColorContext: React.Context<ColorState | null> = createContext(null);
|
|
24
|
+
hook useColor(): ColorState {
|
|
25
|
+
const color = useContext(ColorContext);
|
|
26
|
+
if (color == null) throw new Error("ColorPicker parts must be inside ColorPicker.Root");
|
|
27
|
+
return color;
|
|
28
|
+
}
|
|
29
|
+
export component ColorPickerRoot(
|
|
30
|
+
children: React.Node,
|
|
31
|
+
value?: string,
|
|
32
|
+
defaultValue?: string = "#000000",
|
|
33
|
+
onValueChange?: (value: string) => void,
|
|
34
|
+
disabled?: boolean = false,
|
|
35
|
+
render?: RenderProp,
|
|
36
|
+
...rest: Rest
|
|
37
|
+
) {
|
|
38
|
+
const [current, setCurrent] = useControlled(value, defaultValue, onValueChange);
|
|
39
|
+
const normalized = parseColor(current);
|
|
40
|
+
if (normalized == null) throw new RangeError("ColorPicker value must be a CSS hex color");
|
|
41
|
+
const state = {
|
|
42
|
+
value: normalized,
|
|
43
|
+
disabled,
|
|
44
|
+
set: (next: string) => {
|
|
45
|
+
const color = parseColor(next);
|
|
46
|
+
if (!disabled && color != null) setCurrent(color);
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
const props = withProps(rest, { children });
|
|
50
|
+
return (
|
|
51
|
+
<ColorContext.Provider value={state}>
|
|
52
|
+
{render != null ? render(props) : <div {...props} />}
|
|
53
|
+
</ColorContext.Provider>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
export component ColorPickerInput(render?: RenderProp, ...rest: Rest) {
|
|
57
|
+
const color = useColor();
|
|
58
|
+
const props = withProps(rest, {
|
|
59
|
+
type: "color",
|
|
60
|
+
value: color.value.slice(0, 7),
|
|
61
|
+
disabled: color.disabled,
|
|
62
|
+
onChange: composeHandlers(rest.onChange, (event: $FlowFixMe) =>
|
|
63
|
+
color.set(event.currentTarget.value + color.value.slice(7)),
|
|
64
|
+
),
|
|
65
|
+
});
|
|
66
|
+
return render != null ? render(props) : <input {...props} />;
|
|
67
|
+
}
|
|
68
|
+
export component ColorPickerField(render?: RenderProp, ...rest: Rest) {
|
|
69
|
+
const color = useColor();
|
|
70
|
+
const [draft, setDraft] = useState<string | null>(null);
|
|
71
|
+
const text = draft ?? color.value;
|
|
72
|
+
const invalid = parseColor(text) == null;
|
|
73
|
+
const commit = () => {
|
|
74
|
+
if (!invalid) {
|
|
75
|
+
color.set(text);
|
|
76
|
+
setDraft(null);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
const props = withProps(rest, {
|
|
80
|
+
type: "text",
|
|
81
|
+
value: text,
|
|
82
|
+
disabled: color.disabled,
|
|
83
|
+
"aria-invalid": invalid ? "true" : rest["aria-invalid"],
|
|
84
|
+
onChange: composeHandlers(rest.onChange, (event: $FlowFixMe) =>
|
|
85
|
+
setDraft(event.currentTarget.value),
|
|
86
|
+
),
|
|
87
|
+
onBlur: composeHandlers(rest.onBlur, commit),
|
|
88
|
+
onKeyDown: composeHandlers(rest.onKeyDown, (event: $FlowFixMe) => {
|
|
89
|
+
if (event.key === "Enter") {
|
|
90
|
+
event.preventDefault();
|
|
91
|
+
commit();
|
|
92
|
+
}
|
|
93
|
+
}),
|
|
94
|
+
});
|
|
95
|
+
return render != null ? render(props) : <input {...props} />;
|
|
96
|
+
}
|
|
97
|
+
export component ColorPickerChannel(
|
|
98
|
+
channel: "red" | "green" | "blue" | "alpha",
|
|
99
|
+
render?: RenderProp,
|
|
100
|
+
...rest: Rest
|
|
101
|
+
) {
|
|
102
|
+
const color = useColor();
|
|
103
|
+
const at = ["red", "green", "blue", "alpha"].indexOf(channel);
|
|
104
|
+
const value = parseInt(
|
|
105
|
+
(color.value + (color.value.length === 7 ? "ff" : "")).slice(1 + at * 2, 3 + at * 2),
|
|
106
|
+
16,
|
|
107
|
+
);
|
|
108
|
+
const change = (next: number) => {
|
|
109
|
+
const hex = color.value.slice(1).padEnd(8, "f");
|
|
110
|
+
const byte = Math.max(0, Math.min(255, Math.round(next)))
|
|
111
|
+
.toString(16)
|
|
112
|
+
.padStart(2, "0");
|
|
113
|
+
const full = hex.slice(0, at * 2) + byte + hex.slice(at * 2 + 2);
|
|
114
|
+
color.set("#" + (at === 3 || color.value.length === 9 ? full : full.slice(0, 6)));
|
|
115
|
+
};
|
|
116
|
+
const props = withProps(rest, {
|
|
117
|
+
type: "range",
|
|
118
|
+
min: 0,
|
|
119
|
+
max: 255,
|
|
120
|
+
step: 1,
|
|
121
|
+
value,
|
|
122
|
+
disabled: color.disabled,
|
|
123
|
+
"aria-label": rest["aria-label"] ?? channel,
|
|
124
|
+
"aria-valuetext": channel === "alpha" ? `${Math.round((value / 255) * 100)}%` : String(value),
|
|
125
|
+
onChange: composeHandlers(rest.onChange, (event: $FlowFixMe) =>
|
|
126
|
+
change(Number(event.currentTarget.value)),
|
|
127
|
+
),
|
|
128
|
+
onKeyDown: composeHandlers(rest.onKeyDown, (event: $FlowFixMe) => {
|
|
129
|
+
if (color.disabled) return;
|
|
130
|
+
const key = event.key;
|
|
131
|
+
if (
|
|
132
|
+
![
|
|
133
|
+
"ArrowLeft",
|
|
134
|
+
"ArrowRight",
|
|
135
|
+
"ArrowUp",
|
|
136
|
+
"ArrowDown",
|
|
137
|
+
"PageUp",
|
|
138
|
+
"PageDown",
|
|
139
|
+
"Home",
|
|
140
|
+
"End",
|
|
141
|
+
].includes(key)
|
|
142
|
+
)
|
|
143
|
+
return;
|
|
144
|
+
event.preventDefault();
|
|
145
|
+
const rtl = directionOf(event.currentTarget) === "rtl";
|
|
146
|
+
change(
|
|
147
|
+
key === "Home"
|
|
148
|
+
? 0
|
|
149
|
+
: key === "End"
|
|
150
|
+
? 255
|
|
151
|
+
: value +
|
|
152
|
+
(key === "PageUp"
|
|
153
|
+
? 10
|
|
154
|
+
: key === "PageDown"
|
|
155
|
+
? -10
|
|
156
|
+
: key === "ArrowUp" || key === (rtl ? "ArrowLeft" : "ArrowRight")
|
|
157
|
+
? 1
|
|
158
|
+
: -1),
|
|
159
|
+
);
|
|
160
|
+
}),
|
|
161
|
+
});
|
|
162
|
+
return render != null ? render(props) : <input {...props} />;
|
|
163
|
+
}
|
|
164
|
+
export component ColorPickerSwatch(render?: RenderProp, ...rest: Rest) {
|
|
165
|
+
const color = useColor();
|
|
166
|
+
const props = withProps(rest, {
|
|
167
|
+
"aria-hidden": "true",
|
|
168
|
+
"data-color": color.value,
|
|
169
|
+
style: { ...(rest.style as $FlowFixMe), backgroundColor: color.value },
|
|
170
|
+
});
|
|
171
|
+
return render != null ? render(props) : <span {...props} />;
|
|
172
|
+
}
|