@flood-md/react 0.2.6 → 0.2.8
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 +42 -1
- package/dist/alert-dialog.js +1 -1
- package/dist/calendar-period-grid.d.ts +17 -0
- package/dist/calendar-period-grid.js +13 -0
- package/dist/calendar-value.d.ts +10 -0
- package/dist/calendar-value.js +48 -0
- package/dist/calendar.d.ts +21 -0
- package/dist/calendar.js +259 -0
- package/dist/checkbox.js +1 -1
- package/dist/choice-group.js +1 -1
- package/dist/collection-cards.d.ts +4 -0
- package/dist/collection-cards.js +21 -0
- package/dist/color-controls.d.ts +5 -0
- package/dist/color-controls.js +110 -0
- package/dist/combobox.js +22 -13
- package/dist/command-list.d.ts +3 -0
- package/dist/command-list.js +93 -0
- package/dist/content-elements.js +7 -7
- package/dist/context-menu.d.ts +2 -0
- package/dist/context-menu.js +76 -0
- package/dist/data-display.js +2 -2
- package/dist/data-table.d.ts +2 -0
- package/dist/data-table.js +58 -0
- package/dist/date-field.d.ts +2 -0
- package/dist/date-field.js +172 -0
- package/dist/date-picker.d.ts +3 -0
- package/dist/date-picker.js +108 -0
- package/dist/date-range-field.d.ts +3 -0
- package/dist/date-range-field.js +84 -0
- package/dist/date-range-picker.d.ts +2 -0
- package/dist/date-range-picker.js +81 -0
- package/dist/date-range-value.d.ts +8 -0
- package/dist/date-range-value.js +13 -0
- package/dist/date-time-picker.d.ts +3 -0
- package/dist/date-time-picker.js +31 -0
- package/dist/date-time-range-picker.d.ts +2 -0
- package/dist/date-time-range-picker.js +51 -0
- package/dist/date-value.d.ts +10 -0
- package/dist/date-value.js +31 -0
- package/dist/disclosure.js +1 -1
- package/dist/editable.d.ts +3 -0
- package/dist/editable.js +135 -0
- package/dist/error-summary.d.ts +3 -0
- package/dist/error-summary.js +39 -0
- package/dist/feedback-actions.js +2 -2
- package/dist/feedback.js +2 -2
- package/dist/file-input.js +1 -1
- package/dist/file-records.d.ts +5 -0
- package/dist/file-records.js +55 -0
- package/dist/form-controls.js +25 -13
- package/dist/foundations.css +387 -1
- package/dist/hover-card.d.ts +3 -0
- package/dist/hover-card.js +144 -0
- package/dist/index.d.ts +661 -2
- package/dist/index.js +190 -86
- package/dist/menu.d.ts +27 -1
- package/dist/menu.js +327 -89
- package/dist/navigation-layout.js +6 -6
- package/dist/navigation-tree.d.ts +3 -0
- package/dist/navigation-tree.js +98 -0
- package/dist/navigation.js +3 -3
- package/dist/numeric-input.js +1 -1
- package/dist/overlays.js +11 -10
- package/dist/range-calendar.d.ts +2 -0
- package/dist/range-calendar.js +35 -0
- package/dist/resizable-panels.d.ts +3 -0
- package/dist/resizable-panels.js +205 -0
- package/dist/scroll-area.js +1 -1
- package/dist/select.js +1 -1
- package/dist/slider.js +1 -1
- package/dist/tag-input.d.ts +3 -0
- package/dist/tag-input.js +181 -0
- package/dist/text-inputs.js +2 -2
- package/dist/time-field.d.ts +2 -0
- package/dist/time-field.js +176 -0
- package/dist/time-picker.d.ts +2 -0
- package/dist/time-picker.js +83 -0
- package/dist/time-value.d.ts +12 -0
- package/dist/time-value.js +36 -0
- package/dist/toolbar.js +1 -1
- package/dist/tree-select.d.ts +3 -0
- package/dist/tree-select.js +113 -0
- package/dist/tree.d.ts +3 -0
- package/dist/tree.js +206 -0
- package/package.json +2 -2
package/dist/tree.js
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef, useEffect, useId, useImperativeHandle, useLayoutEffect, useRef, useState } from "react";
|
|
3
|
+
const useBrowserLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
|
|
4
|
+
const searchable = (value) => value.normalize("NFKD").replace(/\p{M}/gu, "").toLocaleLowerCase();
|
|
5
|
+
const values = (value) => [...new Set(typeof value === "string" ? (value ? [value] : []) : value ?? [])];
|
|
6
|
+
/** Application selection, not navigation or a form field; expansion and selection are independent. */
|
|
7
|
+
export const SelectionTree = /* @__PURE__ */ forwardRef(function SelectionTree({ label, hideLabel = false, items, value, defaultValue, onValueChange, multiple = false, expandedValues, defaultExpandedValues = [], onExpandedValuesChange, disabled = false, readOnly = false, controlSize = "md", emptyMessage = "No items.", loadingMessage = "Loading…", unloadedMessage = "Content not loaded.", readOnlyMessage = "Selection is read only.", id: explicitId, className, onKeyDown, onClick, onDoubleClick, onFocus, onBlur, ...props }, forwardedRef) {
|
|
8
|
+
if (![label, emptyMessage, loadingMessage, unloadedMessage, readOnlyMessage].every(text => text.trim()))
|
|
9
|
+
throw new Error("Flood Tree requires nonempty labels and state messages");
|
|
10
|
+
const all = new Map();
|
|
11
|
+
const collect = (siblings, parent, level) => {
|
|
12
|
+
if (level > 256)
|
|
13
|
+
throw new Error("Flood Tree nesting exceeds the supported depth");
|
|
14
|
+
siblings.forEach((item, index) => {
|
|
15
|
+
if (!item.value.trim() || !item.label.trim() || all.has(item.value))
|
|
16
|
+
throw new Error("Flood Tree items require globally unique nonempty values and labels");
|
|
17
|
+
all.set(item.value, { item, parent, level, position: index + 1, size: siblings.length, branch: !!item.hasChildren || !!item.loading || !!item.children?.length });
|
|
18
|
+
if (item.children)
|
|
19
|
+
collect(item.children, item.value, level + 1);
|
|
20
|
+
});
|
|
21
|
+
};
|
|
22
|
+
collect(items, undefined, 1);
|
|
23
|
+
const [localSelected, setLocalSelected] = useState(() => values(defaultValue));
|
|
24
|
+
const [localExpanded, setLocalExpanded] = useState(() => values(defaultExpandedValues));
|
|
25
|
+
const selectedValues = values(value === undefined ? localSelected : value).filter(key => all.has(key) && all.get(key).item.selectable !== false), selected = new Set(multiple ? selectedValues : selectedValues.slice(0, 1));
|
|
26
|
+
const expandedList = values(expandedValues ?? localExpanded), expanded = new Set(expandedList);
|
|
27
|
+
const visible = [];
|
|
28
|
+
for (const entry of all.values()) {
|
|
29
|
+
let parent = entry.parent, shown = true;
|
|
30
|
+
while (parent) {
|
|
31
|
+
if (!expanded.has(parent)) {
|
|
32
|
+
shown = false;
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
parent = all.get(parent)?.parent;
|
|
36
|
+
}
|
|
37
|
+
if (shown)
|
|
38
|
+
visible.push(entry);
|
|
39
|
+
}
|
|
40
|
+
const visibleValues = new Set(visible.map(entry => entry.item.value));
|
|
41
|
+
const root = useRef(null), tree = useRef(null), nodes = useRef(new Map());
|
|
42
|
+
const previous = useRef({ all, visible });
|
|
43
|
+
const [focused, setFocused] = useState();
|
|
44
|
+
const [direction, setDirection] = useState("ltr");
|
|
45
|
+
const document = root.current?.ownerDocument, domFocused = document?.activeElement;
|
|
46
|
+
const wasFocused = !!domFocused && !!root.current?.contains(domFocused) && domFocused.hasAttribute("data-tree-value");
|
|
47
|
+
let active = focused && visibleValues.has(focused) ? focused : undefined;
|
|
48
|
+
if (!active && focused) {
|
|
49
|
+
let ancestor = all.get(focused)?.parent ?? previous.current.all.get(focused)?.parent;
|
|
50
|
+
while (ancestor) {
|
|
51
|
+
if (visibleValues.has(ancestor)) {
|
|
52
|
+
active = ancestor;
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
ancestor = all.get(ancestor)?.parent ?? previous.current.all.get(ancestor)?.parent;
|
|
56
|
+
}
|
|
57
|
+
if (!active) {
|
|
58
|
+
const old = previous.current.visible.findIndex(entry => entry.item.value === focused);
|
|
59
|
+
active = visible[Math.min(Math.max(old, 0), visible.length - 1)]?.item.value;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
active ??= visible.find(entry => selected.has(entry.item.value))?.item.value ?? visible[0]?.item.value;
|
|
63
|
+
const generated = useId(), id = explicitId ?? `flood-tree-${generated}`;
|
|
64
|
+
const itemId = (key) => `${id}-item-${encodeURIComponent(key)}`;
|
|
65
|
+
useImperativeHandle(forwardedRef, () => root.current, []);
|
|
66
|
+
useBrowserLayoutEffect(() => {
|
|
67
|
+
const node = root.current;
|
|
68
|
+
if (!node)
|
|
69
|
+
return;
|
|
70
|
+
const update = () => setDirection(node.ownerDocument.defaultView.getComputedStyle(node).direction);
|
|
71
|
+
update();
|
|
72
|
+
const observer = new node.ownerDocument.defaultView.MutationObserver(update);
|
|
73
|
+
let ancestor = node;
|
|
74
|
+
while (ancestor) {
|
|
75
|
+
observer.observe(ancestor, { attributes: true, attributeFilter: ["dir"] });
|
|
76
|
+
ancestor = ancestor.parentElement;
|
|
77
|
+
}
|
|
78
|
+
return () => observer.disconnect();
|
|
79
|
+
}, [props.dir]);
|
|
80
|
+
useBrowserLayoutEffect(() => {
|
|
81
|
+
previous.current = { all, visible };
|
|
82
|
+
if (focused !== active)
|
|
83
|
+
setFocused(active);
|
|
84
|
+
const current = document?.activeElement;
|
|
85
|
+
if (wasFocused && !disabled && (current === document?.body || !!current && !!root.current?.contains(current))) {
|
|
86
|
+
if (active && nodes.current.get(active) !== current)
|
|
87
|
+
nodes.current.get(active)?.focus({ preventScroll: true });
|
|
88
|
+
else if (!active && tree.current !== current)
|
|
89
|
+
tree.current?.focus({ preventScroll: true });
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
const search = useRef({ text: "", time: 0 });
|
|
93
|
+
const focusItem = (key) => { setFocused(key); nodes.current.get(key)?.focus({ preventScroll: true }); };
|
|
94
|
+
const toggle = (entry) => {
|
|
95
|
+
if (disabled || entry.item.disabled || !entry.branch)
|
|
96
|
+
return;
|
|
97
|
+
const next = expanded.has(entry.item.value) ? expandedList.filter(key => key !== entry.item.value) : [...expandedList, entry.item.value];
|
|
98
|
+
if (expandedValues === undefined)
|
|
99
|
+
setLocalExpanded(next);
|
|
100
|
+
onExpandedValuesChange?.(next);
|
|
101
|
+
};
|
|
102
|
+
const select = (entry) => {
|
|
103
|
+
if (disabled || readOnly || entry.item.disabled || entry.item.readOnly || entry.item.selectable === false)
|
|
104
|
+
return;
|
|
105
|
+
const key = entry.item.value, next = multiple ? (selected.has(key) ? selectedValues.filter(value => value !== key) : [...selectedValues, key]) : [key];
|
|
106
|
+
if (next.length === selectedValues.length && next.every((key, index) => key === selectedValues[index]))
|
|
107
|
+
return;
|
|
108
|
+
if (value === undefined)
|
|
109
|
+
setLocalSelected(next);
|
|
110
|
+
onValueChange?.(multiple ? next : key);
|
|
111
|
+
};
|
|
112
|
+
const eventEntry = (target) => {
|
|
113
|
+
if (!(target instanceof (root.current?.ownerDocument.defaultView?.Element ?? Element)))
|
|
114
|
+
return;
|
|
115
|
+
const node = target.closest("[data-tree-entry]");
|
|
116
|
+
if (!node || !tree.current?.contains(node))
|
|
117
|
+
return;
|
|
118
|
+
return all.get(node.dataset.treeValue);
|
|
119
|
+
};
|
|
120
|
+
const render = (siblings) => siblings.map(item => {
|
|
121
|
+
const entry = all.get(item.value), open = entry.branch && expanded.has(item.value), selectedItem = selected.has(item.value), status = open ? (item.loading ? loadingMessage : item.hasChildren && item.children === undefined ? unloadedMessage : entry.branch && !item.children?.length ? emptyMessage : undefined) : undefined;
|
|
122
|
+
return _jsxs("div", { className: "flood-tree__branch", "data-tree-entry": true, "data-tree-value": item.value, "data-has-icon": item.icon != null || undefined, style: { "--flood-tree-depth": entry.level - 1 }, children: [_jsx("div", { id: itemId(item.value), ref: node => { if (node)
|
|
123
|
+
nodes.current.set(item.value, node);
|
|
124
|
+
else
|
|
125
|
+
nodes.current.delete(item.value); }, className: "flood-tree__item", role: "treeitem", "data-tree-value": item.value, "data-disabled": disabled || item.disabled || undefined, "data-readonly": readOnly || item.readOnly || undefined, tabIndex: !disabled && active === item.value ? 0 : -1, "aria-labelledby": [`${itemId(item.value)}-label`, item.description ? `${itemId(item.value)}-description` : undefined].filter(Boolean).join(" "), "aria-describedby": [status ? `${itemId(item.value)}-status` : undefined, readOnly || item.readOnly ? `${id}-readonly` : undefined].filter(Boolean).join(" ") || undefined, "aria-owns": open && item.children?.length ? `${itemId(item.value)}-group` : undefined, "aria-level": entry.level, "aria-posinset": entry.position, "aria-setsize": entry.size, "aria-expanded": entry.branch ? open : undefined, "aria-selected": item.selectable === false ? undefined : selectedItem, "aria-disabled": disabled || item.disabled || undefined, "aria-busy": item.loading || undefined, children: _jsxs("div", { className: "flood-tree__row", "data-tree-row": true, "data-selected": selectedItem || undefined, children: [_jsx("span", { className: "flood-tree__disclosure", "data-tree-toggle": entry.branch ? "true" : undefined, "aria-hidden": "true", children: entry.branch && _jsx("svg", { viewBox: "0 0 24 24", fill: "none", className: "flood-tree__chevron", "data-expanded": open || undefined, children: _jsx("path", { d: "m9 5 7 7-7 7", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" }) }) }), item.icon != null && _jsx("span", { className: "flood-tree__icon", "aria-hidden": "true", children: item.icon }), _jsxs("span", { className: "flood-tree__text", children: [_jsx("span", { id: `${itemId(item.value)}-label`, className: "flood-tree__label", children: item.label }), item.description && _jsxs(_Fragment, { children: [" ", _jsx("span", { id: `${itemId(item.value)}-description`, className: "flood-tree__description", children: item.description })] })] })] }) }), open && !!item.children?.length && _jsx("div", { id: `${itemId(item.value)}-group`, role: "group", className: "flood-tree__group", children: render(item.children) }), status && _jsx("div", { id: `${itemId(item.value)}-status`, className: "flood-tree__branch-status", role: "status", children: status })] }, item.value);
|
|
126
|
+
});
|
|
127
|
+
return _jsxs("div", { ...props, id: id, ref: root, className: ["flood-tree", `flood-control--${controlSize}`, className].filter(Boolean).join(" "), "data-disabled": disabled || undefined, "data-readonly": readOnly || undefined, "data-tree-direction": direction, onFocus: event => { onFocus?.(event); const entry = eventEntry(event.target); if (entry)
|
|
128
|
+
setFocused(entry.item.value); }, onBlur: onBlur, onClick: event => {
|
|
129
|
+
onClick?.(event);
|
|
130
|
+
if (event.defaultPrevented || disabled)
|
|
131
|
+
return;
|
|
132
|
+
const entry = eventEntry(event.target);
|
|
133
|
+
if (!entry)
|
|
134
|
+
return;
|
|
135
|
+
focusItem(entry.item.value);
|
|
136
|
+
if (event.detail > 1)
|
|
137
|
+
return;
|
|
138
|
+
if (event.target.closest("[data-tree-toggle='true']"))
|
|
139
|
+
toggle(entry);
|
|
140
|
+
else if (entry.item.selectable === false && entry.branch)
|
|
141
|
+
toggle(entry);
|
|
142
|
+
else
|
|
143
|
+
select(entry);
|
|
144
|
+
}, onDoubleClick: event => {
|
|
145
|
+
onDoubleClick?.(event);
|
|
146
|
+
if (event.defaultPrevented || disabled)
|
|
147
|
+
return;
|
|
148
|
+
const entry = eventEntry(event.target);
|
|
149
|
+
if (entry && entry.item.selectable !== false && !event.target.closest("[data-tree-toggle='true']"))
|
|
150
|
+
toggle(entry);
|
|
151
|
+
}, onKeyDown: event => {
|
|
152
|
+
onKeyDown?.(event);
|
|
153
|
+
if (event.defaultPrevented || disabled || event.nativeEvent.isComposing || event.keyCode === 229 || event.altKey || event.ctrlKey || event.metaKey || (event.shiftKey && event.key.length !== 1))
|
|
154
|
+
return;
|
|
155
|
+
const entry = eventEntry(event.target);
|
|
156
|
+
if (!entry)
|
|
157
|
+
return;
|
|
158
|
+
const index = visible.findIndex(item => item.item.value === entry.item.value);
|
|
159
|
+
const rtl = root.current?.ownerDocument.defaultView.getComputedStyle(root.current).direction === "rtl";
|
|
160
|
+
const forward = rtl ? "ArrowLeft" : "ArrowRight", back = rtl ? "ArrowRight" : "ArrowLeft";
|
|
161
|
+
if (event.key === "ArrowDown" || event.key === "ArrowUp" || event.key === "Home" || event.key === "End") {
|
|
162
|
+
event.preventDefault();
|
|
163
|
+
const next = event.key === "Home" ? 0 : event.key === "End" ? visible.length - 1 : Math.max(0, Math.min(visible.length - 1, index + (event.key === "ArrowDown" ? 1 : -1)));
|
|
164
|
+
if (visible[next])
|
|
165
|
+
focusItem(visible[next].item.value);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (event.key === forward) {
|
|
169
|
+
event.preventDefault();
|
|
170
|
+
if (entry.branch && !expanded.has(entry.item.value))
|
|
171
|
+
toggle(entry);
|
|
172
|
+
else if (entry.branch && entry.item.children?.length)
|
|
173
|
+
focusItem(entry.item.children[0].value);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (event.key === back) {
|
|
177
|
+
event.preventDefault();
|
|
178
|
+
if (entry.branch && expanded.has(entry.item.value))
|
|
179
|
+
toggle(entry);
|
|
180
|
+
else if (entry.parent)
|
|
181
|
+
focusItem(entry.parent);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
185
|
+
event.preventDefault();
|
|
186
|
+
if (entry.item.selectable === false && entry.branch)
|
|
187
|
+
toggle(entry);
|
|
188
|
+
else
|
|
189
|
+
select(entry);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (event.key.length === 1 && event.key.trim()) {
|
|
193
|
+
const now = Date.now(), text = searchable(event.key);
|
|
194
|
+
search.current = { text: now - search.current.time > 700 ? text : search.current.text + text, time: now };
|
|
195
|
+
const query = [...search.current.text].every(letter => letter === text) ? text : search.current.text;
|
|
196
|
+
for (let offset = 1; offset <= visible.length; offset++) {
|
|
197
|
+
const candidate = visible[(index + offset) % visible.length];
|
|
198
|
+
if (searchable(candidate.item.label).startsWith(query)) {
|
|
199
|
+
event.preventDefault();
|
|
200
|
+
focusItem(candidate.item.value);
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}, children: [_jsx("span", { className: ["flood-label", hideLabel ? "flood-visually-hidden" : ""].filter(Boolean).join(" "), id: `${id}-label`, children: label }), _jsx("span", { id: `${id}-readonly`, hidden: true, children: readOnlyMessage }), _jsx("div", { ref: tree, role: "tree", "aria-labelledby": `${id}-label`, "aria-multiselectable": multiple || undefined, "aria-disabled": disabled || undefined, "aria-describedby": [!items.length ? `${id}-empty` : undefined, readOnly ? `${id}-readonly` : undefined].filter(Boolean).join(" ") || undefined, tabIndex: !disabled && !items.length ? 0 : -1, className: "flood-tree__viewport", children: render(items) }), !items.length && _jsx("div", { id: `${id}-empty`, role: "status", className: "flood-tree__empty", children: emptyMessage })] });
|
|
206
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flood-md/react",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.8",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Original Flood components for React and Next.js.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"react-dom": "^18.3.0 || ^19.0.0"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@flood-md/tokens": "0.2.
|
|
33
|
+
"@flood-md/tokens": "0.2.8"
|
|
34
34
|
},
|
|
35
35
|
"publishConfig": {
|
|
36
36
|
"access": "public"
|