@tensor_1/react 2.35.0 → 2.37.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +154 -15
- package/dist/components/Avatar.d.ts +26 -4
- package/dist/components/Avatar.js +26 -3
- package/dist/components/CommandPalette.d.ts +6 -1
- package/dist/components/CommandPalette.js +9 -3
- package/dist/components/DateFieldParts.d.ts +61 -0
- package/dist/components/DateFieldParts.js +195 -0
- package/dist/components/DatePicker.d.ts +129 -0
- package/dist/components/DatePicker.js +340 -0
- package/dist/components/DateRangePicker.d.ts +34 -3
- package/dist/components/DateRangePicker.js +120 -17
- package/dist/components/EntityPicker.d.ts +29 -4
- package/dist/components/EntityPicker.js +20 -8
- package/dist/components/InlineEdit.d.ts +74 -0
- package/dist/components/InlineEdit.js +176 -0
- package/dist/components/Radio.d.ts +23 -2
- package/dist/components/Radio.js +18 -3
- package/dist/components/SchedulingAgenda.js +3 -1
- package/dist/components/SchedulingTimeGrid.js +6 -4
- package/dist/components/SchedulingTimeline.d.ts +130 -15
- package/dist/components/SchedulingTimeline.js +768 -141
- package/dist/components/SearchSuggest.d.ts +12 -1
- package/dist/components/SearchSuggest.js +7 -2
- package/dist/components/Tree.d.ts +30 -2
- package/dist/components/Tree.js +135 -10
- package/dist/date-field-model.d.ts +40 -0
- package/dist/date-field-model.js +79 -0
- package/dist/hooks/usePrinting.d.ts +15 -0
- package/dist/hooks/usePrinting.js +29 -0
- package/dist/index.cjs +3890 -2070
- package/dist/index.d.cts +808 -262
- package/dist/index.d.ts +9 -6
- package/dist/index.js +40 -4
- package/dist/labels-context.d.ts +14 -1
- package/dist/scheduling-timeline-model.d.ts +61 -11
- package/dist/scheduling-timeline-model.js +85 -26
- package/dist/scheduling-timeline-rows.d.ts +40 -0
- package/dist/scheduling-timeline-rows.js +42 -0
- package/package.json +5 -3
|
@@ -50,6 +50,17 @@ interface SearchSuggestProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onCha
|
|
|
50
50
|
empty?: ReactNode;
|
|
51
51
|
/** Shown instead of the empty state when suggestions could not be loaded. */
|
|
52
52
|
error?: ReactNode;
|
|
53
|
+
/**
|
|
54
|
+
* Shown in the open list when there is nothing else to show there: no options, nothing
|
|
55
|
+
* loading, no error and no empty state. For a search that has not started yet, e.g.
|
|
56
|
+
* "Type at least 2 characters." (HAR-1648).
|
|
57
|
+
*/
|
|
58
|
+
idle?: ReactNode;
|
|
59
|
+
/**
|
|
60
|
+
* A line under the options that is not an option: arrow keys skip it and it cannot be
|
|
61
|
+
* chosen. For "More matches. Keep typing to narrow the search." when the results were cut off.
|
|
62
|
+
*/
|
|
63
|
+
note?: ReactNode;
|
|
53
64
|
/** A keyboard hint shown in the empty, unfocused field, e.g. "/". */
|
|
54
65
|
shortcutHint?: string;
|
|
55
66
|
/** Accessible name of the clear button (default "Clear search"). */
|
|
@@ -86,6 +97,6 @@ interface SearchSuggestProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onCha
|
|
|
86
97
|
* (`aria-activedescendant`), Enter opens it (the first row when none is active), Escape closes the
|
|
87
98
|
* list and a second Escape clears the text.
|
|
88
99
|
*/
|
|
89
|
-
declare function SearchSuggest({ value, onValueChange, options, onSelect, label, placeholder, highlight, heading, headingAction, footer, loading, loadingLabel, empty, error, shortcutHint, clearLabel: clearLabelProp, status, size, open: openProp, strategy, onOpenChange, inputRef, autoFocus, name, inputId, inputDescribedBy, className, onBlur, ...props }: SearchSuggestProps): react.JSX.Element;
|
|
100
|
+
declare function SearchSuggest({ value, onValueChange, options, onSelect, label, placeholder, highlight, heading, headingAction, footer, loading, loadingLabel, empty, idle, note, error, shortcutHint, clearLabel: clearLabelProp, status, size, open: openProp, strategy, onOpenChange, inputRef, autoFocus, name, inputId, inputDescribedBy, className, onBlur, ...props }: SearchSuggestProps): react.JSX.Element;
|
|
90
101
|
|
|
91
102
|
export { SearchSuggest, type SearchSuggestAction, type SearchSuggestOption, type SearchSuggestProps };
|
|
@@ -32,6 +32,8 @@ function SearchSuggest({
|
|
|
32
32
|
loading = false,
|
|
33
33
|
loadingLabel,
|
|
34
34
|
empty,
|
|
35
|
+
idle,
|
|
36
|
+
note,
|
|
35
37
|
error,
|
|
36
38
|
shortcutHint,
|
|
37
39
|
clearLabel: clearLabelProp,
|
|
@@ -72,7 +74,8 @@ function SearchSuggest({
|
|
|
72
74
|
const hasText = value.trim().length > 0;
|
|
73
75
|
const showEmpty = hasText && !loading && !error && options.length === 0 && empty != null;
|
|
74
76
|
const showLoading = loading && options.length === 0;
|
|
75
|
-
const
|
|
77
|
+
const showIdle = idle != null && itemCount === 0 && !loading && error == null && !showEmpty;
|
|
78
|
+
const visible = open && (itemCount > 0 || showLoading || showEmpty || showIdle || error != null);
|
|
76
79
|
const optionId = (index) => `${id}-option-${index}`;
|
|
77
80
|
useEffect(() => {
|
|
78
81
|
if (!visible || active < 0) return;
|
|
@@ -294,7 +297,9 @@ function SearchSuggest({
|
|
|
294
297
|
loadingLabel && /* @__PURE__ */ jsx("span", { children: loadingLabel })
|
|
295
298
|
] }),
|
|
296
299
|
error != null && options.length === 0 && !loading && /* @__PURE__ */ jsx("div", { className: "uix-search-suggest__state", children: error }),
|
|
297
|
-
showEmpty && /* @__PURE__ */ jsx("div", { className: "uix-search-suggest__state", children: empty })
|
|
300
|
+
showEmpty && /* @__PURE__ */ jsx("div", { className: "uix-search-suggest__state", children: empty }),
|
|
301
|
+
showIdle && /* @__PURE__ */ jsx("div", { className: "uix-search-suggest__state", children: idle }),
|
|
302
|
+
note != null && options.length > 0 && /* @__PURE__ */ jsx("div", { className: "uix-search-suggest__note", children: note })
|
|
298
303
|
] }),
|
|
299
304
|
/* @__PURE__ */ jsx("span", { className: "uix-visually-hidden", role: "status", "aria-live": "polite", children: status ?? "" })
|
|
300
305
|
]
|
|
@@ -8,7 +8,23 @@ interface TreeNodeData {
|
|
|
8
8
|
typeaheadLabel?: string;
|
|
9
9
|
icon?: ReactNode;
|
|
10
10
|
children?: TreeNodeData[];
|
|
11
|
+
/**
|
|
12
|
+
* The node can be expanded although its `children` are not known yet (HAR-1382). With
|
|
13
|
+
* `Tree loadChildren`, expanding it loads them; until then it shows a loading row, and an
|
|
14
|
+
* error row with Retry when the load failed.
|
|
15
|
+
*/
|
|
16
|
+
hasChildren?: boolean;
|
|
17
|
+
}
|
|
18
|
+
/** The words of a tree that loads children on expand (HAR-1382). */
|
|
19
|
+
interface TreeLabels {
|
|
20
|
+
/** The row under a node whose children are loading. */
|
|
21
|
+
loading: string;
|
|
22
|
+
/** The row under a node whose children could not be loaded. */
|
|
23
|
+
error: string;
|
|
24
|
+
/** The action in that row. Enter, Space or a click on the row runs it. */
|
|
25
|
+
retry: string;
|
|
11
26
|
}
|
|
27
|
+
declare const DEFAULT_TREE_LABELS: TreeLabels;
|
|
12
28
|
interface TreeNodeProps {
|
|
13
29
|
node: TreeNodeData;
|
|
14
30
|
expanded: Set<string>;
|
|
@@ -21,6 +37,8 @@ interface TreeNodeProps {
|
|
|
21
37
|
level?: number;
|
|
22
38
|
/** Whether a click on the row itself toggles a branch (see {@link TreeProps.toggleOnRowClick}). */
|
|
23
39
|
rowToggles?: boolean;
|
|
40
|
+
/** Ids of nodes whose children are loading (`aria-busy`). */
|
|
41
|
+
busy?: ReadonlySet<string>;
|
|
24
42
|
}
|
|
25
43
|
interface TreeProps extends Omit<HTMLAttributes<HTMLUListElement>, 'onSelect' | 'onChange'> {
|
|
26
44
|
nodes: TreeNodeData[];
|
|
@@ -46,7 +64,17 @@ interface TreeProps extends Omit<HTMLAttributes<HTMLUListElement>, 'onSelect' |
|
|
|
46
64
|
* where a row click only selects and the chevron toggles. Enter/Space still does both.
|
|
47
65
|
*/
|
|
48
66
|
toggleOnRowClick?: boolean;
|
|
67
|
+
/**
|
|
68
|
+
* Loads the children of a node that has `hasChildren` and no `children` yet, the first time
|
|
69
|
+
* it is expanded (HAR-1382; an org chart, a CMDB hierarchy). While it runs the node is
|
|
70
|
+
* `aria-busy` and shows a loading row; a rejection shows an error row whose Enter, Space or
|
|
71
|
+
* click retries. The result is kept for the life of the tree; a node that later arrives with
|
|
72
|
+
* its own `children` uses those.
|
|
73
|
+
*/
|
|
74
|
+
loadChildren?: (node: TreeNodeData) => Promise<TreeNodeData[]>;
|
|
75
|
+
/** Words of the loading and error rows. Default: `UixLabelsProvider` `tree`, then English. */
|
|
76
|
+
labels?: Partial<TreeLabels>;
|
|
49
77
|
}
|
|
50
|
-
declare function Tree({ nodes, expanded: controlledExpanded, defaultExpanded, selected, onToggle, onSelect, virtualize, rowHeight, maxHeight, toggleOnRowClick, className, ...props }: TreeProps): react.JSX.Element;
|
|
78
|
+
declare function Tree({ nodes: nodesProp, expanded: controlledExpanded, defaultExpanded, selected, onToggle, onSelect: onSelectProp, virtualize, rowHeight, maxHeight, toggleOnRowClick, loadChildren, labels: labelOverrides, className, ...props }: TreeProps): react.JSX.Element;
|
|
51
79
|
|
|
52
|
-
export { Tree, type TreeNodeData, type TreeNodeProps, type TreeProps };
|
|
80
|
+
export { DEFAULT_TREE_LABELS, Tree, type TreeLabels, type TreeNodeData, type TreeNodeProps, type TreeProps };
|
package/dist/components/Tree.js
CHANGED
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
+
import { useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
|
|
4
4
|
import { cx } from "../cx.js";
|
|
5
|
+
import { useUixLabels } from "../labels-context.js";
|
|
5
6
|
import { flattenTree, treeNav } from "../tree-model.js";
|
|
6
7
|
import { virtualWindow, shouldVirtualize } from "../table-engine.js";
|
|
7
8
|
const ChevronIcon = () => /* @__PURE__ */ jsx("svg", { viewBox: "0 0 12 12", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: "M2 4l4 4 4-4" }) });
|
|
8
|
-
|
|
9
|
+
const DEFAULT_TREE_LABELS = {
|
|
10
|
+
loading: "Loading\u2026",
|
|
11
|
+
error: "Could not load.",
|
|
12
|
+
retry: "Retry"
|
|
13
|
+
};
|
|
14
|
+
const LOADING = "::uix-loading";
|
|
15
|
+
const FAILED = "::uix-error";
|
|
16
|
+
const syntheticOf = (id) => id.endsWith(LOADING) ? { parent: id.slice(0, -LOADING.length), kind: "loading" } : id.endsWith(FAILED) ? { parent: id.slice(0, -FAILED.length), kind: "error" } : null;
|
|
17
|
+
const Ring = () => /* @__PURE__ */ jsx("span", { className: "uix-spinner uix-spinner--sm", "aria-hidden": "true" });
|
|
18
|
+
function TreeNode({ node, expanded, selected, selectable, tabbableId, onToggle, onSelect, onFocusNode, level = 1, rowToggles = true, busy }) {
|
|
9
19
|
const hasChildren = Array.isArray(node.children) && node.children.length > 0;
|
|
10
20
|
const isExpanded = expanded.has(node.id);
|
|
11
21
|
return /* @__PURE__ */ jsxs(
|
|
@@ -14,7 +24,8 @@ function TreeNode({ node, expanded, selected, selectable, tabbableId, onToggle,
|
|
|
14
24
|
role: "treeitem",
|
|
15
25
|
"aria-level": level,
|
|
16
26
|
"aria-expanded": hasChildren ? isExpanded : void 0,
|
|
17
|
-
"aria-selected": selectable ? selected === node.id : void 0,
|
|
27
|
+
"aria-selected": selectable && !syntheticOf(node.id) ? selected === node.id : void 0,
|
|
28
|
+
"aria-busy": busy?.has(node.id) || void 0,
|
|
18
29
|
"data-id": node.id,
|
|
19
30
|
tabIndex: tabbableId === node.id ? 0 : -1,
|
|
20
31
|
className: "uix-tree__item",
|
|
@@ -64,7 +75,8 @@ function TreeNode({ node, expanded, selected, selectable, tabbableId, onToggle,
|
|
|
64
75
|
onSelect,
|
|
65
76
|
onFocusNode,
|
|
66
77
|
level: level + 1,
|
|
67
|
-
rowToggles
|
|
78
|
+
rowToggles,
|
|
79
|
+
busy
|
|
68
80
|
},
|
|
69
81
|
child.id
|
|
70
82
|
)) })
|
|
@@ -73,16 +85,18 @@ function TreeNode({ node, expanded, selected, selectable, tabbableId, onToggle,
|
|
|
73
85
|
);
|
|
74
86
|
}
|
|
75
87
|
function Tree({
|
|
76
|
-
nodes,
|
|
88
|
+
nodes: nodesProp,
|
|
77
89
|
expanded: controlledExpanded,
|
|
78
90
|
defaultExpanded,
|
|
79
91
|
selected,
|
|
80
92
|
onToggle,
|
|
81
|
-
onSelect,
|
|
93
|
+
onSelect: onSelectProp,
|
|
82
94
|
virtualize,
|
|
83
95
|
rowHeight = 32,
|
|
84
96
|
maxHeight = 384,
|
|
85
97
|
toggleOnRowClick,
|
|
98
|
+
loadChildren,
|
|
99
|
+
labels: labelOverrides,
|
|
86
100
|
className,
|
|
87
101
|
...props
|
|
88
102
|
}) {
|
|
@@ -90,8 +104,111 @@ function Tree({
|
|
|
90
104
|
const expanded = controlledExpanded ?? internalExpanded;
|
|
91
105
|
const rootRef = useRef(null);
|
|
92
106
|
const [focusedId, setFocusedId] = useState(void 0);
|
|
93
|
-
const selectable = selected !== void 0 ||
|
|
107
|
+
const selectable = selected !== void 0 || onSelectProp != null;
|
|
94
108
|
const rowToggles = toggleOnRowClick ?? !selectable;
|
|
109
|
+
const labels = { ...DEFAULT_TREE_LABELS, ...useUixLabels().tree, ...labelOverrides };
|
|
110
|
+
const treeId = useId();
|
|
111
|
+
const [loaded, setLoaded] = useState(() => /* @__PURE__ */ new Map());
|
|
112
|
+
const [failed, setFailed] = useState(() => /* @__PURE__ */ new Set());
|
|
113
|
+
const [loading, setLoading] = useState(() => /* @__PURE__ */ new Set());
|
|
114
|
+
const inFlight = useRef(/* @__PURE__ */ new Set());
|
|
115
|
+
const loadRef = useRef(loadChildren);
|
|
116
|
+
loadRef.current = loadChildren;
|
|
117
|
+
const refocus = useRef(null);
|
|
118
|
+
const nodes = useMemo(() => {
|
|
119
|
+
const row = (parent, kind) => ({
|
|
120
|
+
id: parent.id + (kind === "error" ? FAILED : LOADING),
|
|
121
|
+
typeaheadLabel: "",
|
|
122
|
+
label: kind === "error" ? /* @__PURE__ */ jsxs("span", { className: "uix-tree__status uix-tree__status--error", children: [
|
|
123
|
+
labels.error,
|
|
124
|
+
" ",
|
|
125
|
+
/* @__PURE__ */ jsx("span", { className: "uix-tree__retry", children: labels.retry })
|
|
126
|
+
] }) : /* @__PURE__ */ jsxs("span", { className: "uix-tree__status", children: [
|
|
127
|
+
/* @__PURE__ */ jsx(Ring, {}),
|
|
128
|
+
labels.loading
|
|
129
|
+
] })
|
|
130
|
+
});
|
|
131
|
+
const decorate = (list) => list.map((node) => {
|
|
132
|
+
const known = node.children ?? loaded.get(node.id);
|
|
133
|
+
if (known) return { ...node, children: decorate(known) };
|
|
134
|
+
if (!node.hasChildren) return node;
|
|
135
|
+
return { ...node, children: [row(node, failed.has(node.id) ? "error" : "loading")] };
|
|
136
|
+
});
|
|
137
|
+
return loadChildren || nodesProp.some(function lazy(n) {
|
|
138
|
+
return !!n.hasChildren || !!n.children?.some(lazy);
|
|
139
|
+
}) ? decorate(nodesProp) : nodesProp;
|
|
140
|
+
}, [nodesProp, loaded, failed, loadChildren, labels.error, labels.retry, labels.loading]);
|
|
141
|
+
const load = useCallback((node) => {
|
|
142
|
+
const run = loadRef.current;
|
|
143
|
+
if (!run || inFlight.current.has(node.id)) return;
|
|
144
|
+
inFlight.current.add(node.id);
|
|
145
|
+
setFailed((prev) => {
|
|
146
|
+
if (!prev.has(node.id)) return prev;
|
|
147
|
+
const next = new Set(prev);
|
|
148
|
+
next.delete(node.id);
|
|
149
|
+
return next;
|
|
150
|
+
});
|
|
151
|
+
setLoading((prev) => new Set(prev).add(node.id));
|
|
152
|
+
const settle = (apply, focusNext) => {
|
|
153
|
+
inFlight.current.delete(node.id);
|
|
154
|
+
const active = typeof document !== "undefined" ? document.activeElement : null;
|
|
155
|
+
if (active?.dataset?.id === node.id + LOADING || active?.dataset?.id === node.id + FAILED) refocus.current = focusNext;
|
|
156
|
+
setLoading((prev) => {
|
|
157
|
+
const next = new Set(prev);
|
|
158
|
+
next.delete(node.id);
|
|
159
|
+
return next;
|
|
160
|
+
});
|
|
161
|
+
apply();
|
|
162
|
+
};
|
|
163
|
+
run(node).then(
|
|
164
|
+
(children) => settle(() => setLoaded((prev) => new Map(prev).set(node.id, children)), node.id),
|
|
165
|
+
() => settle(() => setFailed((prev) => new Set(prev).add(node.id)), node.id + FAILED)
|
|
166
|
+
);
|
|
167
|
+
}, []);
|
|
168
|
+
useEffect(() => {
|
|
169
|
+
if (!loadChildren) return;
|
|
170
|
+
const visit = (list) => {
|
|
171
|
+
for (const node of list) {
|
|
172
|
+
const known = node.children ?? loaded.get(node.id);
|
|
173
|
+
if (known) visit(known);
|
|
174
|
+
else if (node.hasChildren && expanded.has(node.id) && !failed.has(node.id)) load(node);
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
visit(nodesProp);
|
|
178
|
+
}, [nodesProp, expanded, loaded, failed, loadChildren, load]);
|
|
179
|
+
useEffect(() => {
|
|
180
|
+
const id = refocus.current;
|
|
181
|
+
if (id == null) return;
|
|
182
|
+
refocus.current = null;
|
|
183
|
+
setFocusedId(id);
|
|
184
|
+
document.querySelector(`[data-uix-tree="${CSS.escape(treeId)}"] [role="treeitem"][data-id="${CSS.escape(id)}"]`)?.focus();
|
|
185
|
+
}, [loaded, failed, loading, treeId]);
|
|
186
|
+
const findNode = (id, list = nodesProp) => {
|
|
187
|
+
for (const node of list) {
|
|
188
|
+
if (node.id === id) return node;
|
|
189
|
+
const inside = findNode(id, node.children ?? loaded.get(node.id) ?? []);
|
|
190
|
+
if (inside) return inside;
|
|
191
|
+
}
|
|
192
|
+
return void 0;
|
|
193
|
+
};
|
|
194
|
+
const handleSelect = (id) => {
|
|
195
|
+
const synthetic = syntheticOf(id);
|
|
196
|
+
if (!synthetic) {
|
|
197
|
+
onSelectProp?.(id);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if (synthetic.kind !== "error") return;
|
|
201
|
+
const parent = findNode(synthetic.parent);
|
|
202
|
+
if (!parent) return;
|
|
203
|
+
refocus.current = parent.id + LOADING;
|
|
204
|
+
setFailed((prev) => {
|
|
205
|
+
const next = new Set(prev);
|
|
206
|
+
next.delete(parent.id);
|
|
207
|
+
return next;
|
|
208
|
+
});
|
|
209
|
+
load(parent);
|
|
210
|
+
};
|
|
211
|
+
const onSelect = selectable || loadChildren ? handleSelect : void 0;
|
|
95
212
|
const handleToggle = useCallback((id) => {
|
|
96
213
|
if (!controlledExpanded) {
|
|
97
214
|
setInternalExpanded((prev) => {
|
|
@@ -121,6 +238,8 @@ function Tree({
|
|
|
121
238
|
focusedId,
|
|
122
239
|
setFocusedId,
|
|
123
240
|
rowToggles,
|
|
241
|
+
busy: loading,
|
|
242
|
+
treeId,
|
|
124
243
|
className,
|
|
125
244
|
...props
|
|
126
245
|
}
|
|
@@ -185,7 +304,7 @@ function Tree({
|
|
|
185
304
|
break;
|
|
186
305
|
}
|
|
187
306
|
};
|
|
188
|
-
return /* @__PURE__ */ jsx("ul", { ref: rootRef, className: cx("uix-tree", className), role: "tree", onKeyDown, ...props, children: nodes.map((node) => /* @__PURE__ */ jsx(
|
|
307
|
+
return /* @__PURE__ */ jsx("ul", { ref: rootRef, className: cx("uix-tree", className), role: "tree", "data-uix-tree": treeId, onKeyDown, ...props, children: nodes.map((node) => /* @__PURE__ */ jsx(
|
|
189
308
|
TreeNode,
|
|
190
309
|
{
|
|
191
310
|
node,
|
|
@@ -197,7 +316,8 @@ function Tree({
|
|
|
197
316
|
onSelect,
|
|
198
317
|
onFocusNode: setFocusedId,
|
|
199
318
|
level: 1,
|
|
200
|
-
rowToggles
|
|
319
|
+
rowToggles,
|
|
320
|
+
busy: loading
|
|
201
321
|
},
|
|
202
322
|
node.id
|
|
203
323
|
)) });
|
|
@@ -214,6 +334,8 @@ function VirtualTreeView({
|
|
|
214
334
|
focusedId,
|
|
215
335
|
setFocusedId,
|
|
216
336
|
rowToggles,
|
|
337
|
+
busy,
|
|
338
|
+
treeId,
|
|
217
339
|
className,
|
|
218
340
|
...props
|
|
219
341
|
}) {
|
|
@@ -273,6 +395,7 @@ function VirtualTreeView({
|
|
|
273
395
|
ref: scrollRef,
|
|
274
396
|
className: cx("uix-tree", "uix-tree--virtual", className),
|
|
275
397
|
role: "tree",
|
|
398
|
+
"data-uix-tree": treeId,
|
|
276
399
|
tabIndex: -1,
|
|
277
400
|
style: { maxHeight, overflow: "auto" },
|
|
278
401
|
onScroll: (e) => setScrollTop(e.currentTarget.scrollTop),
|
|
@@ -299,7 +422,8 @@ function VirtualTreeView({
|
|
|
299
422
|
"aria-setsize": f.setsize,
|
|
300
423
|
"aria-posinset": f.posinset,
|
|
301
424
|
"aria-expanded": f.hasChildren ? f.isExpanded : void 0,
|
|
302
|
-
"aria-selected": selectable ? selected === f.node.id : void 0,
|
|
425
|
+
"aria-selected": selectable && !syntheticOf(f.node.id) ? selected === f.node.id : void 0,
|
|
426
|
+
"aria-busy": busy.has(f.node.id) || void 0,
|
|
303
427
|
"data-id": f.node.id,
|
|
304
428
|
tabIndex: tabbableId === f.node.id || !windowHasTabbable && i === 0 ? 0 : -1,
|
|
305
429
|
className: "uix-tree__item",
|
|
@@ -346,5 +470,6 @@ function VirtualTreeView({
|
|
|
346
470
|
);
|
|
347
471
|
}
|
|
348
472
|
export {
|
|
473
|
+
DEFAULT_TREE_LABELS,
|
|
349
474
|
Tree
|
|
350
475
|
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* uix date-field model — pure helpers behind `DatePicker`, `DateTimePicker` and the field mode
|
|
3
|
+
* of `DateRangePicker` (HAR-1383): how a locale orders day / month / year, how a typed date is
|
|
4
|
+
* read, and how a date-time value is split and joined. No DOM, no React, no runtime import, so
|
|
5
|
+
* the unit test loads this file directly (date-field-model.test.mjs).
|
|
6
|
+
*
|
|
7
|
+
* A date is an ISO calendar date, `YYYY-MM-DD`. A date-time is a wall-clock value,
|
|
8
|
+
* `YYYY-MM-DDTHH:mm`, as `<input type="datetime-local">` holds it: no zone, no offset. Which
|
|
9
|
+
* zone that wall clock is in is the product's to know and to show.
|
|
10
|
+
*/
|
|
11
|
+
type DatePart = 'day' | 'month' | 'year';
|
|
12
|
+
/** Whether `YYYY-MM-DD` names a real calendar day (2026-02-30 does not). */
|
|
13
|
+
declare function isValidDateKey(value: string): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* The order in which `locale` writes a numeric date: `['day', 'month', 'year']` for de or
|
|
16
|
+
* en-GB, `['month', 'day', 'year']` for en-US, `['year', 'month', 'day']` for sv or ja.
|
|
17
|
+
*/
|
|
18
|
+
declare function dateOrder(locale?: string | readonly string[]): DatePart[];
|
|
19
|
+
/** A hint for the field, e.g. `DD.MM.YYYY`, `MM/DD/YYYY`, `YYYY-MM-DD`, in the locale's own shape. */
|
|
20
|
+
declare function datePattern(locale?: string | readonly string[]): string;
|
|
21
|
+
/** A numeric date in the locale's shape: 22.11.2026, 11/22/2026, 2026-11-22. */
|
|
22
|
+
declare function formatDateKey(value: string, locale?: string | readonly string[]): string;
|
|
23
|
+
/**
|
|
24
|
+
* Read a typed date. ISO `YYYY-MM-DD` is always accepted; otherwise three numbers separated by
|
|
25
|
+
* `.`, `/`, `-` or spaces are read in the locale's order (so `3.4.2026` is 3 April in German and
|
|
26
|
+
* `3/4/2026` is 4 March in en-US). A two-digit year means 2000–2099. Returns the ISO date, or
|
|
27
|
+
* `null` when the text is not a real date.
|
|
28
|
+
*/
|
|
29
|
+
declare function parseDateInput(text: string, locale?: string | readonly string[]): string | null;
|
|
30
|
+
/** Split `YYYY-MM-DDTHH:mm` (seconds are dropped) into its date and time. */
|
|
31
|
+
declare function splitDateTime(value: string | null | undefined): {
|
|
32
|
+
date: string | null;
|
|
33
|
+
time: string;
|
|
34
|
+
};
|
|
35
|
+
/** Join a date and a time into `YYYY-MM-DDTHH:mm`; without a date there is no value. */
|
|
36
|
+
declare function joinDateTime(date: string | null, time: string, fallbackTime?: string): string | null;
|
|
37
|
+
/** The short name of `timeZone` on that day ("CEST", "GMT+2"), for the label beside a time. */
|
|
38
|
+
declare function timeZoneName(timeZone: string, date: string | null, locale?: string | readonly string[]): string;
|
|
39
|
+
|
|
40
|
+
export { type DatePart, dateOrder, datePattern, formatDateKey, isValidDateKey, joinDateTime, parseDateInput, splitDateTime, timeZoneName };
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
const pad = (n, width = 2) => String(n).padStart(width, "0");
|
|
2
|
+
function isValidDateKey(value) {
|
|
3
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
|
|
4
|
+
if (!match) return false;
|
|
5
|
+
const [year, month, day] = [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
6
|
+
if (month < 1 || month > 12 || day < 1) return false;
|
|
7
|
+
return day <= new Date(Date.UTC(year, month, 0)).getUTCDate();
|
|
8
|
+
}
|
|
9
|
+
function dateOrder(locale) {
|
|
10
|
+
try {
|
|
11
|
+
const parts = new Intl.DateTimeFormat(locale, { year: "numeric", month: "2-digit", day: "2-digit", timeZone: "UTC" }).formatToParts(new Date(Date.UTC(2026, 10, 22)));
|
|
12
|
+
const order = parts.map((part) => part.type).filter((type) => type === "day" || type === "month" || type === "year");
|
|
13
|
+
if (order.length === 3 && new Set(order).size === 3) return order;
|
|
14
|
+
} catch {
|
|
15
|
+
}
|
|
16
|
+
return ["year", "month", "day"];
|
|
17
|
+
}
|
|
18
|
+
function datePattern(locale) {
|
|
19
|
+
const letters = { day: "DD", month: "MM", year: "YYYY" };
|
|
20
|
+
try {
|
|
21
|
+
return new Intl.DateTimeFormat(locale, { year: "numeric", month: "2-digit", day: "2-digit", timeZone: "UTC" }).formatToParts(new Date(Date.UTC(2026, 10, 22))).map((part) => part.type === "day" || part.type === "month" || part.type === "year" ? letters[part.type] : part.value).join("");
|
|
22
|
+
} catch {
|
|
23
|
+
return "YYYY-MM-DD";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function formatDateKey(value, locale) {
|
|
27
|
+
if (!isValidDateKey(value)) return value;
|
|
28
|
+
return new Intl.DateTimeFormat(locale, { year: "numeric", month: "2-digit", day: "2-digit", timeZone: "UTC" }).format(/* @__PURE__ */ new Date(`${value}T00:00:00Z`));
|
|
29
|
+
}
|
|
30
|
+
function parseDateInput(text, locale) {
|
|
31
|
+
const trimmed = text.trim();
|
|
32
|
+
if (!trimmed) return null;
|
|
33
|
+
if (/^\d{4}-\d{1,2}-\d{1,2}$/.test(trimmed)) {
|
|
34
|
+
const [year2, month, day] = trimmed.split("-").map(Number);
|
|
35
|
+
const iso2 = `${pad(year2, 4)}-${pad(month)}-${pad(day)}`;
|
|
36
|
+
return isValidDateKey(iso2) ? iso2 : null;
|
|
37
|
+
}
|
|
38
|
+
const numbers = trimmed.split(/[.\/\-\s]+/).filter(Boolean);
|
|
39
|
+
if (numbers.length !== 3 || numbers.some((n) => !/^\d{1,4}$/.test(n))) return null;
|
|
40
|
+
const order = dateOrder(locale);
|
|
41
|
+
const read = {};
|
|
42
|
+
order.forEach((part, index) => {
|
|
43
|
+
read[part] = Number(numbers[index]);
|
|
44
|
+
});
|
|
45
|
+
let year = read.year;
|
|
46
|
+
const yearText = numbers[order.indexOf("year")];
|
|
47
|
+
if (yearText.length <= 2) year += 2e3;
|
|
48
|
+
else if (yearText.length !== 4) return null;
|
|
49
|
+
const iso = `${pad(year, 4)}-${pad(read.month)}-${pad(read.day)}`;
|
|
50
|
+
return isValidDateKey(iso) ? iso : null;
|
|
51
|
+
}
|
|
52
|
+
function splitDateTime(value) {
|
|
53
|
+
const match = value ? /^(\d{4}-\d{2}-\d{2})(?:T(\d{2}:\d{2}))?/.exec(value) : null;
|
|
54
|
+
if (!match || !isValidDateKey(match[1])) return { date: null, time: "" };
|
|
55
|
+
return { date: match[1], time: match[2] ?? "" };
|
|
56
|
+
}
|
|
57
|
+
function joinDateTime(date, time, fallbackTime = "00:00") {
|
|
58
|
+
if (!date) return null;
|
|
59
|
+
return `${date}T${/^\d{2}:\d{2}/.test(time) ? time.slice(0, 5) : fallbackTime}`;
|
|
60
|
+
}
|
|
61
|
+
function timeZoneName(timeZone, date, locale) {
|
|
62
|
+
try {
|
|
63
|
+
const at = date && isValidDateKey(date) ? /* @__PURE__ */ new Date(`${date}T12:00:00Z`) : /* @__PURE__ */ new Date();
|
|
64
|
+
const part = new Intl.DateTimeFormat(locale, { timeZone, timeZoneName: "short" }).formatToParts(at).find((p) => p.type === "timeZoneName");
|
|
65
|
+
return part?.value ?? timeZone;
|
|
66
|
+
} catch {
|
|
67
|
+
return timeZone;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
export {
|
|
71
|
+
dateOrder,
|
|
72
|
+
datePattern,
|
|
73
|
+
formatDateKey,
|
|
74
|
+
isValidDateKey,
|
|
75
|
+
joinDateTime,
|
|
76
|
+
parseDateInput,
|
|
77
|
+
splitDateTime,
|
|
78
|
+
timeZoneName
|
|
79
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `true` while the page is printed, or shown with print media.
|
|
3
|
+
*
|
|
4
|
+
* A view that mounts only the rows near its viewport uses this to mount all of them for the
|
|
5
|
+
* paper. The browser lays the page out for print as soon as the `beforeprint` handlers return,
|
|
6
|
+
* so the state is committed inside the event (`flushSync`), not in a later task.
|
|
7
|
+
*
|
|
8
|
+
* It cannot answer in time when `window.print()` is called from inside a React effect or
|
|
9
|
+
* lifecycle method, on mount or later: React commits the change after the effect, which is
|
|
10
|
+
* after the page was laid out. A page that prints itself that way turns windowing off through
|
|
11
|
+
* the component's own prop instead, or calls `print()` from an event handler or a timer.
|
|
12
|
+
*/
|
|
13
|
+
declare function usePrinting(enabled?: boolean): boolean;
|
|
14
|
+
|
|
15
|
+
export { usePrinting };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { flushSync } from "react-dom";
|
|
4
|
+
function usePrinting(enabled = true) {
|
|
5
|
+
const [printing, setPrinting] = useState(false);
|
|
6
|
+
useEffect(() => {
|
|
7
|
+
if (!enabled || typeof window === "undefined") return void 0;
|
|
8
|
+
const set = (next) => {
|
|
9
|
+
flushSync(() => setPrinting(next));
|
|
10
|
+
};
|
|
11
|
+
const before = () => set(true);
|
|
12
|
+
const after = () => set(false);
|
|
13
|
+
const query = typeof window.matchMedia === "function" ? window.matchMedia("print") : null;
|
|
14
|
+
const changed = (event) => set(event.matches);
|
|
15
|
+
window.addEventListener("beforeprint", before);
|
|
16
|
+
window.addEventListener("afterprint", after);
|
|
17
|
+
query?.addEventListener?.("change", changed);
|
|
18
|
+
setPrinting(Boolean(query?.matches));
|
|
19
|
+
return () => {
|
|
20
|
+
window.removeEventListener("beforeprint", before);
|
|
21
|
+
window.removeEventListener("afterprint", after);
|
|
22
|
+
query?.removeEventListener?.("change", changed);
|
|
23
|
+
};
|
|
24
|
+
}, [enabled]);
|
|
25
|
+
return enabled && printing;
|
|
26
|
+
}
|
|
27
|
+
export {
|
|
28
|
+
usePrinting
|
|
29
|
+
};
|