@flood-md/react 0.2.3 → 0.2.5
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/README.md +398 -3
- package/dist/alert-dialog.d.ts +2 -0
- package/dist/alert-dialog.js +23 -0
- package/dist/avatar.d.ts +5 -0
- package/dist/avatar.js +8 -0
- package/dist/checkbox.d.ts +4 -0
- package/dist/checkbox.js +15 -0
- package/dist/combobox.d.ts +11 -0
- package/dist/combobox.js +301 -0
- package/dist/content-elements.d.ts +8 -0
- package/dist/content-elements.js +47 -0
- package/dist/data-display.d.ts +3 -0
- package/dist/data-display.js +11 -0
- package/dist/disclosure.js +2 -1
- package/dist/feedback-actions.d.ts +3 -0
- package/dist/feedback-actions.js +106 -0
- package/dist/feedback.d.ts +8 -0
- package/dist/feedback.js +29 -0
- package/dist/file-input.d.ts +8 -0
- package/dist/file-input.js +86 -0
- package/dist/focusable-hidden.d.ts +3 -0
- package/dist/focusable-hidden.js +99 -0
- package/dist/fonts/InterVariable-Italic.woff2 +0 -0
- package/dist/fonts/README.md +9 -5
- package/dist/form-controls.d.ts +6 -0
- package/dist/form-controls.js +250 -0
- package/dist/foundations.css +543 -12
- package/dist/index.d.ts +513 -2
- package/dist/index.js +223 -36
- package/dist/menu.d.ts +2 -0
- package/dist/menu.js +130 -0
- package/dist/navigation-layout.d.ts +7 -0
- package/dist/navigation-layout.js +77 -0
- package/dist/navigation.js +8 -8
- package/dist/numeric-input.d.ts +8 -0
- package/dist/numeric-input.js +68 -0
- package/dist/overlay-runtime.js +43 -16
- package/dist/overlays.d.ts +7 -1
- package/dist/overlays.js +17 -6
- package/dist/scroll-area.js +17 -2
- package/dist/select.js +96 -47
- package/dist/slider.d.ts +7 -0
- package/dist/slider.js +89 -0
- package/dist/styles.css +9 -0
- package/dist/text-inputs.d.ts +9 -0
- package/dist/text-inputs.js +45 -0
- package/dist/toolbar.d.ts +2 -0
- package/dist/toolbar.js +80 -0
- package/package.json +2 -2
package/dist/combobox.js
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef, useEffect, useId, useImperativeHandle, useLayoutEffect, useRef, useState } from "react";
|
|
3
|
+
import { Input, InputGroup, IconButton, ScrollArea, Tag } from "./index.js";
|
|
4
|
+
import { mergeDescriptions, resolveFieldStatus } from "./field-state.js";
|
|
5
|
+
import { positionLayer, registerLayer } from "./overlay-runtime.js";
|
|
6
|
+
const useBrowserLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
|
|
7
|
+
const searchable = (text) => text.normalize("NFKD").replace(/\p{M}/gu, "").toLowerCase();
|
|
8
|
+
/** Shared search/layer model. Public single and multiple value contracts stay distinct. */
|
|
9
|
+
export const SearchSelection = forwardRef(function SearchSelection({ options, selectedValues: value, initialValues: defaultValue, onSelectionChange: onValueChange, multiple = false, removeItemLabel = "Remove", clearable = false, emptyMessage = "No results found.", listLabel = "Options", toggleLabel = "Show options", clearLabel = "Clear selection", invalidSelectionMessage = "Select an option.", controlSize = "md", readOnly, disabled, required, name, id: explicitId, className, onKeyDown, onBlur, onClick, onInvalid, autoComplete = "off", ...props }, forwardedRef) {
|
|
10
|
+
if (new Set(options.map(option => option.value)).size !== options.length || options.some(option => !option.value || !option.label.trim()))
|
|
11
|
+
throw new Error("Flood Combobox options need unique nonempty values and labels");
|
|
12
|
+
if (![emptyMessage, listLabel, toggleLabel, clearLabel, invalidSelectionMessage].every(text => text.trim()))
|
|
13
|
+
throw new Error("Flood Combobox messages and action labels must be nonempty");
|
|
14
|
+
for (const values of [defaultValue, value])
|
|
15
|
+
if (values !== undefined && (!Array.isArray(values) || new Set(values).size !== values.length || values.some(item => typeof item !== "string" || !item)))
|
|
16
|
+
throw new Error("Flood selection values must be unique nonempty strings");
|
|
17
|
+
if (multiple && !removeItemLabel.trim())
|
|
18
|
+
throw new Error("Flood MultiSelect requires an accessible removal label");
|
|
19
|
+
const generatedId = useId(), id = explicitId ?? `flood-combobox-${generatedId}`;
|
|
20
|
+
const menuId = `${id}-options`, validationId = `${id}-validation`;
|
|
21
|
+
const input = useRef(null), root = useRef(null), panel = useRef(null), viewport = useRef(null);
|
|
22
|
+
useImperativeHandle(forwardedRef, () => input.current, []);
|
|
23
|
+
const initial = useRef([...defaultValue]);
|
|
24
|
+
const [local, setLocal] = useState(() => [...defaultValue]), [query, setQuery] = useState(null);
|
|
25
|
+
const [open, setOpen] = useState(false), [activeValue, setActiveValue] = useState();
|
|
26
|
+
const [validation, setValidation] = useState("");
|
|
27
|
+
const selections = (value ?? local).map(id => options.find(option => option.value === id)).filter((option) => Boolean(option));
|
|
28
|
+
const selected = selections[0], selectedIds = selections.map(option => option.value);
|
|
29
|
+
const displayed = query ?? (multiple ? "" : selected?.label ?? "");
|
|
30
|
+
const filtered = query === null ? options : options.filter(option => searchable([option.label, ...(option.keywords ?? [])].join(" ")).includes(searchable(query)));
|
|
31
|
+
const enabled = filtered.filter(option => !option.disabled);
|
|
32
|
+
const active = enabled.find(option => option.value === activeValue) ?? enabled[0];
|
|
33
|
+
const optionId = (optionValue) => `${menuId}-${options.findIndex(option => option.value === optionValue)}`;
|
|
34
|
+
const current = useRef({ value, selected, readOnly, disabled });
|
|
35
|
+
current.current = { value, selected, readOnly, disabled };
|
|
36
|
+
const close = () => { setOpen(false); setQuery(null); };
|
|
37
|
+
const inert = () => Boolean(readOnly || disabled || input.current?.matches(":disabled"));
|
|
38
|
+
const publish = (next) => {
|
|
39
|
+
if (value === undefined)
|
|
40
|
+
setLocal(next);
|
|
41
|
+
if (next.length !== selectedIds.length || next.some((id, index) => id !== selectedIds[index]))
|
|
42
|
+
onValueChange?.([...next]);
|
|
43
|
+
setValidation("");
|
|
44
|
+
};
|
|
45
|
+
const remove = (id) => {
|
|
46
|
+
if (inert() || options.find(option => option.value === id)?.disabled)
|
|
47
|
+
return;
|
|
48
|
+
publish(selectedIds.filter(value => value !== id));
|
|
49
|
+
input.current?.focus({ preventScroll: true });
|
|
50
|
+
};
|
|
51
|
+
const select = (next) => {
|
|
52
|
+
if (inert() || (next !== "" && !options.some(option => option.value === next && !option.disabled)))
|
|
53
|
+
return;
|
|
54
|
+
if (multiple) {
|
|
55
|
+
publish(next === "" ? selections.filter(option => option.disabled).map(option => option.value) : selectedIds.includes(next) ? selectedIds.filter(id => id !== next) : [...selectedIds, next]);
|
|
56
|
+
if (next) {
|
|
57
|
+
setQuery("");
|
|
58
|
+
setActiveValue(next);
|
|
59
|
+
setOpen(true);
|
|
60
|
+
}
|
|
61
|
+
else
|
|
62
|
+
close();
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
publish(next ? [next] : []);
|
|
66
|
+
close();
|
|
67
|
+
}
|
|
68
|
+
input.current?.focus({ preventScroll: true });
|
|
69
|
+
};
|
|
70
|
+
const show = () => {
|
|
71
|
+
if (inert())
|
|
72
|
+
return;
|
|
73
|
+
setActiveValue(selected?.value);
|
|
74
|
+
setOpen(true);
|
|
75
|
+
};
|
|
76
|
+
useBrowserLayoutEffect(() => {
|
|
77
|
+
input.current?.setCustomValidity(required && !selections.length ? invalidSelectionMessage : "");
|
|
78
|
+
}, [required, selections.length, invalidSelectionMessage]);
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
const form = input.current?.form;
|
|
81
|
+
if (!form)
|
|
82
|
+
return;
|
|
83
|
+
const reset = (event) => queueMicrotask(() => {
|
|
84
|
+
if (event.defaultPrevented)
|
|
85
|
+
return;
|
|
86
|
+
if (current.current.value === undefined)
|
|
87
|
+
setLocal(initial.current);
|
|
88
|
+
setQuery(null);
|
|
89
|
+
setOpen(false);
|
|
90
|
+
setValidation("");
|
|
91
|
+
});
|
|
92
|
+
form.addEventListener("reset", reset);
|
|
93
|
+
return () => form.removeEventListener("reset", reset);
|
|
94
|
+
}, [props.form]);
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
const control = input.current, Observer = control?.ownerDocument.defaultView?.MutationObserver;
|
|
97
|
+
if (!control || !Observer)
|
|
98
|
+
return;
|
|
99
|
+
const observer = new Observer(() => { if (control.matches(":disabled")) {
|
|
100
|
+
setOpen(false);
|
|
101
|
+
setQuery(null);
|
|
102
|
+
} });
|
|
103
|
+
let parent = control.parentElement;
|
|
104
|
+
while (parent) {
|
|
105
|
+
if (parent.localName === "fieldset")
|
|
106
|
+
observer.observe(parent, { attributes: true, attributeFilter: ["disabled"] });
|
|
107
|
+
parent = parent.parentElement;
|
|
108
|
+
}
|
|
109
|
+
return () => observer.disconnect();
|
|
110
|
+
}, []);
|
|
111
|
+
useEffect(() => { if (disabled || readOnly || props.hidden) {
|
|
112
|
+
setOpen(false);
|
|
113
|
+
setQuery(null);
|
|
114
|
+
} }, [disabled, readOnly, props.hidden]);
|
|
115
|
+
useBrowserLayoutEffect(() => {
|
|
116
|
+
if (!open || !panel.current || !root.current || !input.current)
|
|
117
|
+
return;
|
|
118
|
+
const popup = panel.current, anchor = root.current, control = input.current;
|
|
119
|
+
const document = popup.ownerDocument, view = document.defaultView;
|
|
120
|
+
let alive = true, frame = 0, unregister = () => { };
|
|
121
|
+
const assign = (element, property, value) => { if (element.style.getPropertyValue(property) !== value)
|
|
122
|
+
element.style.setProperty(property, value); };
|
|
123
|
+
const update = () => {
|
|
124
|
+
if (!alive)
|
|
125
|
+
return;
|
|
126
|
+
if (!anchor.isConnected || !anchor.getClientRects().length || control.matches(":disabled")) {
|
|
127
|
+
setOpen(false);
|
|
128
|
+
setQuery(null);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const rect = anchor.getBoundingClientRect(), visual = view.visualViewport;
|
|
132
|
+
const rootSize = parseFloat(view.getComputedStyle(document.documentElement).fontSize);
|
|
133
|
+
const length = (name) => { const raw = view.getComputedStyle(control).getPropertyValue(name).trim(); return parseFloat(raw) * (raw.endsWith("rem") ? rootSize : 1); };
|
|
134
|
+
const gap = length("--flood-sys-space-2") || 0;
|
|
135
|
+
const width = visual?.width ?? view.innerWidth, height = visual?.height ?? view.innerHeight, top = visual?.offsetTop ?? 0;
|
|
136
|
+
const room = Math.max(top + height - rect.bottom - gap * 2, rect.top - top - gap * 2);
|
|
137
|
+
const maxHeight = Math.min(length("--flood-sys-control-md-height") * 8, Math.max(0, room));
|
|
138
|
+
assign(popup, "width", `${Math.min(rect.width, Math.max(0, width - gap * 2))}px`);
|
|
139
|
+
assign(popup, "max-height", `${maxHeight}px`);
|
|
140
|
+
assign(popup, "column-gap", "var(--flood-sys-space-2)");
|
|
141
|
+
const css = view.getComputedStyle(popup);
|
|
142
|
+
const inset = parseFloat(css.paddingTop) + parseFloat(css.paddingBottom) + parseFloat(css.borderTopWidth) + parseFloat(css.borderBottomWidth);
|
|
143
|
+
if (viewport.current)
|
|
144
|
+
assign(viewport.current, "max-height", `${Math.max(0, maxHeight - inset)}px`);
|
|
145
|
+
positionLayer(anchor, popup, "bottom", "start");
|
|
146
|
+
frame = view.requestAnimationFrame(update);
|
|
147
|
+
};
|
|
148
|
+
const start = () => {
|
|
149
|
+
if (!alive)
|
|
150
|
+
return;
|
|
151
|
+
const dialog = anchor.closest("dialog");
|
|
152
|
+
if (dialog && !dialog.open) {
|
|
153
|
+
frame = view.requestAnimationFrame(start);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (typeof popup.showPopover !== "function")
|
|
157
|
+
throw new Error("Flood Combobox requires the native Popover API");
|
|
158
|
+
popup.style.visibility = "hidden";
|
|
159
|
+
popup.showPopover();
|
|
160
|
+
update();
|
|
161
|
+
unregister = registerLayer(document, { panel: popup, anchor: () => anchor, close: () => { setOpen(false); setQuery(null); } });
|
|
162
|
+
};
|
|
163
|
+
const toggle = (event) => { if (alive && event.newState === "closed" && !popup.matches(":popover-open")) {
|
|
164
|
+
setOpen(false);
|
|
165
|
+
setQuery(null);
|
|
166
|
+
} };
|
|
167
|
+
popup.addEventListener("toggle", toggle);
|
|
168
|
+
start();
|
|
169
|
+
return () => { alive = false; view.cancelAnimationFrame(frame); unregister(); popup.removeEventListener("toggle", toggle); if (popup.matches(":popover-open"))
|
|
170
|
+
popup.hidePopover(); };
|
|
171
|
+
}, [open]);
|
|
172
|
+
useEffect(() => {
|
|
173
|
+
if (!open || !active || !viewport.current)
|
|
174
|
+
return;
|
|
175
|
+
const option = panel.current?.ownerDocument.getElementById(optionId(active.value));
|
|
176
|
+
if (!option)
|
|
177
|
+
return;
|
|
178
|
+
const area = viewport.current, a = option.getBoundingClientRect(), b = area.getBoundingClientRect();
|
|
179
|
+
if (a.top < b.top)
|
|
180
|
+
area.scrollTop -= b.top - a.top;
|
|
181
|
+
else if (a.bottom > b.bottom)
|
|
182
|
+
area.scrollTop += a.bottom - b.bottom;
|
|
183
|
+
}, [open, active?.value, query, options]);
|
|
184
|
+
const glyph = (clear) => _jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: clear ? _jsx("path", { d: "m6 6 12 12M18 6 6 18" }) : _jsx("path", { d: "m6 9 6 6 6-6" }) });
|
|
185
|
+
const inputProps = {
|
|
186
|
+
...props, id, type: "text", value: displayed, disabled, readOnly, required: required && (!multiple || !selections.length), controlSize, autoComplete,
|
|
187
|
+
role: "combobox", "aria-autocomplete": "list", "aria-haspopup": "listbox", "aria-expanded": open, "aria-controls": open ? menuId : undefined,
|
|
188
|
+
"aria-required": required || undefined, "aria-activedescendant": open && active ? optionId(active.value) : undefined,
|
|
189
|
+
"aria-describedby": mergeDescriptions(props["aria-describedby"], validation && validationId, multiple && Boolean(selections.length) && `${id}-selection`),
|
|
190
|
+
invalid: props.invalid || Boolean(validation),
|
|
191
|
+
onChange: event => {
|
|
192
|
+
if (inert())
|
|
193
|
+
return;
|
|
194
|
+
const text = event.currentTarget.value;
|
|
195
|
+
setQuery(text);
|
|
196
|
+
setActiveValue(undefined);
|
|
197
|
+
setOpen(true);
|
|
198
|
+
setValidation("");
|
|
199
|
+
if (!multiple && text === "" && selected)
|
|
200
|
+
publish([]);
|
|
201
|
+
},
|
|
202
|
+
onClick: event => { onClick?.(event); if (!event.defaultPrevented)
|
|
203
|
+
show(); },
|
|
204
|
+
onBlur: event => {
|
|
205
|
+
onBlur?.(event);
|
|
206
|
+
if (event.defaultPrevented || root.current?.contains(event.relatedTarget))
|
|
207
|
+
return;
|
|
208
|
+
close();
|
|
209
|
+
},
|
|
210
|
+
onInvalid: event => { onInvalid?.(event); if (!event.defaultPrevented)
|
|
211
|
+
setValidation(event.currentTarget.validationMessage); },
|
|
212
|
+
onKeyDown: event => {
|
|
213
|
+
onKeyDown?.(event);
|
|
214
|
+
if (event.defaultPrevented || event.altKey || event.ctrlKey || event.metaKey || event.nativeEvent.isComposing)
|
|
215
|
+
return;
|
|
216
|
+
// The composite owns Backspace; readonly selection must not become browser Back.
|
|
217
|
+
if (multiple && readOnly && event.key === "Backspace")
|
|
218
|
+
event.preventDefault();
|
|
219
|
+
if (inert())
|
|
220
|
+
return;
|
|
221
|
+
const rtl = event.currentTarget.ownerDocument.defaultView?.getComputedStyle(event.currentTarget).direction === "rtl";
|
|
222
|
+
if (multiple && event.key === "Backspace" && displayed === "") {
|
|
223
|
+
const last = [...selections].reverse().find(option => !option.disabled);
|
|
224
|
+
if (last) {
|
|
225
|
+
event.preventDefault();
|
|
226
|
+
remove(last.value);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
else if (multiple && event.key === (rtl ? "ArrowRight" : "ArrowLeft") && event.currentTarget.selectionStart === 0 && event.currentTarget.selectionEnd === 0) {
|
|
230
|
+
const buttons = root.current?.querySelectorAll(".flood-tag__remove:not(:disabled)");
|
|
231
|
+
if (buttons?.length) {
|
|
232
|
+
event.preventDefault();
|
|
233
|
+
buttons[buttons.length - 1].focus();
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
else if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
|
237
|
+
event.preventDefault();
|
|
238
|
+
if (!open) {
|
|
239
|
+
setActiveValue(event.key === "ArrowUp" ? enabled.at(-1)?.value : selected?.value ?? enabled[0]?.value);
|
|
240
|
+
setOpen(true);
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
const index = enabled.findIndex(option => option.value === active?.value);
|
|
244
|
+
const next = (index + (event.key === "ArrowDown" ? 1 : -1) + enabled.length) % enabled.length;
|
|
245
|
+
setActiveValue(enabled[next]?.value);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
else if (event.key === "Enter" && open) {
|
|
249
|
+
event.preventDefault();
|
|
250
|
+
if (active)
|
|
251
|
+
select(active.value);
|
|
252
|
+
}
|
|
253
|
+
else if (event.key === "Escape" && open) {
|
|
254
|
+
event.preventDefault();
|
|
255
|
+
event.stopPropagation();
|
|
256
|
+
close();
|
|
257
|
+
}
|
|
258
|
+
else if (event.key === "Tab")
|
|
259
|
+
close();
|
|
260
|
+
},
|
|
261
|
+
};
|
|
262
|
+
const removable = selections.some(option => !option.disabled);
|
|
263
|
+
const actions = _jsxs("span", { className: "flood-combobox__actions", children: [clearable && _jsx(IconButton, { label: clearLabel, variant: "ghost", size: controlSize, className: !selected ? "flood-combobox__clear-empty" : undefined, disabled: disabled || readOnly || !selected || (multiple && !removable), "aria-hidden": !selected || undefined, tabIndex: -1, onPointerDown: event => event.preventDefault(), onClick: () => select(""), children: glyph(true) }), _jsx(IconButton, { label: toggleLabel, variant: "ghost", size: controlSize, disabled: disabled || readOnly, tabIndex: -1, "aria-expanded": open, "aria-controls": open ? menuId : undefined, onPointerDown: event => event.preventDefault(), onClick: () => { if (open)
|
|
264
|
+
close();
|
|
265
|
+
else
|
|
266
|
+
show(); input.current?.focus({ preventScroll: true }); }, children: glyph(false) })] });
|
|
267
|
+
return _jsxs("div", { className: multiple ? "flood-combobox flood-multi-select" : "flood-combobox", ref: root, hidden: props.hidden, dir: props.dir, lang: props.lang, children: [multiple ? _jsxs("div", { className: ["flood-input-group", "flood-multi-select__control", `flood-control--${controlSize}`, className].filter(Boolean).join(" "), style: props.style, "data-disabled": disabled || undefined, "data-readonly": readOnly || undefined, "data-status": resolveFieldStatus(props.status, props.invalid || Boolean(validation), props["aria-invalid"]), onClick: event => { if (event.target === event.currentTarget && !inert()) {
|
|
268
|
+
input.current?.focus();
|
|
269
|
+
show();
|
|
270
|
+
} }, children: [selections.map(option => _jsx(Tag, { size: controlSize === "sm" ? "sm" : controlSize === "md" ? "md" : "lg", disabled: disabled || option.disabled, onRemove: readOnly ? undefined : () => remove(option.value), removeLabel: `${removeItemLabel} ${option.label}`, removeButtonProps: { tabIndex: -1, onKeyDown: event => {
|
|
271
|
+
if (event.defaultPrevented || event.altKey || event.ctrlKey || event.metaKey || event.nativeEvent.isComposing || inert())
|
|
272
|
+
return;
|
|
273
|
+
const rtl = event.currentTarget.ownerDocument.defaultView?.getComputedStyle(event.currentTarget).direction === "rtl";
|
|
274
|
+
const buttons = [...root.current.querySelectorAll(".flood-tag__remove:not(:disabled)")];
|
|
275
|
+
const index = buttons.indexOf(event.currentTarget);
|
|
276
|
+
if (event.key === "Backspace" || event.key === "Delete") {
|
|
277
|
+
event.preventDefault();
|
|
278
|
+
remove(option.value);
|
|
279
|
+
}
|
|
280
|
+
else if (event.key === "Escape") {
|
|
281
|
+
event.preventDefault();
|
|
282
|
+
event.stopPropagation();
|
|
283
|
+
close();
|
|
284
|
+
input.current?.focus();
|
|
285
|
+
}
|
|
286
|
+
else if (event.key === (rtl ? "ArrowLeft" : "ArrowRight")) {
|
|
287
|
+
event.preventDefault();
|
|
288
|
+
(buttons[index + 1] ?? input.current)?.focus();
|
|
289
|
+
}
|
|
290
|
+
else if (event.key === (rtl ? "ArrowRight" : "ArrowLeft")) {
|
|
291
|
+
event.preventDefault();
|
|
292
|
+
(buttons[Math.max(0, index - 1)])?.focus();
|
|
293
|
+
}
|
|
294
|
+
} }, children: option.label }, option.value)), _jsxs("div", { className: "flood-multi-select__editor", children: [_jsx(Input, { ...inputProps, ref: input, style: undefined }), actions] })] }) : _jsx(InputGroup, { ...inputProps, ref: input, className: ["flood-combobox__control", className].filter(Boolean).join(" "), end: actions }), (multiple ? selections : [selected]).map((option, index) => _jsx("input", { type: "hidden", name: name, value: option?.value ?? "", disabled: disabled, form: props.form }, option?.value ?? index)), multiple && Boolean(selections.length) && _jsx("span", { id: `${id}-selection`, className: "flood-visually-hidden", children: selections.map(option => option.label).join(", ") }), open && _jsx("div", { ref: panel, popover: "manual", className: "flood-select__menu flood-combobox__menu", onPointerDown: event => { if (event.pointerType === "mouse")
|
|
295
|
+
event.preventDefault(); }, children: _jsxs(ScrollArea, { ref: viewport, id: menuId, role: "listbox", "aria-label": listLabel, "aria-multiselectable": multiple || undefined, orientation: "vertical", tabIndex: -1, className: "flood-select__viewport", children: [!filtered.length && _jsx("div", { className: "flood-select__empty", role: "option", "aria-disabled": "true", "aria-selected": "false", children: emptyMessage }), filtered.map(option => _jsxs("div", { id: optionId(option.value), role: "option", "aria-selected": selectedIds.includes(option.value), "aria-disabled": option.disabled || undefined, className: "flood-select__option", "data-active": option.value === active?.value || undefined, onPointerMove: event => { if (event.pointerType === "mouse" && !option.disabled)
|
|
296
|
+
setActiveValue(option.value); }, onPointerDown: event => {
|
|
297
|
+
// Touch must retain its compatibility click and native scrolling.
|
|
298
|
+
if (event.pointerType === "mouse")
|
|
299
|
+
event.preventDefault();
|
|
300
|
+
}, onMouseDown: event => event.preventDefault(), onClick: () => select(option.value), children: [_jsxs("span", { className: "flood-combobox__option-text", children: [_jsx("span", { children: option.label }), option.description && _jsx("span", { className: "flood-combobox__description", children: option.description })] }), _jsx("span", { className: "flood-select__selected", "aria-hidden": "true", children: selectedIds.includes(option.value) ? "✓" : "" })] }, option.value))] }) }), open && !filtered.length && _jsx("span", { className: "flood-visually-hidden", role: "status", children: emptyMessage }), validation && _jsx("span", { id: validationId, className: "flood-select__validation", role: "alert", children: validation })] });
|
|
301
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { AvatarGroupProps, StatusIndicatorProps, FigureProps, BlockquoteProps, InlineCodeProps, MetricProps, TimelineProps } from "./index.js";
|
|
2
|
+
export declare const GroupedAvatars: import("react").ForwardRefExoticComponent<AvatarGroupProps & import("react").RefAttributes<HTMLUListElement>>;
|
|
3
|
+
export declare const NamedStatus: import("react").ForwardRefExoticComponent<StatusIndicatorProps & import("react").RefAttributes<HTMLSpanElement>>;
|
|
4
|
+
export declare const CaptionedFigure: import("react").ForwardRefExoticComponent<FigureProps & import("react").RefAttributes<HTMLElement>>;
|
|
5
|
+
export declare const AttributedQuote: import("react").ForwardRefExoticComponent<BlockquoteProps & import("react").RefAttributes<HTMLQuoteElement>>;
|
|
6
|
+
export declare const InlineCodeText: import("react").ForwardRefExoticComponent<InlineCodeProps & import("react").RefAttributes<HTMLElement>>;
|
|
7
|
+
export declare const MeasuredValue: import("react").ForwardRefExoticComponent<MetricProps & import("react").RefAttributes<HTMLDListElement>>;
|
|
8
|
+
export declare const EventTimeline: import("react").ForwardRefExoticComponent<TimelineProps & import("react").RefAttributes<HTMLOListElement>>;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef } from "react";
|
|
3
|
+
import { Avatar, VisuallyHidden } from "./index.js";
|
|
4
|
+
const cx = (...parts) => parts.filter(Boolean).join(" ");
|
|
5
|
+
export const GroupedAvatars = forwardRef(function GroupedAvatars({ items, label, max = 4, size = "md", overflowLabel, className, ...props }, ref) {
|
|
6
|
+
if (!Number.isInteger(max) || max < 1)
|
|
7
|
+
throw new Error("Flood AvatarGroup max must be a positive integer");
|
|
8
|
+
if (!label.trim() || items.some(item => !item.id.trim() || !item.name.trim()) || new Set(items.map(item => item.id)).size !== items.length)
|
|
9
|
+
throw new Error("Flood AvatarGroup needs a label and unique nonempty item IDs/names");
|
|
10
|
+
const visible = items.slice(0, max), remaining = items.length - visible.length;
|
|
11
|
+
if (remaining > 0 && overflowLabel !== undefined && !overflowLabel.trim())
|
|
12
|
+
throw new Error("Flood AvatarGroup overflowLabel must be nonempty");
|
|
13
|
+
return _jsxs("ul", { ...props, ref: ref, "aria-label": label, className: cx("flood-avatar-group", className), "data-size": size, children: [visible.map(item => _jsx("li", { children: _jsx(Avatar, { name: item.name, src: item.src, fallback: item.fallback, size: size }) }, item.id)), remaining > 0 && _jsx("li", { children: _jsxs("span", { className: "flood-avatar flood-avatar-group__overflow", "data-size": size, role: "img", "aria-label": overflowLabel ?? `${remaining} more people`, children: ["+", remaining] }) })] });
|
|
14
|
+
});
|
|
15
|
+
export const NamedStatus = forwardRef(function NamedStatus({ label, tone = "neutral", size = "md", announce = false, className, role, ...props }, ref) {
|
|
16
|
+
if (!label.trim())
|
|
17
|
+
throw new Error("Flood StatusIndicator requires a visible label");
|
|
18
|
+
return _jsxs("span", { ...props, ref: ref, role: announce ? "status" : role, className: cx("flood-status-indicator", className), "data-tone": tone, "data-size": size, children: [_jsx("span", { className: "flood-status-indicator__dot", "aria-hidden": "true" }), label] });
|
|
19
|
+
});
|
|
20
|
+
export const CaptionedFigure = forwardRef(function CaptionedFigure({ caption, captionPosition = "after", children, className, ...props }, ref) {
|
|
21
|
+
const content = caption !== undefined && caption !== null && caption !== false ? _jsx("figcaption", { className: "flood-figure__caption", children: caption }) : null;
|
|
22
|
+
return _jsxs("figure", { ...props, ref: ref, className: cx("flood-figure", className), children: [captionPosition === "before" && content, children, captionPosition === "after" && content] });
|
|
23
|
+
});
|
|
24
|
+
export const AttributedQuote = forwardRef(function AttributedQuote({ attribution, children, className, ...props }, ref) {
|
|
25
|
+
return _jsxs("blockquote", { ...props, ref: ref, className: cx("flood-blockquote", className), children: [_jsx("div", { className: "flood-blockquote__quote", children: children }), attribution !== undefined && attribution !== null && attribution !== false && _jsx("footer", { className: "flood-blockquote__attribution", children: attribution })] });
|
|
26
|
+
});
|
|
27
|
+
export const InlineCodeText = forwardRef(function InlineCodeText({ size = "md", wrap = true, className, ...props }, ref) {
|
|
28
|
+
return _jsx("code", { ...props, ref: ref, className: cx("flood-inline-code", className), "data-size": size, "data-wrap": wrap });
|
|
29
|
+
});
|
|
30
|
+
export const MeasuredValue = forwardRef(function MeasuredValue({ label, value, hint, trend, size = "md", className, ...props }, ref) {
|
|
31
|
+
if (!label.trim() || (trend && !trend.label.trim()))
|
|
32
|
+
throw new Error("Flood Metric requires a label and meaningful trend text");
|
|
33
|
+
return _jsxs("dl", { ...props, ref: ref, className: cx("flood-metric", className), "data-size": size, children: [_jsx("dt", { className: "flood-metric__label", children: label }), _jsx("dd", { className: "flood-metric__value", children: value }), trend && _jsxs("dd", { className: "flood-metric__trend", "data-tone": trend.tone ?? "neutral", children: [_jsx("span", { "aria-hidden": "true", children: trend.direction === "up" ? "↑" : trend.direction === "down" ? "↓" : "→" }), trend.label] }), hint !== undefined && hint !== null && hint !== false && _jsx("dd", { className: "flood-metric__hint", children: hint })] });
|
|
34
|
+
});
|
|
35
|
+
export const EventTimeline = forwardRef(function EventTimeline({ items, label, stateLabels, className, ...props }, ref) {
|
|
36
|
+
if (!label.trim() || items.some(item => !item.id.trim() || !item.title.trim()) || new Set(items.map(item => item.id)).size !== items.length)
|
|
37
|
+
throw new Error("Flood Timeline needs a label and unique nonempty item IDs/titles");
|
|
38
|
+
if (items.filter(item => item.state === "current").length > 1)
|
|
39
|
+
throw new Error("Flood Timeline has at most one current event");
|
|
40
|
+
const labels = { complete: stateLabels?.complete ?? "Complete", current: stateLabels?.current ?? "Current", upcoming: stateLabels?.upcoming ?? "Upcoming" };
|
|
41
|
+
if (Object.values(labels).some(value => !value.trim()))
|
|
42
|
+
throw new Error("Flood Timeline state labels must be nonempty");
|
|
43
|
+
return _jsx("ol", { ...props, ref: ref, "aria-label": label, className: cx("flood-timeline", className), children: items.map(item => {
|
|
44
|
+
const state = item.state ?? "upcoming";
|
|
45
|
+
return _jsxs("li", { className: "flood-timeline__item", "data-state": state, "aria-current": state === "current" ? "step" : undefined, children: [_jsx("span", { className: "flood-timeline__marker", "aria-hidden": "true", children: item.icon ?? (state === "complete" ? _jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("path", { d: "m6 12 4 4 8-8" }) }) : state === "current" ? _jsx("span", {}) : null) }), _jsxs("div", { className: "flood-timeline__content", children: [_jsxs("div", { className: "flood-timeline__title", children: [_jsxs(VisuallyHidden, { children: [labels[state], ": "] }), item.title] }), item.description !== undefined && item.description !== null && _jsx("div", { className: "flood-timeline__description", children: item.description }), item.timeLabel && (item.dateTime ? _jsx("time", { className: "flood-timeline__time", dateTime: item.dateTime, children: item.timeLabel }) : _jsx("span", { className: "flood-timeline__time", children: item.timeLabel }))] })] }, item.id);
|
|
46
|
+
}) });
|
|
47
|
+
});
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { DescriptionListProps, TableProps } from "./index.js";
|
|
2
|
+
export declare const DescribedData: import("react").ForwardRefExoticComponent<DescriptionListProps & import("react").RefAttributes<HTMLDListElement>>;
|
|
3
|
+
export declare const TabularData: import("react").ForwardRefExoticComponent<TableProps & import("react").RefAttributes<HTMLTableElement>>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef } from "react";
|
|
3
|
+
import { ScrollArea } from "./index.js";
|
|
4
|
+
export const DescribedData = forwardRef(function DescribedData({ items, layout, density, dividers, className, ...props }, ref) {
|
|
5
|
+
if (items.some(item => !item.id.trim()) || new Set(items.map(item => item.id)).size !== items.length)
|
|
6
|
+
throw new Error("Flood description items require unique nonempty IDs");
|
|
7
|
+
return _jsx("dl", { ...props, ref: ref, className: ["flood-description-list", className].filter(Boolean).join(" "), "data-layout": layout, "data-density": density, "data-dividers": dividers, children: items.map(item => _jsxs("div", { className: "flood-description-list__item", children: [_jsx("dt", { children: item.term }), _jsx("dd", { children: item.description })] }, item.id)) });
|
|
8
|
+
});
|
|
9
|
+
export const TabularData = forwardRef(function TabularData({ caption, children, hideCaption, density, striped, scrollLabel, className, hidden, dir, lang, ...props }, ref) {
|
|
10
|
+
return _jsx("div", { className: "flood-table-container", hidden: hidden, dir: dir, lang: lang, children: _jsx(ScrollArea, { orientation: "horizontal", "aria-label": scrollLabel ?? caption, children: _jsxs("table", { ...props, ref: ref, hidden: hidden, dir: dir, lang: lang, className: ["flood-table", className].filter(Boolean).join(" "), "data-density": density, "data-striped": striped, children: [_jsx("caption", { className: hideCaption ? "flood-visually-hidden" : undefined, children: caption }), children] }) }) });
|
|
11
|
+
});
|
package/dist/disclosure.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { createElement, forwardRef, useEffect, useId, useLayoutEffect, useRef, useImperativeHandle } from "react";
|
|
3
|
+
import { Button } from "./index.js";
|
|
3
4
|
const useBrowserLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
|
|
4
5
|
/** Shared original disclosure anatomy. The closed panel stays mounted but hidden. */
|
|
5
6
|
export const Disclosure = forwardRef(function Disclosure({ title, expanded, onExpandedChange, disabled, controlSize, headingLevel, className, children, id: explicitId, ...props }, ref) {
|
|
@@ -14,7 +15,7 @@ export const Disclosure = forwardRef(function Disclosure({ title, expanded, onEx
|
|
|
14
15
|
if (!expanded)
|
|
15
16
|
focusedInside.current = false;
|
|
16
17
|
}, [expanded, disabled]);
|
|
17
|
-
const button = _jsxs(
|
|
18
|
+
const button = _jsxs(Button, { ref: trigger, variant: "ghost", size: controlSize, id: `${id}-trigger`, type: "button", disabled: disabled, "aria-expanded": expanded, "aria-controls": `${id}-panel`, className: "flood-disclosure__trigger", onClick: () => onExpandedChange(!expanded), children: [_jsx("span", { className: "flood-disclosure__title", children: title }), _jsx("span", { className: "flood-disclosure__indicator", "aria-hidden": "true" })] });
|
|
18
19
|
return _jsxs("div", { ...props, id: id, ref: root, "data-open": expanded, className: ["flood-disclosure", `flood-control--${controlSize}`, className].filter(Boolean).join(" "), children: [createElement(headingLevel ? `h${headingLevel}` : "div", { className: "flood-disclosure__heading" }, button), _jsx("div", { ref: panel, id: `${id}-panel`, className: "flood-disclosure__panel", hidden: !expanded, onFocusCapture: () => { focusedInside.current = true; }, onBlurCapture: event => { if (event.relatedTarget && !event.currentTarget.contains(event.relatedTarget))
|
|
19
20
|
focusedInside.current = false; }, children: children })] });
|
|
20
21
|
});
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { CopyButtonProps, ToastRegionProps } from "./index.js";
|
|
2
|
+
export declare const ClipboardAction: import("react").ForwardRefExoticComponent<CopyButtonProps & import("react").RefAttributes<HTMLButtonElement>>;
|
|
3
|
+
export declare const NotificationRegion: import("react").ForwardRefExoticComponent<ToastRegionProps & import("react").RefAttributes<HTMLDivElement>>;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from "react";
|
|
3
|
+
import { Button, IconButton, IconSlot, Spinner, VisuallyHidden } from "./index.js";
|
|
4
|
+
export const ClipboardAction = forwardRef(function ClipboardAction({ value, copyLabel = "Copy", copiedLabel = "Copied", errorLabel = "Failed", resetDelay = 2000, onCopied, onCopyError, className, disabled, size = "md", variant = "subtle", "aria-label": accessibleLabel, ...props }, ref) {
|
|
5
|
+
if (!Number.isFinite(resetDelay) || resetDelay < 0)
|
|
6
|
+
throw new Error("Flood CopyButton resetDelay must be finite and nonnegative");
|
|
7
|
+
const [phase, setPhase] = useState("idle");
|
|
8
|
+
const [pending, setPending] = useState(false);
|
|
9
|
+
const timer = useRef(undefined);
|
|
10
|
+
const alive = useRef(true);
|
|
11
|
+
useEffect(() => { alive.current = true; return () => { alive.current = false; clearTimeout(timer.current); }; }, []);
|
|
12
|
+
const label = phase === "copied" ? copiedLabel : phase === "error" ? errorLabel : copyLabel;
|
|
13
|
+
const copy = async (node) => {
|
|
14
|
+
if (pending || node.matches(":disabled"))
|
|
15
|
+
return;
|
|
16
|
+
clearTimeout(timer.current);
|
|
17
|
+
setPending(true);
|
|
18
|
+
const view = node.ownerDocument.defaultView;
|
|
19
|
+
try {
|
|
20
|
+
if (!view?.navigator.clipboard?.writeText)
|
|
21
|
+
throw new Error("Clipboard writing is unavailable in this context");
|
|
22
|
+
await view.navigator.clipboard.writeText(value);
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
if (alive.current) {
|
|
26
|
+
setPending(false);
|
|
27
|
+
setPhase("error");
|
|
28
|
+
}
|
|
29
|
+
onCopyError?.(error);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (alive.current) {
|
|
33
|
+
setPending(false);
|
|
34
|
+
setPhase("copied");
|
|
35
|
+
clearTimeout(timer.current);
|
|
36
|
+
timer.current = setTimeout(() => { if (alive.current)
|
|
37
|
+
setPhase("idle"); }, resetDelay);
|
|
38
|
+
}
|
|
39
|
+
onCopied?.();
|
|
40
|
+
};
|
|
41
|
+
return _jsxs(_Fragment, { children: [_jsx(Button, { ...props, ref: ref, type: "button", size: size, variant: variant, disabled: disabled || pending, "aria-label": accessibleLabel ? (phase === "idle" ? accessibleLabel : `${label}: ${accessibleLabel}`) : label, className: ["flood-copy-button", className].filter(Boolean).join(" "), onClick: event => { void copy(event.currentTarget); }, children: _jsx("span", { className: "flood-copy-button__labels", "aria-hidden": "true", children: [copyLabel, copiedLabel, errorLabel].map((text, index) => _jsxs("span", { "data-visible": index === (phase === "copied" ? 1 : phase === "error" ? 2 : 0) || undefined, children: [_jsx(IconSlot, { size: size, children: pending ? _jsx(Spinner, { decorative: true }) : _jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round", children: index === 1 ? _jsx("path", { d: "m5 12 4 4L19 6" }) : index === 2 ? _jsx("path", { d: "m6 6 12 12M18 6 6 18" }) : _jsxs(_Fragment, { children: [_jsx("rect", { x: "8", y: "8", width: "12", height: "12", rx: "2" }), _jsx("path", { d: "M15 8V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h3" })] }) }) }), _jsx("span", { children: text })] }, index)) }) }), _jsx(VisuallyHidden, { role: "status", children: phase === "idle" ? "" : label })] });
|
|
42
|
+
});
|
|
43
|
+
function TimedToast({ item, closeLabel, onDismiss }) {
|
|
44
|
+
const duration = item.duration ?? (item.action || item.status === "error" ? 0 : 5000);
|
|
45
|
+
if (!Number.isFinite(duration) || duration < 0)
|
|
46
|
+
throw new Error("Flood toast duration must be finite and nonnegative");
|
|
47
|
+
const [hover, setHover] = useState(false), [focus, setFocus] = useState(false), [hidden, setHidden] = useState(false);
|
|
48
|
+
const node = useRef(null), remaining = useRef(duration), callback = useRef(onDismiss), requested = useRef(false);
|
|
49
|
+
useEffect(() => { callback.current = onDismiss; }, [onDismiss]);
|
|
50
|
+
useEffect(() => { remaining.current = duration; requested.current = false; }, [duration]);
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
const document = node.current?.ownerDocument;
|
|
53
|
+
if (!document)
|
|
54
|
+
return;
|
|
55
|
+
const update = () => setHidden(document.hidden);
|
|
56
|
+
update();
|
|
57
|
+
document.addEventListener("visibilitychange", update);
|
|
58
|
+
return () => document.removeEventListener("visibilitychange", update);
|
|
59
|
+
}, []);
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
if (!duration || hover || focus || hidden || requested.current)
|
|
62
|
+
return;
|
|
63
|
+
const started = Date.now();
|
|
64
|
+
const timer = setTimeout(() => { if (requested.current)
|
|
65
|
+
return; requested.current = true; callback.current(item.id); }, remaining.current);
|
|
66
|
+
return () => { clearTimeout(timer); remaining.current = Math.max(0, remaining.current - (Date.now() - started)); };
|
|
67
|
+
}, [duration, hover, focus, hidden, item.id]);
|
|
68
|
+
const dismiss = () => {
|
|
69
|
+
requested.current = true;
|
|
70
|
+
const document = node.current?.ownerDocument;
|
|
71
|
+
if (node.current?.contains(document?.activeElement ?? null)) {
|
|
72
|
+
const region = node.current?.closest(".flood-toast-region");
|
|
73
|
+
const sibling = node.current?.nextElementSibling ?? node.current?.previousElementSibling;
|
|
74
|
+
(sibling?.querySelector("button:not(:disabled)") ?? region)?.focus({ preventScroll: true });
|
|
75
|
+
}
|
|
76
|
+
onDismiss(item.id);
|
|
77
|
+
};
|
|
78
|
+
return _jsxs("li", { ref: node, className: "flood-toast", "data-status": item.status ?? "neutral", onPointerEnter: () => setHover(true), onPointerLeave: () => setHover(false), onFocusCapture: () => setFocus(true), onBlurCapture: event => { if (!event.currentTarget.contains(event.relatedTarget))
|
|
79
|
+
setFocus(false); }, children: [_jsxs("div", { className: "flood-toast__message", role: item.status === "error" ? "alert" : "status", "aria-atomic": "true", children: [_jsx("div", { className: "flood-toast__title", children: item.title }), item.description && _jsx("div", { className: "flood-toast__description", children: item.description })] }), _jsx(IconButton, { label: `${closeLabel}: ${item.title}`, size: "sm", variant: "ghost", className: "flood-toast__close", onClick: dismiss, children: _jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", children: _jsx("path", { d: "m6 6 12 12M18 6 6 18" }) }) }), item.action && _jsx(Button, { size: "sm", variant: "subtle", className: "flood-toast__action", onClick: item.action.onClick, children: item.action.label })] });
|
|
80
|
+
}
|
|
81
|
+
export const NotificationRegion = forwardRef(function NotificationRegion({ items, onDismiss, label = "Notifications", closeLabel = "Dismiss notification", placement = "bottom-end", limit = 3, inline = false, hotkey = true, className, ...props }, forwardedRef) {
|
|
82
|
+
if (!Number.isInteger(limit) || limit < 1)
|
|
83
|
+
throw new Error("Flood ToastRegion limit must be a positive integer");
|
|
84
|
+
const ids = new Set();
|
|
85
|
+
for (const item of items) {
|
|
86
|
+
if (!item.id.trim() || ids.has(item.id) || !item.title.trim())
|
|
87
|
+
throw new Error("Flood toasts require unique nonempty IDs and titles");
|
|
88
|
+
ids.add(item.id);
|
|
89
|
+
}
|
|
90
|
+
const node = useRef(null);
|
|
91
|
+
useImperativeHandle(forwardedRef, () => node.current, []);
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
const element = node.current, document = element?.ownerDocument;
|
|
94
|
+
if (!hotkey || !element || !document)
|
|
95
|
+
return;
|
|
96
|
+
const key = (event) => {
|
|
97
|
+
if (event.defaultPrevented || event.key !== "F8" || event.altKey || event.ctrlKey || event.metaKey || !element.querySelector(".flood-toast") || element.hidden || Array.from(document.querySelectorAll(".flood-toast-region")).find(region => !region.hidden && region.getClientRects().length > 0 && region.querySelector(".flood-toast")) !== element)
|
|
98
|
+
return;
|
|
99
|
+
event.preventDefault();
|
|
100
|
+
(element.querySelector('button:not(:disabled)') ?? element).focus({ preventScroll: true });
|
|
101
|
+
};
|
|
102
|
+
document.addEventListener("keydown", key);
|
|
103
|
+
return () => document.removeEventListener("keydown", key);
|
|
104
|
+
}, [hotkey]);
|
|
105
|
+
return _jsx("div", { ...props, ref: node, role: "region", "aria-label": label, tabIndex: -1, className: ["flood-toast-region", className].filter(Boolean).join(" "), "data-placement": placement, "data-inline": inline || undefined, children: _jsx("ol", { className: "flood-toast-region__list", children: items.slice(0, limit).map(item => _jsx(TimedToast, { item: item, closeLabel: closeLabel, onDismiss: onDismiss }, item.id)) }) });
|
|
106
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { AlertProps, ProgressProps } from "./index.js";
|
|
2
|
+
export declare function validateRange(value: number | undefined, min: number, max: number, allowUnknown: boolean): void;
|
|
3
|
+
export declare const ValueIndicator: import("react").ForwardRefExoticComponent<ProgressProps & {
|
|
4
|
+
kind: "progress" | "meter";
|
|
5
|
+
min: number;
|
|
6
|
+
max: number;
|
|
7
|
+
} & import("react").RefAttributes<HTMLDivElement>>;
|
|
8
|
+
export declare const FeedbackMessage: import("react").ForwardRefExoticComponent<AlertProps & import("react").RefAttributes<HTMLDivElement>>;
|
package/dist/feedback.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef, useId } from "react";
|
|
3
|
+
import { VisuallyHidden } from "./index.js";
|
|
4
|
+
export function validateRange(value, min, max, allowUnknown) {
|
|
5
|
+
if (!Number.isFinite(min) || !Number.isFinite(max) || max <= min || !Number.isFinite(max - min))
|
|
6
|
+
throw new RangeError("Flood indicators require a finite increasing range");
|
|
7
|
+
if (value === undefined && allowUnknown)
|
|
8
|
+
return;
|
|
9
|
+
if (value === undefined || !Number.isFinite(value) || value < min || value > max)
|
|
10
|
+
throw new RangeError("Flood indicator value must be finite and within its range");
|
|
11
|
+
}
|
|
12
|
+
export const ValueIndicator = forwardRef(function ValueIndicator({ kind, label, value, min, max, valueText, size, tone, hideLabel, className, ...props }, ref) {
|
|
13
|
+
const labelId = `flood-indicator-${useId()}`;
|
|
14
|
+
validateRange(value, min, max, kind === "progress");
|
|
15
|
+
const unknown = value === undefined;
|
|
16
|
+
const percent = unknown ? undefined : ((value - min) / (max - min)) * 100;
|
|
17
|
+
const caption = _jsxs(_Fragment, { children: [_jsx("span", { id: labelId, children: label }), valueText && _jsx("span", { className: "flood-indicator__value", "aria-hidden": "true", children: valueText })] });
|
|
18
|
+
return _jsxs("div", { ...props, ref: ref, className: ["flood-indicator", className].filter(Boolean).join(" "), "data-kind": kind, "data-size": size, "data-tone": tone, "data-indeterminate": unknown, children: [hideLabel ? _jsx(VisuallyHidden, { as: "div", children: caption }) : _jsx("div", { className: "flood-indicator__caption", children: caption }), _jsx("div", { className: "flood-indicator__track", role: kind === "meter" ? "meter" : "progressbar", "aria-labelledby": labelId, "aria-valuemin": min, "aria-valuemax": max, "aria-valuenow": value, "aria-valuetext": valueText, children: _jsx("span", { className: "flood-indicator__fill", style: { inlineSize: percent === undefined ? undefined : `${percent}%` } }) })] });
|
|
19
|
+
});
|
|
20
|
+
function StatusGlyph({ tone }) {
|
|
21
|
+
return _jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round", focusable: "false", children: tone === "success" ? _jsxs(_Fragment, { children: [_jsx("circle", { cx: "12", cy: "12", r: "9" }), _jsx("path", { d: "m8 12 3 3 5-6" })] }) :
|
|
22
|
+
tone === "warning" ? _jsxs(_Fragment, { children: [_jsx("path", { d: "m12 3 10 17H2L12 3Z" }), _jsx("path", { d: "M12 9v4m0 3h.01" })] }) :
|
|
23
|
+
tone === "error" ? _jsxs(_Fragment, { children: [_jsx("circle", { cx: "12", cy: "12", r: "9" }), _jsx("path", { d: "m9 9 6 6m0-6-6 6" })] }) :
|
|
24
|
+
_jsxs(_Fragment, { children: [_jsx("circle", { cx: "12", cy: "12", r: "9" }), _jsx("path", { d: "M12 11v5m0-9h.01" })] }) });
|
|
25
|
+
}
|
|
26
|
+
export const FeedbackMessage = forwardRef(function FeedbackMessage({ title, tone, icon, announcement, children, className, ...props }, ref) {
|
|
27
|
+
const artwork = icon === undefined ? _jsx(StatusGlyph, { tone: tone }) : icon;
|
|
28
|
+
return _jsxs("div", { ...props, ref: ref, className: ["flood-alert", className].filter(Boolean).join(" "), "data-tone": tone, role: announcement === "assertive" ? "alert" : announcement === "polite" ? "status" : undefined, "aria-atomic": announcement === "off" ? undefined : true, children: [artwork != null && _jsx("span", { className: "flood-alert__icon", "aria-hidden": "true", children: artwork }), _jsxs("div", { className: "flood-alert__content", children: [title && _jsx("p", { className: "flood-alert__title", children: title }), children != null && _jsx("div", { className: "flood-alert__body", children: children })] })] });
|
|
29
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { FileInputProps, IconButton, IconSlot } from "./index.js";
|
|
2
|
+
interface Props extends FileInputProps {
|
|
3
|
+
actionComponent: typeof IconButton;
|
|
4
|
+
iconComponent: typeof IconSlot;
|
|
5
|
+
}
|
|
6
|
+
/** FileList and native form submission stay owned by the browser. */
|
|
7
|
+
export declare const FilePicker: import("react").ForwardRefExoticComponent<Props & import("react").RefAttributes<HTMLInputElement>>;
|
|
8
|
+
export {};
|