@flood-md/react 0.2.10 → 0.2.12
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 +275 -5
- package/dist/app-shell.d.ts +3 -0
- package/dist/app-shell.js +66 -0
- package/dist/bulk-action-bar.d.ts +3 -0
- package/dist/bulk-action-bar.js +17 -0
- package/dist/collection-toolbar.d.ts +3 -0
- package/dist/collection-toolbar.js +26 -0
- package/dist/collection-view-switcher.d.ts +3 -0
- package/dist/collection-view-switcher.js +19 -0
- package/dist/combobox.d.ts +6 -1
- package/dist/combobox.js +77 -29
- package/dist/command-list.d.ts +5 -1
- package/dist/command-list.js +2 -2
- package/dist/data-display.js +28 -4
- package/dist/data-table-columns.d.ts +25 -0
- package/dist/data-table-columns.js +271 -0
- package/dist/data-table-expansion.d.ts +21 -0
- package/dist/data-table-expansion.js +79 -0
- package/dist/data-table.js +140 -12
- package/dist/date-picker.js +2 -2
- package/dist/date-range-picker.js +2 -2
- package/dist/date-time-picker.js +2 -2
- package/dist/date-time-range-picker.js +2 -2
- package/dist/disclosure.d.ts +2 -1
- package/dist/disclosure.js +4 -4
- package/dist/editable.js +57 -18
- package/dist/filter-condition.d.ts +3 -0
- package/dist/filter-condition.js +21 -0
- package/dist/filter-group.d.ts +2 -0
- package/dist/filter-group.js +62 -0
- package/dist/foundations.css +1131 -93
- package/dist/index.d.ts +469 -5
- package/dist/index.js +72 -9
- package/dist/menu.js +5 -4
- package/dist/menubar.d.ts +2 -0
- package/dist/menubar.js +85 -0
- package/dist/navigation-group.d.ts +3 -0
- package/dist/navigation-group.js +13 -0
- package/dist/navigation-item.d.ts +3 -0
- package/dist/navigation-item.js +68 -0
- package/dist/navigation-layout.d.ts +2 -1
- package/dist/navigation-layout.js +12 -4
- package/dist/navigation-tree.js +51 -7
- package/dist/navigation.js +2 -2
- package/dist/overflow-actions.d.ts +2 -0
- package/dist/overflow-actions.js +75 -0
- package/dist/overlay-runtime.d.ts +6 -2
- package/dist/overlay-runtime.js +41 -5
- package/dist/overlays.d.ts +1 -0
- package/dist/overlays.js +34 -18
- package/dist/select.js +4 -1
- package/dist/sidebar.d.ts +2 -0
- package/dist/sidebar.js +58 -0
- package/dist/sparkline.d.ts +2 -0
- package/dist/sparkline.js +65 -0
- package/dist/toolbar.js +2 -2
- package/package.json +2 -2
package/dist/combobox.js
CHANGED
|
@@ -1,39 +1,68 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { forwardRef, useEffect, useId, useImperativeHandle, useLayoutEffect, useRef, useState } from "react";
|
|
3
|
-
import { Input, InputGroup, IconButton, ScrollArea, Tag } from "./index.js";
|
|
3
|
+
import { Input, InputGroup, IconButton, ScrollArea, Tag, Button } from "./index.js";
|
|
4
4
|
import { mergeDescriptions, resolveFieldStatus } from "./field-state.js";
|
|
5
5
|
import { positionLayer, registerLayer } from "./overlay-runtime.js";
|
|
6
6
|
const useBrowserLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
|
|
7
7
|
const searchable = (text) => text.normalize("NFKD").replace(/\p{M}/gu, "").toLowerCase();
|
|
8
8
|
/** Shared search/layer model. Public single and multiple value contracts stay distinct. */
|
|
9
|
-
export const SearchSelection = /* @__PURE__ */ 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) {
|
|
9
|
+
export const SearchSelection = /* @__PURE__ */ forwardRef(function SearchSelection({ options, selectedOptions = [], query: controlledQuery, onQueryChange, filtering = "local", optionsQuery, loadState = "ready", loadingMessage = "Loading options…", errorMessage = "Could not load options.", onRetry, retryLabel = "Try again", freeText = false, textValue, initialText = "", onTextChange, onOptionSelect, 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
10
|
if (new Set(options.map(option => option.value)).size !== options.length || options.some(option => !option.value || !option.label.trim()))
|
|
11
11
|
throw new Error("Flood Combobox options need unique nonempty values and labels");
|
|
12
|
-
if (![emptyMessage, listLabel, toggleLabel, clearLabel, invalidSelectionMessage].every(text => text.trim()))
|
|
12
|
+
if (![emptyMessage, listLabel, toggleLabel, clearLabel, invalidSelectionMessage, loadingMessage, errorMessage, retryLabel].every(text => text.trim()))
|
|
13
13
|
throw new Error("Flood Combobox messages and action labels must be nonempty");
|
|
14
|
+
if (new Set(selectedOptions.map(option => option.value)).size !== selectedOptions.length || selectedOptions.some(option => !option.value || !option.label.trim()))
|
|
15
|
+
throw new Error("Flood selectedOptions requires unique named records");
|
|
14
16
|
for (const values of [defaultValue, value])
|
|
15
17
|
if (values !== undefined && (!Array.isArray(values) || new Set(values).size !== values.length || values.some(item => typeof item !== "string" || !item)))
|
|
16
18
|
throw new Error("Flood selection values must be unique nonempty strings");
|
|
17
19
|
if (multiple && !removeItemLabel.trim())
|
|
18
20
|
throw new Error("Flood MultiSelect requires an accessible removal label");
|
|
21
|
+
if (filtering === "manual" && ((freeText ? textValue : controlledQuery) === undefined || optionsQuery === undefined))
|
|
22
|
+
throw new Error("Flood manual search requires controlled text/query and optionsQuery");
|
|
19
23
|
const generatedId = useId(), id = explicitId ?? `flood-combobox-${generatedId}`;
|
|
20
24
|
const menuId = `${id}-options`, validationId = `${id}-validation`;
|
|
21
25
|
const input = useRef(null), root = useRef(null), panel = useRef(null), viewport = useRef(null);
|
|
22
26
|
useImperativeHandle(forwardedRef, () => input.current, []);
|
|
23
|
-
const initial = useRef([...defaultValue]);
|
|
24
|
-
const [local, setLocal] = useState(() => [...defaultValue]), [
|
|
27
|
+
const initial = useRef([...defaultValue]), initialFreeText = useRef(initialText);
|
|
28
|
+
const [local, setLocal] = useState(() => [...defaultValue]), [draftQuery, setDraftQuery] = useState(null);
|
|
29
|
+
const [localText, setLocalText] = useState(initialText);
|
|
30
|
+
const text = textValue ?? localText;
|
|
31
|
+
const query = freeText ? text : draftQuery === null ? null : controlledQuery ?? draftQuery;
|
|
32
|
+
const setQuery = (next) => { setDraftQuery(next); if (!freeText && next !== null)
|
|
33
|
+
onQueryChange?.(next); };
|
|
34
|
+
const publishText = (next) => { if (textValue === undefined)
|
|
35
|
+
setLocalText(next); if (next !== text)
|
|
36
|
+
onTextChange?.(next); };
|
|
25
37
|
const [open, setOpen] = useState(false), [activeValue, setActiveValue] = useState();
|
|
26
38
|
const [validation, setValidation] = useState("");
|
|
27
|
-
const selections = (value ?? local).map(id => options.find(option => option.value === id)).filter((option) => Boolean(option));
|
|
39
|
+
const selections = (value ?? local).map(id => selectedOptions.find(option => option.value === id) ?? options.find(option => option.value === id)).filter((option) => Boolean(option));
|
|
28
40
|
const selected = selections[0], selectedIds = selections.map(option => option.value);
|
|
29
|
-
const displayed = query ?? (multiple ? "" : selected?.label ?? "");
|
|
30
|
-
const
|
|
41
|
+
const displayed = freeText ? text : query ?? (multiple ? "" : selected?.label ?? "");
|
|
42
|
+
const resultQuery = freeText ? text : controlledQuery ?? draftQuery ?? "";
|
|
43
|
+
const pending = loadState === "loading" || (filtering === "manual" && optionsQuery !== resultQuery);
|
|
44
|
+
const failed = loadState === "error" && !pending;
|
|
45
|
+
const resultMessage = pending ? loadingMessage : failed ? errorMessage : emptyMessage;
|
|
46
|
+
const filtered = pending || failed ? [] : filtering === "manual" || query === null ? options : options.filter(option => searchable([option.label, ...(option.keywords ?? [])].join(" ")).includes(searchable(query)));
|
|
31
47
|
const enabled = filtered.filter(option => !option.disabled);
|
|
48
|
+
const groups = [];
|
|
49
|
+
for (const option of filtered) {
|
|
50
|
+
const previous = groups.at(-1);
|
|
51
|
+
if (previous && previous.label === option.group)
|
|
52
|
+
previous.options.push(option);
|
|
53
|
+
else
|
|
54
|
+
groups.push({ label: option.group, options: [option] });
|
|
55
|
+
}
|
|
32
56
|
const active = enabled.find(option => option.value === activeValue) ?? enabled[0];
|
|
33
57
|
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 = () => {
|
|
58
|
+
const current = useRef({ value, textValue, selected, readOnly, disabled });
|
|
59
|
+
current.current = { value, textValue, selected, readOnly, disabled };
|
|
60
|
+
const close = () => {
|
|
61
|
+
if (panel.current?.contains(panel.current.ownerDocument.activeElement))
|
|
62
|
+
input.current?.focus({ preventScroll: true });
|
|
63
|
+
setOpen(false);
|
|
64
|
+
setQuery(null);
|
|
65
|
+
};
|
|
37
66
|
const inert = () => Boolean(readOnly || disabled || input.current?.matches(":disabled"));
|
|
38
67
|
const publish = (next) => {
|
|
39
68
|
if (value === undefined)
|
|
@@ -43,14 +72,23 @@ export const SearchSelection = /* @__PURE__ */ forwardRef(function SearchSelecti
|
|
|
43
72
|
setValidation("");
|
|
44
73
|
};
|
|
45
74
|
const remove = (id) => {
|
|
46
|
-
if (inert() ||
|
|
75
|
+
if (inert() || selections.find(option => option.value === id)?.disabled)
|
|
47
76
|
return;
|
|
48
77
|
publish(selectedIds.filter(value => value !== id));
|
|
49
78
|
input.current?.focus({ preventScroll: true });
|
|
50
79
|
};
|
|
51
80
|
const select = (next) => {
|
|
52
|
-
if (inert() || (next !== "" && !
|
|
81
|
+
if (inert() || (next !== "" && !filtered.some(option => option.value === next && !option.disabled)))
|
|
53
82
|
return;
|
|
83
|
+
if (freeText) {
|
|
84
|
+
const option = filtered.find(option => option.value === next);
|
|
85
|
+
publishText(option?.label ?? "");
|
|
86
|
+
if (option)
|
|
87
|
+
onOptionSelect?.(option);
|
|
88
|
+
close();
|
|
89
|
+
input.current?.focus({ preventScroll: true });
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
54
92
|
if (multiple) {
|
|
55
93
|
publish(next === "" ? selections.filter(option => option.disabled).map(option => option.value) : selectedIds.includes(next) ? selectedIds.filter(id => id !== next) : [...selectedIds, next]);
|
|
56
94
|
if (next) {
|
|
@@ -75,7 +113,7 @@ export const SearchSelection = /* @__PURE__ */ forwardRef(function SearchSelecti
|
|
|
75
113
|
};
|
|
76
114
|
useBrowserLayoutEffect(() => {
|
|
77
115
|
const control = input.current;
|
|
78
|
-
control?.setCustomValidity(required && !selections.length ? invalidSelectionMessage : "");
|
|
116
|
+
control?.setCustomValidity(!freeText && required && !selections.length ? invalidSelectionMessage : "");
|
|
79
117
|
if (validation && control?.validity.valid)
|
|
80
118
|
setValidation("");
|
|
81
119
|
});
|
|
@@ -92,6 +130,8 @@ export const SearchSelection = /* @__PURE__ */ forwardRef(function SearchSelecti
|
|
|
92
130
|
return;
|
|
93
131
|
if (current.current.value === undefined)
|
|
94
132
|
setLocal(initial.current);
|
|
133
|
+
if (current.current.textValue === undefined)
|
|
134
|
+
setLocalText(initialFreeText.current);
|
|
95
135
|
setQuery(null);
|
|
96
136
|
setOpen(false);
|
|
97
137
|
setValidation("");
|
|
@@ -167,7 +207,7 @@ export const SearchSelection = /* @__PURE__ */ forwardRef(function SearchSelecti
|
|
|
167
207
|
popup.style.visibility = "hidden";
|
|
168
208
|
popup.showPopover();
|
|
169
209
|
update();
|
|
170
|
-
unregister = registerLayer(document, { panel: popup, anchor: () => anchor, close
|
|
210
|
+
unregister = registerLayer(document, { panel: popup, anchor: () => anchor, close });
|
|
171
211
|
};
|
|
172
212
|
const toggle = (event) => { if (alive && event.newState === "closed" && !popup.matches(":popover-open")) {
|
|
173
213
|
setOpen(false);
|
|
@@ -192,7 +232,8 @@ export const SearchSelection = /* @__PURE__ */ forwardRef(function SearchSelecti
|
|
|
192
232
|
}, [open, active?.value, query, options]);
|
|
193
233
|
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" }) });
|
|
194
234
|
const inputProps = {
|
|
195
|
-
...props, id, type: "text", value: displayed,
|
|
235
|
+
...props, id, type: "text", value: displayed, placeholder: multiple && selections.length ? undefined : props.placeholder,
|
|
236
|
+
name: freeText ? name : undefined, disabled, readOnly, required: required && (!multiple || !selections.length), controlSize, autoComplete,
|
|
196
237
|
role: "combobox", "aria-autocomplete": "list", "aria-haspopup": "listbox", "aria-expanded": open, "aria-controls": open ? menuId : undefined,
|
|
197
238
|
"aria-required": required || undefined, "aria-activedescendant": open && active ? optionId(active.value) : undefined,
|
|
198
239
|
"aria-describedby": mergeDescriptions(props["aria-describedby"], validation && validationId, multiple && Boolean(selections.length) && `${id}-selection`),
|
|
@@ -201,11 +242,14 @@ export const SearchSelection = /* @__PURE__ */ forwardRef(function SearchSelecti
|
|
|
201
242
|
if (inert())
|
|
202
243
|
return;
|
|
203
244
|
const text = event.currentTarget.value;
|
|
204
|
-
|
|
245
|
+
if (freeText)
|
|
246
|
+
publishText(text);
|
|
247
|
+
else
|
|
248
|
+
setQuery(text);
|
|
205
249
|
setActiveValue(undefined);
|
|
206
250
|
setOpen(true);
|
|
207
251
|
setValidation("");
|
|
208
|
-
if (!multiple && text === "" && selected)
|
|
252
|
+
if (!freeText && !multiple && text === "" && selected)
|
|
209
253
|
publish([]);
|
|
210
254
|
},
|
|
211
255
|
onClick: event => { onClick?.(event); if (!event.defaultPrevented)
|
|
@@ -255,25 +299,28 @@ export const SearchSelection = /* @__PURE__ */ forwardRef(function SearchSelecti
|
|
|
255
299
|
}
|
|
256
300
|
}
|
|
257
301
|
else if (event.key === "Enter" && open) {
|
|
258
|
-
|
|
302
|
+
if (!freeText || active)
|
|
303
|
+
event.preventDefault();
|
|
259
304
|
if (active)
|
|
260
305
|
select(active.value);
|
|
306
|
+
else
|
|
307
|
+
close();
|
|
261
308
|
}
|
|
262
309
|
else if (event.key === "Escape" && open) {
|
|
263
310
|
event.preventDefault();
|
|
264
311
|
event.stopPropagation();
|
|
265
312
|
close();
|
|
266
313
|
}
|
|
267
|
-
else if (event.key === "Tab")
|
|
314
|
+
else if (event.key === "Tab" && !(failed && onRetry && !event.shiftKey))
|
|
268
315
|
close();
|
|
269
316
|
},
|
|
270
317
|
};
|
|
271
318
|
const removable = selections.some(option => !option.disabled);
|
|
272
|
-
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)
|
|
319
|
+
const actions = _jsxs("span", { className: "flood-combobox__actions", children: [clearable && _jsx(IconButton, { label: clearLabel, variant: "ghost", size: controlSize, className: !(freeText ? text : selected) ? "flood-combobox__clear-empty" : undefined, disabled: disabled || readOnly || !(freeText ? text : selected) || (multiple && !removable), "aria-hidden": !(freeText ? text : 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)
|
|
273
320
|
close();
|
|
274
321
|
else
|
|
275
322
|
show(); input.current?.focus({ preventScroll: true }); }, children: glyph(false) })] });
|
|
276
|
-
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()) {
|
|
323
|
+
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"]), "data-has-selection": Boolean(selections.length) || undefined, "data-query": Boolean(displayed) || undefined, "data-clearable": clearable || undefined, onClick: event => { if (event.target === event.currentTarget && !inert()) {
|
|
277
324
|
input.current?.focus();
|
|
278
325
|
show();
|
|
279
326
|
} }, 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 => {
|
|
@@ -300,11 +347,12 @@ export const SearchSelection = /* @__PURE__ */ forwardRef(function SearchSelecti
|
|
|
300
347
|
event.preventDefault();
|
|
301
348
|
(buttons[Math.max(0, index - 1)])?.focus();
|
|
302
349
|
}
|
|
303
|
-
} }, 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 &&
|
|
304
|
-
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:
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
350
|
+
} }, 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 }), !freeText && (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 && _jsxs("div", { ref: panel, popover: "manual", className: "flood-select__menu flood-combobox__menu", onPointerDown: event => { if (event.pointerType === "mouse")
|
|
351
|
+
event.preventDefault(); }, children: [_jsxs(ScrollArea, { ref: viewport, id: menuId, role: "listbox", "aria-label": listLabel, "aria-busy": pending || undefined, "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: resultMessage }), groups.map((group, index) => _jsxs("div", { role: group.label ? "group" : "presentation", "aria-label": group.label, children: [group.label && _jsx("div", { className: "flood-select__group", "aria-hidden": "true", children: group.label }), group.options.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)
|
|
352
|
+
setActiveValue(option.value); }, onPointerDown: event => {
|
|
353
|
+
// Touch must retain its compatibility click and native scrolling.
|
|
354
|
+
if (event.pointerType === "mouse")
|
|
355
|
+
event.preventDefault();
|
|
356
|
+
}, 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))] }, index))] }), failed && onRetry && _jsx(Button, { variant: "subtle", size: "sm", onClick: () => { input.current?.focus({ preventScroll: true }); onRetry(); }, onBlur: event => { if (!root.current?.contains(event.relatedTarget))
|
|
357
|
+
close(); }, children: retryLabel })] }), open && !filtered.length && _jsx("span", { className: "flood-visually-hidden", role: "status", children: resultMessage }), validation && _jsx("span", { id: validationId, className: "flood-select__validation", role: "alert", children: validation })] });
|
|
310
358
|
});
|
package/dist/command-list.d.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
1
|
import { type CommandListProps } from "./index.js";
|
|
2
|
+
interface SearchCommandListProps extends CommandListProps {
|
|
3
|
+
integrated?: boolean;
|
|
4
|
+
}
|
|
2
5
|
/** Searchable action suggestions. No selected form value or modal lifecycle. */
|
|
3
|
-
export declare const SearchCommandList: import("react").ForwardRefExoticComponent<
|
|
6
|
+
export declare const SearchCommandList: import("react").ForwardRefExoticComponent<SearchCommandListProps & import("react").RefAttributes<HTMLInputElement>>;
|
|
7
|
+
export {};
|
package/dist/command-list.js
CHANGED
|
@@ -3,7 +3,7 @@ import { forwardRef, useEffect, useId, useImperativeHandle, useRef, useState } f
|
|
|
3
3
|
import { Input, Label, ScrollArea, Surface, Text, VisuallyHidden } from "./index.js";
|
|
4
4
|
const searchable = (value) => value.normalize("NFKD").replace(/\p{M}/gu, "").toLowerCase();
|
|
5
5
|
/** Searchable action suggestions. No selected form value or modal lifecycle. */
|
|
6
|
-
export const SearchCommandList = /* @__PURE__ */ forwardRef(function SearchCommandList({ label, hideLabel = false, items, query, defaultQuery = "", onQueryChange, onAction, disabled = false, loading = false, placeholder = "Search commands…", emptyMessage = "No commands found.", loadingMessage = "Loading commands…", controlSize = "md", id: explicitId, className, onKeyDown, ...props }, forwardedRef) {
|
|
6
|
+
export const SearchCommandList = /* @__PURE__ */ forwardRef(function SearchCommandList({ label, hideLabel = false, items, query, defaultQuery = "", onQueryChange, onAction, disabled = false, loading = false, placeholder = "Search commands…", emptyMessage = "No commands found.", loadingMessage = "Loading commands…", controlSize = "md", integrated = false, id: explicitId, className, onKeyDown, ...props }, forwardedRef) {
|
|
7
7
|
if (![label, emptyMessage, loadingMessage].every(value => value.trim()))
|
|
8
8
|
throw new Error("Flood CommandList requires nonempty accessible labels and state messages");
|
|
9
9
|
if (new Set(items.map(item => item.id)).size !== items.length || items.some(item => !item.id.trim() || !item.label.trim()))
|
|
@@ -84,7 +84,7 @@ export const SearchCommandList = /* @__PURE__ */ forwardRef(function SearchComma
|
|
|
84
84
|
setActiveId(enabled[next].id);
|
|
85
85
|
}, children: [_jsx(Label, { htmlFor: inputId, className: hideLabel ? "flood-visually-hidden" : undefined, children: label }), _jsx(Input, { ref: input, id: inputId, value: text, disabled: disabled, type: "text", controlSize: controlSize, placeholder: placeholder, autoComplete: "off", spellCheck: false, role: "combobox", "aria-autocomplete": "list", "aria-haspopup": "listbox", "aria-expanded": "true", "aria-controls": listId, "aria-activedescendant": active ? optionId(active) : undefined, onCompositionStart: () => { composing.current = true; }, onCompositionEnd: () => { composing.current = false; }, onChange: event => { const next = event.currentTarget.value; if (disabled || event.currentTarget.matches(":disabled"))
|
|
86
86
|
return; if (query === undefined)
|
|
87
|
-
setLocalQuery(next); onQueryChange?.(next); } }), _jsx(Surface, { padding: "1", className: "flood-command-list__surface", children: _jsx(ScrollArea, { ref: viewport, id: listId, role: "listbox", "aria-label": label, "aria-busy": loading || undefined, "aria-disabled": effectiveDisabled || undefined, tabIndex: -1, className: "flood-command-list__viewport",
|
|
87
|
+
setLocalQuery(next); onQueryChange?.(next); } }), _jsx(Surface, { elevation: integrated ? "overlay" : "default", padding: integrated ? "2" : "1", style: integrated ? { background: "transparent" } : undefined, className: "flood-command-list__surface", children: _jsx(ScrollArea, { ref: viewport, id: listId, role: "listbox", "aria-label": label, "aria-busy": loading || undefined, "aria-disabled": effectiveDisabled || undefined, tabIndex: -1, className: "flood-command-list__viewport", children: status ? _jsx("div", { role: "option", "aria-disabled": "true", "aria-selected": "false", className: "flood-command-list__empty", children: _jsx(Text, { size: "compact", tone: "subtle", children: status }) }) : [...groups].map(([group, entries], groupIndex) => {
|
|
88
88
|
const content = entries.map(item => _jsxs("div", { id: optionId(item), role: "option", "aria-disabled": item.disabled || effectiveDisabled || undefined, "aria-selected": item.id === active?.id, "data-active": item.id === active?.id || undefined, className: "flood-command-list__option", onPointerMove: event => { if (event.pointerType === "mouse" && !blocked() && !item.disabled)
|
|
89
89
|
setActiveId(item.id); }, onPointerDown: event => { if (event.pointerType === "mouse")
|
|
90
90
|
event.preventDefault(); }, onMouseDown: event => event.preventDefault(), onClick: () => invoke(item), children: [_jsx("span", { className: "flood-command-list__label", children: item.label }), item.description && _jsx("span", { className: "flood-command-list__description", children: item.description })] }, item.id));
|
package/dist/data-display.js
CHANGED
|
@@ -1,11 +1,35 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { forwardRef } from "react";
|
|
2
|
+
import { forwardRef, useEffect, useRef } from "react";
|
|
3
3
|
import { ScrollArea } from "./index.js";
|
|
4
4
|
export const DescribedData = /* @__PURE__ */ 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)
|
|
5
|
+
if (items.some(item => typeof item.id !== "string" || !item.id.trim()) || new Set(items.map(item => item.id)).size !== items.length)
|
|
6
6
|
throw new Error("Flood description items require unique nonempty IDs");
|
|
7
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
8
|
});
|
|
9
|
-
export const TabularData = /* @__PURE__ */ forwardRef(function TabularData({ caption, children, hideCaption, density, striped, scrollLabel, cellVerticalAlign, className, hidden, dir, lang, ...props }, ref) {
|
|
10
|
-
|
|
9
|
+
export const TabularData = /* @__PURE__ */ forwardRef(function TabularData({ caption, children, hideCaption, density, striped, scrollLabel, cellVerticalAlign, maxViewportHeight, stickyHeader, className, hidden, dir, lang, ...props }, ref) {
|
|
10
|
+
const viewport = useRef(null);
|
|
11
|
+
const scrollVertically = maxViewportHeight !== undefined;
|
|
12
|
+
const pinHeader = scrollVertically && stickyHeader;
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
const node = viewport.current;
|
|
15
|
+
const header = node?.querySelector("table")?.tHead;
|
|
16
|
+
if (!pinHeader || !node || !header)
|
|
17
|
+
return;
|
|
18
|
+
const update = () => { node.style.scrollPaddingBlockStart = `${header.offsetHeight}px`; };
|
|
19
|
+
update();
|
|
20
|
+
const observer = new ResizeObserver(update);
|
|
21
|
+
observer.observe(header);
|
|
22
|
+
return () => {
|
|
23
|
+
observer.disconnect();
|
|
24
|
+
node.style.removeProperty("scroll-padding-block-start");
|
|
25
|
+
};
|
|
26
|
+
}, [pinHeader, children]);
|
|
27
|
+
return _jsx("div", { className: "flood-table-container", hidden: hidden, dir: dir, lang: lang, children: _jsx(ScrollArea, { ref: viewport, orientation: scrollVertically ? "both" : "horizontal", contentInset: scrollVertically ? "scrollbar" : undefined, tabIndex: scrollVertically ? 0 : undefined, style: scrollVertically ? { maxBlockSize: maxViewportHeight } : undefined, "aria-label": scrollLabel ?? caption, onFocus: pinHeader ? event => {
|
|
28
|
+
const target = event.target;
|
|
29
|
+
const header = event.currentTarget.querySelector("table")?.tHead;
|
|
30
|
+
if (header && target !== event.currentTarget && !header.contains(target) &&
|
|
31
|
+
!target.closest("[popover], dialog[open]") && target.getBoundingClientRect().top < header.getBoundingClientRect().bottom) {
|
|
32
|
+
target.scrollIntoView({ block: "nearest", inline: "nearest" });
|
|
33
|
+
}
|
|
34
|
+
} : undefined, 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, "data-cell-vertical-align": cellVerticalAlign, "data-sticky-header": pinHeader || undefined, children: [_jsx("caption", { className: hideCaption ? "flood-visually-hidden" : undefined, children: caption }), children] }) }) });
|
|
11
35
|
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type CSSProperties } from "react";
|
|
2
|
+
import { type DataTableColumn, type DataTableProps } from "./index.js";
|
|
3
|
+
export declare function useDataTableColumnLayout(columns: readonly DataTableColumn[], visibleColumns: readonly DataTableColumn[], { columnWidths, defaultColumnWidths, onColumnWidthsChange, measureViewport, }: Pick<DataTableProps, "columnWidths" | "defaultColumnWidths" | "onColumnWidthsChange"> & {
|
|
4
|
+
measureViewport?: boolean;
|
|
5
|
+
}): {
|
|
6
|
+
table: import("react").RefObject<HTMLTableElement | null>;
|
|
7
|
+
fixedWidth: number | undefined;
|
|
8
|
+
viewportWidth: number | undefined;
|
|
9
|
+
disclosureWidth: number | undefined;
|
|
10
|
+
atDefaults: boolean;
|
|
11
|
+
reset: () => void;
|
|
12
|
+
utilityCell: (id: string, header?: boolean) => {
|
|
13
|
+
"data-pin-edge": "end" | "start" | undefined;
|
|
14
|
+
"data-pin-boundary": true | undefined;
|
|
15
|
+
style: CSSProperties | undefined;
|
|
16
|
+
"data-data-table-utility": string;
|
|
17
|
+
};
|
|
18
|
+
columnCell: (column: DataTableColumn, header?: boolean) => {
|
|
19
|
+
"data-pin-edge": "end" | "start" | undefined;
|
|
20
|
+
"data-pin-boundary": true | undefined;
|
|
21
|
+
style: CSSProperties | undefined;
|
|
22
|
+
"data-data-table-column": string;
|
|
23
|
+
};
|
|
24
|
+
renderHandle: (column: DataTableColumn, label: string, disabled: boolean) => false | import("react").JSX.Element | undefined;
|
|
25
|
+
};
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
3
|
+
import { IconSlot } from "./index.js";
|
|
4
|
+
const defaultMinWidth = 64;
|
|
5
|
+
const defaultMaxWidth = 960;
|
|
6
|
+
const columnKey = (id) => `column:${id}`;
|
|
7
|
+
const utilityKey = (id) => `utility:${id}`;
|
|
8
|
+
const validWidth = (value) => typeof value === "number" && Number.isFinite(value) && value > 0;
|
|
9
|
+
function bounds(column) {
|
|
10
|
+
const min = validWidth(column.minWidth) ? Math.ceil(column.minWidth) : defaultMinWidth;
|
|
11
|
+
const max = validWidth(column.maxWidth) ? Math.floor(column.maxWidth) : defaultMaxWidth;
|
|
12
|
+
return { min, max: Math.max(min, max) };
|
|
13
|
+
}
|
|
14
|
+
function clampWidth(value, { min, max }) {
|
|
15
|
+
return Math.max(min, Math.min(max, Math.round(value)));
|
|
16
|
+
}
|
|
17
|
+
function normalizeWidths(columns, widths) {
|
|
18
|
+
return new Map(columns.flatMap(column => {
|
|
19
|
+
const value = widths && Object.prototype.hasOwnProperty.call(widths, column.id) ? widths[column.id] : undefined;
|
|
20
|
+
return column.resizable && validWidth(value) ? [[column.id, clampWidth(value, bounds(column))]] : [];
|
|
21
|
+
}));
|
|
22
|
+
}
|
|
23
|
+
function equalWidths(a, b) {
|
|
24
|
+
return a.size === b.size && [...a].every(([key, width]) => b.get(key) === width);
|
|
25
|
+
}
|
|
26
|
+
function measuredColumnWidth(header) {
|
|
27
|
+
const style = header.ownerDocument.defaultView?.getComputedStyle(header);
|
|
28
|
+
if (!style)
|
|
29
|
+
return header.offsetWidth;
|
|
30
|
+
const inlineSize = Number.parseFloat(style.inlineSize);
|
|
31
|
+
if (!Number.isFinite(inlineSize))
|
|
32
|
+
return header.offsetWidth;
|
|
33
|
+
const borderBox = style.boxSizing === "border-box" ? inlineSize : inlineSize +
|
|
34
|
+
Number.parseFloat(style.paddingInlineStart) + Number.parseFloat(style.paddingInlineEnd) +
|
|
35
|
+
Number.parseFloat(style.borderInlineStartWidth) + Number.parseFloat(style.borderInlineEndWidth);
|
|
36
|
+
// Avoid shaving a fraction of a pixel from untouched labels when switching to fixed tracks.
|
|
37
|
+
return Math.ceil(borderBox * 100) / 100;
|
|
38
|
+
}
|
|
39
|
+
function ColumnResizeHandle({ column, label, width, disabled, onPreview, onResize }) {
|
|
40
|
+
const gesture = useRef(null);
|
|
41
|
+
const [dragging, setDragging] = useState(false);
|
|
42
|
+
const limits = bounds(column);
|
|
43
|
+
const direction = (node) => {
|
|
44
|
+
const rtl = node.ownerDocument.defaultView?.getComputedStyle(node).direction === "rtl";
|
|
45
|
+
return (rtl ? -1 : 1) * (column.pinned === "end" ? -1 : 1);
|
|
46
|
+
};
|
|
47
|
+
const cancel = () => {
|
|
48
|
+
const active = gesture.current;
|
|
49
|
+
if (!active)
|
|
50
|
+
return;
|
|
51
|
+
gesture.current = null;
|
|
52
|
+
if (active.handle.hasPointerCapture(active.id))
|
|
53
|
+
active.handle.releasePointerCapture(active.id);
|
|
54
|
+
setDragging(false);
|
|
55
|
+
onPreview(undefined);
|
|
56
|
+
};
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
if (disabled)
|
|
59
|
+
cancel();
|
|
60
|
+
}, [disabled]);
|
|
61
|
+
useEffect(() => () => {
|
|
62
|
+
if (gesture.current)
|
|
63
|
+
onPreview(undefined);
|
|
64
|
+
}, []);
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
if (!dragging)
|
|
67
|
+
return;
|
|
68
|
+
const document = gesture.current?.handle.ownerDocument;
|
|
69
|
+
const view = document?.defaultView;
|
|
70
|
+
const hide = () => { if (document?.hidden)
|
|
71
|
+
cancel(); };
|
|
72
|
+
view?.addEventListener("blur", cancel);
|
|
73
|
+
document?.addEventListener("visibilitychange", hide);
|
|
74
|
+
return () => {
|
|
75
|
+
view?.removeEventListener("blur", cancel);
|
|
76
|
+
document?.removeEventListener("visibilitychange", hide);
|
|
77
|
+
};
|
|
78
|
+
}, [dragging]);
|
|
79
|
+
const requestedWidth = (event) => {
|
|
80
|
+
const active = gesture.current;
|
|
81
|
+
if (active?.id !== event.pointerId)
|
|
82
|
+
return undefined;
|
|
83
|
+
return clampWidth(active.width + (event.clientX - active.origin) * active.direction / active.scale, limits);
|
|
84
|
+
};
|
|
85
|
+
const value = width === undefined ? limits.min : clampWidth(width, limits);
|
|
86
|
+
return _jsx("div", { role: "separator", "aria-label": label, title: label, "aria-orientation": "vertical", "aria-valuemin": limits.min, "aria-valuemax": limits.max, "aria-valuenow": value, "aria-valuetext": `${value} px`, tabIndex: disabled ? -1 : 0, "aria-disabled": disabled || undefined, className: "flood-data-table__resize", "data-dragging": dragging || undefined, onKeyDown: event => {
|
|
87
|
+
if (event.defaultPrevented || disabled || event.altKey || event.ctrlKey || event.metaKey || event.nativeEvent.isComposing)
|
|
88
|
+
return;
|
|
89
|
+
if (event.key === "Escape" && gesture.current) {
|
|
90
|
+
event.preventDefault();
|
|
91
|
+
cancel();
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (gesture.current)
|
|
95
|
+
return;
|
|
96
|
+
let next;
|
|
97
|
+
if (event.key === "Home")
|
|
98
|
+
next = limits.min;
|
|
99
|
+
else if (event.key === "End")
|
|
100
|
+
next = limits.max;
|
|
101
|
+
else if (event.key === "ArrowLeft" || event.key === "ArrowRight") {
|
|
102
|
+
const step = event.shiftKey ? 24 : 8;
|
|
103
|
+
next = clampWidth((width ?? value) + (event.key === "ArrowRight" ? step : -step) * direction(event.currentTarget), limits);
|
|
104
|
+
}
|
|
105
|
+
else
|
|
106
|
+
return;
|
|
107
|
+
event.preventDefault();
|
|
108
|
+
if (next !== width)
|
|
109
|
+
onResize(next);
|
|
110
|
+
}, onPointerDown: event => {
|
|
111
|
+
const handle = event.currentTarget;
|
|
112
|
+
const header = handle.closest("th");
|
|
113
|
+
if (event.defaultPrevented || disabled || gesture.current || event.button !== 0 || !event.isPrimary || !header?.offsetWidth)
|
|
114
|
+
return;
|
|
115
|
+
try {
|
|
116
|
+
handle.setPointerCapture(event.pointerId);
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
event.preventDefault();
|
|
122
|
+
handle.focus({ preventScroll: true });
|
|
123
|
+
gesture.current = {
|
|
124
|
+
handle, id: event.pointerId, origin: event.clientX, width: header.offsetWidth,
|
|
125
|
+
direction: direction(handle), scale: header.getBoundingClientRect().width / header.offsetWidth || 1,
|
|
126
|
+
};
|
|
127
|
+
setDragging(true);
|
|
128
|
+
}, onPointerMove: event => {
|
|
129
|
+
const next = requestedWidth(event);
|
|
130
|
+
if (next !== undefined && !disabled)
|
|
131
|
+
onPreview(event.clientX === gesture.current?.origin ? undefined : next);
|
|
132
|
+
}, onPointerUp: event => {
|
|
133
|
+
const next = requestedWidth(event);
|
|
134
|
+
if (next === undefined)
|
|
135
|
+
return;
|
|
136
|
+
const moved = event.clientX !== gesture.current?.origin && next !== gesture.current?.width;
|
|
137
|
+
cancel();
|
|
138
|
+
if (!disabled && moved)
|
|
139
|
+
onResize(next);
|
|
140
|
+
}, onPointerCancel: event => { if (gesture.current?.id === event.pointerId)
|
|
141
|
+
cancel(); }, onLostPointerCapture: event => { if (gesture.current?.id === event.pointerId)
|
|
142
|
+
cancel(); }, children: _jsx("span", { className: "flood-data-table__resize-icon", children: _jsx(IconSlot, { size: "sm", children: _jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: _jsx("path", { d: "M3 12h18m-14-4-4 4 4 4m10-8 4 4-4 4" }) }) }) }) });
|
|
143
|
+
}
|
|
144
|
+
export function useDataTableColumnLayout(columns, visibleColumns, { columnWidths, defaultColumnWidths, onColumnWidthsChange, measureViewport = false, }) {
|
|
145
|
+
const [local, setLocal] = useState(defaultColumnWidths);
|
|
146
|
+
const [draft, setDraft] = useState();
|
|
147
|
+
const [metrics, setMetrics] = useState({ widths: new Map(), utilityKeys: [], viewport: 0 });
|
|
148
|
+
const table = useRef(null);
|
|
149
|
+
const baseline = useRef(new Map());
|
|
150
|
+
const accepted = normalizeWidths(columns, columnWidths ?? local);
|
|
151
|
+
const defaults = normalizeWidths(columns, defaultColumnWidths);
|
|
152
|
+
const activeDraft = draft && visibleColumns.some(column => column.resizable && column.id === draft.id) ? draft : undefined;
|
|
153
|
+
const visibleSizes = new Map(visibleColumns.flatMap(column => {
|
|
154
|
+
const width = activeDraft?.id === column.id ? activeDraft.width : accepted.get(column.id);
|
|
155
|
+
return width === undefined ? [] : [[columnKey(column.id), width]];
|
|
156
|
+
}));
|
|
157
|
+
const active = measureViewport || columns.some(column => column.resizable || column.pinned);
|
|
158
|
+
const hasSizes = visibleSizes.size > 0;
|
|
159
|
+
const columnOrder = JSON.stringify(visibleColumns.map(column => column.id));
|
|
160
|
+
useLayoutEffect(() => {
|
|
161
|
+
const node = table.current;
|
|
162
|
+
const viewport = node?.closest(".flood-scroll-area__viewport");
|
|
163
|
+
const headerRow = node?.tHead?.rows[0];
|
|
164
|
+
if (!active || !node || !viewport || !headerRow)
|
|
165
|
+
return;
|
|
166
|
+
let headers = Array.from(headerRow.cells);
|
|
167
|
+
const measure = () => {
|
|
168
|
+
const widths = new Map();
|
|
169
|
+
const utilityKeys = [];
|
|
170
|
+
for (const header of headers) {
|
|
171
|
+
const id = header.dataset.dataTableColumn;
|
|
172
|
+
const utility = header.dataset.dataTableUtility;
|
|
173
|
+
const key = id !== undefined ? columnKey(id) : utilityKey(utility ?? `unknown-${header.cellIndex}`);
|
|
174
|
+
if (id === undefined)
|
|
175
|
+
utilityKeys.push(key);
|
|
176
|
+
if (header.offsetWidth === 0)
|
|
177
|
+
continue;
|
|
178
|
+
widths.set(key, header.offsetWidth);
|
|
179
|
+
if (!hasSizes || !baseline.current.has(key))
|
|
180
|
+
baseline.current.set(key, measuredColumnWidth(header));
|
|
181
|
+
}
|
|
182
|
+
setMetrics(previous => previous.viewport === viewport.clientWidth && equalWidths(previous.widths, widths) &&
|
|
183
|
+
previous.utilityKeys.join("|") === utilityKeys.join("|")
|
|
184
|
+
? previous : { widths, utilityKeys, viewport: viewport.clientWidth });
|
|
185
|
+
};
|
|
186
|
+
measure();
|
|
187
|
+
const Observer = node.ownerDocument.defaultView?.ResizeObserver;
|
|
188
|
+
if (!Observer)
|
|
189
|
+
return;
|
|
190
|
+
const observer = new Observer(measure);
|
|
191
|
+
const observeHeaders = () => {
|
|
192
|
+
observer.disconnect();
|
|
193
|
+
headers = Array.from(headerRow.cells);
|
|
194
|
+
observer.observe(viewport);
|
|
195
|
+
for (const header of headers)
|
|
196
|
+
observer.observe(header, { box: "border-box" });
|
|
197
|
+
measure();
|
|
198
|
+
};
|
|
199
|
+
observeHeaders();
|
|
200
|
+
const HeaderObserver = node.ownerDocument.defaultView?.MutationObserver;
|
|
201
|
+
const headerObserver = HeaderObserver && new HeaderObserver(observeHeaders);
|
|
202
|
+
headerObserver?.observe(headerRow, { childList: true });
|
|
203
|
+
return () => {
|
|
204
|
+
observer.disconnect();
|
|
205
|
+
headerObserver?.disconnect();
|
|
206
|
+
};
|
|
207
|
+
}, [active, hasSizes, columnOrder]);
|
|
208
|
+
const sizes = hasSizes ? new Map([...baseline.current, ...visibleSizes]) : metrics.widths;
|
|
209
|
+
const keys = [...metrics.utilityKeys, ...visibleColumns.map(column => columnKey(column.id))];
|
|
210
|
+
const fixedWidth = hasSizes && keys.every(key => sizes.has(key))
|
|
211
|
+
? keys.reduce((total, key) => total + sizes.get(key), 0)
|
|
212
|
+
: undefined;
|
|
213
|
+
const start = visibleColumns.filter(column => column.pinned === "start").map(column => columnKey(column.id));
|
|
214
|
+
const end = visibleColumns.filter(column => column.pinned === "end").map(column => columnKey(column.id)).reverse();
|
|
215
|
+
if (start.length)
|
|
216
|
+
start.unshift(...metrics.utilityKeys);
|
|
217
|
+
const pinned = new Map();
|
|
218
|
+
const pinnedKeys = [...start, ...end];
|
|
219
|
+
const pinnedWidth = pinnedKeys.reduce((total, key) => total + (metrics.widths.get(key) ?? 0), 0);
|
|
220
|
+
if (pinnedKeys.every(key => metrics.widths.has(key)) && pinnedWidth + defaultMinWidth <= metrics.viewport) {
|
|
221
|
+
for (const [edge, group] of [["start", start], ["end", end]]) {
|
|
222
|
+
let offset = 0;
|
|
223
|
+
group.forEach((key, index) => {
|
|
224
|
+
pinned.set(key, { edge, offset, boundary: index === group.length - 1 });
|
|
225
|
+
offset += metrics.widths.get(key);
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
function cell(key, header, width) {
|
|
230
|
+
const pin = pinned.get(key);
|
|
231
|
+
const inlineSize = header ? (fixedWidth === undefined ? width : sizes.get(key)) : undefined;
|
|
232
|
+
return {
|
|
233
|
+
"data-pin-edge": pin?.edge,
|
|
234
|
+
"data-pin-boundary": pin?.boundary || undefined,
|
|
235
|
+
style: inlineSize !== undefined || pin
|
|
236
|
+
? { inlineSize, "--flood-data-table-pin-offset": pin ? `${pin.offset}px` : undefined }
|
|
237
|
+
: undefined,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
function publish(next) {
|
|
241
|
+
if (equalWidths(accepted, next))
|
|
242
|
+
return;
|
|
243
|
+
const widths = Object.fromEntries(next);
|
|
244
|
+
if (columnWidths === undefined)
|
|
245
|
+
setLocal(widths);
|
|
246
|
+
onColumnWidthsChange?.(widths);
|
|
247
|
+
}
|
|
248
|
+
return {
|
|
249
|
+
table, fixedWidth,
|
|
250
|
+
viewportWidth: metrics.viewport || undefined,
|
|
251
|
+
disclosureWidth: metrics.widths.get(utilityKey("expansion")),
|
|
252
|
+
atDefaults: equalWidths(accepted, defaults),
|
|
253
|
+
reset: () => { setDraft(undefined); publish(defaults); },
|
|
254
|
+
utilityCell: (id, header = false) => ({
|
|
255
|
+
...cell(utilityKey(id), header),
|
|
256
|
+
"data-data-table-utility": id,
|
|
257
|
+
}),
|
|
258
|
+
columnCell: (column, header = false) => ({
|
|
259
|
+
...cell(columnKey(column.id), header, header ? (accepted.get(column.id) ?? column.width) : undefined),
|
|
260
|
+
"data-data-table-column": column.id,
|
|
261
|
+
}),
|
|
262
|
+
renderHandle: (column, label, disabled) => column.resizable &&
|
|
263
|
+
_jsx(ColumnResizeHandle, { column: column, label: label, disabled: disabled, width: visibleSizes.get(columnKey(column.id)) ?? metrics.widths.get(columnKey(column.id)), onPreview: width => setDraft(previous => {
|
|
264
|
+
if (width === undefined)
|
|
265
|
+
return previous?.id === column.id ? undefined : previous;
|
|
266
|
+
if (previous?.id === column.id && previous.width === width)
|
|
267
|
+
return previous;
|
|
268
|
+
return { id: column.id, width };
|
|
269
|
+
}), onResize: width => publish(new Map(accepted).set(column.id, width)) }),
|
|
270
|
+
};
|
|
271
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type ReactNode, type TdHTMLAttributes } from "react";
|
|
2
|
+
import { type DataTableLabels, type DataTableRow } from "./index.js";
|
|
3
|
+
export declare function useDataTableExpansion(rows: readonly DataTableRow[], expandedIds: readonly string[] | undefined, defaultExpandedIds: readonly string[] | undefined, onExpandedChange: ((ids: string[]) => void) | undefined): {
|
|
4
|
+
enabled: boolean;
|
|
5
|
+
expanded: Set<string>;
|
|
6
|
+
toggle: (id: string) => void;
|
|
7
|
+
};
|
|
8
|
+
interface DataTableExpandableRowProps {
|
|
9
|
+
row: DataTableRow;
|
|
10
|
+
showDisclosure: boolean;
|
|
11
|
+
expanded: boolean;
|
|
12
|
+
colSpan: number;
|
|
13
|
+
labels: DataTableLabels;
|
|
14
|
+
onToggle: () => void;
|
|
15
|
+
disclosureCellProps?: TdHTMLAttributes<HTMLTableCellElement>;
|
|
16
|
+
viewportWidth?: number;
|
|
17
|
+
disclosureWidth?: number;
|
|
18
|
+
children: (disclosureCell: ReactNode) => ReactNode;
|
|
19
|
+
}
|
|
20
|
+
export declare function DataTableExpandableRow({ row, showDisclosure, expanded, colSpan, labels, onToggle, disclosureCellProps, viewportWidth, disclosureWidth, children }: DataTableExpandableRowProps): import("react").JSX.Element;
|
|
21
|
+
export {};
|