@tensor_1/react 2.42.0 → 2.43.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 +24 -0
- package/dist/components/OrgChart.d.ts +121 -0
- package/dist/components/OrgChart.js +683 -0
- package/dist/index.cjs +1569 -888
- package/dist/index.d.cts +120 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +5 -0
- package/dist/labels-context.d.ts +3 -0
- package/package.json +1 -1
|
@@ -0,0 +1,683 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { cx } from "../cx.js";
|
|
5
|
+
import { fillLabel } from "../fill-label.js";
|
|
6
|
+
import { useUixLabels } from "../labels-context.js";
|
|
7
|
+
import { Avatar } from "./Avatar.js";
|
|
8
|
+
import { Button } from "./Button.js";
|
|
9
|
+
const DEFAULT_ORG_CHART_LABELS = {
|
|
10
|
+
chart: "Org chart",
|
|
11
|
+
loading: "Loading\u2026",
|
|
12
|
+
error: "Could not load.",
|
|
13
|
+
retry: "Retry",
|
|
14
|
+
reportsOne: "{count} direct report",
|
|
15
|
+
reportsMany: "{count} direct reports",
|
|
16
|
+
expand: "Show reports",
|
|
17
|
+
collapse: "Hide reports",
|
|
18
|
+
toolbar: "Zoom",
|
|
19
|
+
zoomIn: "Zoom in",
|
|
20
|
+
zoomOut: "Zoom out",
|
|
21
|
+
fit: "Fit",
|
|
22
|
+
zoomLevel: "{percent}%",
|
|
23
|
+
resetZoom: "{percent}, reset to 100%"
|
|
24
|
+
};
|
|
25
|
+
const ZOOM_STEPS = [0.25, 0.33, 0.5, 0.67, 0.8, 0.9, 1, 1.1, 1.25, 1.5];
|
|
26
|
+
const MIN_ZOOM = ZOOM_STEPS[0];
|
|
27
|
+
const MAX_ZOOM = ZOOM_STEPS[ZOOM_STEPS.length - 1];
|
|
28
|
+
const clampZoom = (z) => Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, z));
|
|
29
|
+
const orgChartZoomStep = (zoom, dir) => {
|
|
30
|
+
if (dir > 0) return ZOOM_STEPS.find((s) => s > zoom + 1e-3) ?? MAX_ZOOM;
|
|
31
|
+
return [...ZOOM_STEPS].reverse().find((s) => s < zoom - 1e-3) ?? MIN_ZOOM;
|
|
32
|
+
};
|
|
33
|
+
const orgChartInitials = (name) => {
|
|
34
|
+
const words = name.trim().split(/\s+/).filter(Boolean);
|
|
35
|
+
if (words.length === 0) return "";
|
|
36
|
+
const first = words[0];
|
|
37
|
+
const last = words.length > 1 ? words[words.length - 1] : "";
|
|
38
|
+
return (Array.from(first)[0] ?? "").concat(last ? Array.from(last)[0] ?? "" : "").toUpperCase();
|
|
39
|
+
};
|
|
40
|
+
const LOADING = "::uix-loading";
|
|
41
|
+
const FAILED = "::uix-error";
|
|
42
|
+
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;
|
|
43
|
+
const idRef = (chartId, nodeId, part) => {
|
|
44
|
+
let hash = 5381;
|
|
45
|
+
for (let i = 0; i < nodeId.length; i += 1) hash = (hash * 33 ^ nodeId.charCodeAt(i)) >>> 0;
|
|
46
|
+
return `${chartId}-${nodeId.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 40)}-${hash.toString(36)}-${part}`;
|
|
47
|
+
};
|
|
48
|
+
const isExpandable = (node, known, lazy) => known ? known.length > 0 : lazy && (node.reportCount ?? 0) > 0;
|
|
49
|
+
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" }) });
|
|
50
|
+
const PlusIcon = () => /* @__PURE__ */ jsx("svg", { viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: "M8 3v10M3 8h10" }) });
|
|
51
|
+
const MinusIcon = () => /* @__PURE__ */ jsx("svg", { viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: "M3 8h10" }) });
|
|
52
|
+
const FitIcon = () => /* @__PURE__ */ jsx("svg", { viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: "M2 6V2h4M14 6V2h-4M2 10v4h4M14 10v4h-4" }) });
|
|
53
|
+
const useIsoLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
|
|
54
|
+
const Ring = () => /* @__PURE__ */ jsx("span", { className: "uix-spinner uix-spinner--sm", "aria-hidden": "true" });
|
|
55
|
+
function StatusItem({ parent, kind, level, ctx }) {
|
|
56
|
+
const id = parent + (kind === "error" ? FAILED : LOADING);
|
|
57
|
+
return /* @__PURE__ */ jsx(
|
|
58
|
+
"li",
|
|
59
|
+
{
|
|
60
|
+
role: "treeitem",
|
|
61
|
+
"aria-level": level,
|
|
62
|
+
"aria-setsize": 1,
|
|
63
|
+
"aria-posinset": 1,
|
|
64
|
+
"data-id": id,
|
|
65
|
+
tabIndex: ctx.tabbableId === id ? 0 : -1,
|
|
66
|
+
className: "uix-org-chart__item uix-org-chart__item--status",
|
|
67
|
+
onFocus: (e) => {
|
|
68
|
+
if (e.target === e.currentTarget) ctx.onFocusItem(id);
|
|
69
|
+
},
|
|
70
|
+
children: kind === "error" ? /* @__PURE__ */ jsxs("span", { className: "uix-org-chart__status uix-org-chart__status--error", onClick: () => ctx.onActivate(id), children: [
|
|
71
|
+
ctx.labels.error,
|
|
72
|
+
" ",
|
|
73
|
+
/* @__PURE__ */ jsx("span", { className: "uix-org-chart__retry", children: ctx.labels.retry })
|
|
74
|
+
] }) : /* @__PURE__ */ jsxs("span", { className: "uix-org-chart__status", children: [
|
|
75
|
+
/* @__PURE__ */ jsx(Ring, {}),
|
|
76
|
+
ctx.labels.loading
|
|
77
|
+
] })
|
|
78
|
+
}
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
function OrgChartItem({ node, level, size, pos, ctx }) {
|
|
82
|
+
const known = node.children ?? ctx.loaded.get(node.id);
|
|
83
|
+
const expandable = isExpandable(node, known, ctx.lazy);
|
|
84
|
+
const open = expandable && ctx.expanded.has(node.id);
|
|
85
|
+
const count = known ? known.length : node.reportCount ?? 0;
|
|
86
|
+
const nameId = idRef(ctx.chartId, node.id, "name");
|
|
87
|
+
const descId = idRef(ctx.chartId, node.id, "desc");
|
|
88
|
+
const reportsText = count > 0 ? fillLabel(count === 1 ? ctx.labels.reportsOne : ctx.labels.reportsMany, { count }) : "";
|
|
89
|
+
const waiting = open && !known;
|
|
90
|
+
const status = waiting ? ctx.failed.has(node.id) ? "error" : "loading" : null;
|
|
91
|
+
const children = open && known && known.length > 0 ? known : null;
|
|
92
|
+
const stack = ctx.stackLeaves && children !== null && children.every((c) => !isExpandable(c, c.children ?? ctx.loaded.get(c.id), ctx.lazy));
|
|
93
|
+
return /* @__PURE__ */ jsxs(
|
|
94
|
+
"li",
|
|
95
|
+
{
|
|
96
|
+
role: "treeitem",
|
|
97
|
+
"aria-level": level,
|
|
98
|
+
"aria-setsize": size,
|
|
99
|
+
"aria-posinset": pos,
|
|
100
|
+
"aria-expanded": expandable ? open : void 0,
|
|
101
|
+
"aria-selected": ctx.selectable ? ctx.selected === node.id : void 0,
|
|
102
|
+
"aria-busy": ctx.loading.has(node.id) || void 0,
|
|
103
|
+
"aria-labelledby": nameId,
|
|
104
|
+
"aria-describedby": descId,
|
|
105
|
+
"data-id": node.id,
|
|
106
|
+
tabIndex: ctx.tabbableId === node.id ? 0 : -1,
|
|
107
|
+
className: "uix-org-chart__item",
|
|
108
|
+
onFocus: (e) => {
|
|
109
|
+
if (e.target === e.currentTarget) ctx.onFocusItem(node.id);
|
|
110
|
+
},
|
|
111
|
+
children: [
|
|
112
|
+
/* @__PURE__ */ jsxs("div", { className: "uix-org-chart__node", onClick: () => ctx.onActivate(node.id), children: [
|
|
113
|
+
/* @__PURE__ */ jsx(Avatar, { size: "lg", src: node.photo, children: node.initials ?? orgChartInitials(node.name) }),
|
|
114
|
+
/* @__PURE__ */ jsxs("span", { className: "uix-org-chart__body", children: [
|
|
115
|
+
/* @__PURE__ */ jsx("span", { className: "uix-org-chart__name", id: nameId, children: node.name }),
|
|
116
|
+
/* @__PURE__ */ jsxs("span", { id: descId, className: "uix-org-chart__desc", children: [
|
|
117
|
+
node.title ? /* @__PURE__ */ jsx("span", { className: "uix-org-chart__title", children: node.title }) : null,
|
|
118
|
+
node.team || node.location ? /* @__PURE__ */ jsxs("span", { className: "uix-org-chart__meta", children: [
|
|
119
|
+
node.team ? /* @__PURE__ */ jsx("span", { children: node.team }) : null,
|
|
120
|
+
node.location ? /* @__PURE__ */ jsx("span", { children: node.location }) : null
|
|
121
|
+
] }) : null,
|
|
122
|
+
node.badge ? /* @__PURE__ */ jsx("span", { className: "uix-org-chart__badge", children: node.badge }) : null,
|
|
123
|
+
reportsText ? /* @__PURE__ */ jsx("span", { className: "uix-visually-hidden", children: reportsText }) : null
|
|
124
|
+
] })
|
|
125
|
+
] }),
|
|
126
|
+
expandable ? (
|
|
127
|
+
// A pointer target for showing the reports without opening the person. Keyboard users
|
|
128
|
+
// toggle with Space or the arrow keys on the item, so it stays out of the tab order and
|
|
129
|
+
// never takes focus (the item keeps it), and the item's aria-expanded carries its state.
|
|
130
|
+
/* @__PURE__ */ jsxs(
|
|
131
|
+
"button",
|
|
132
|
+
{
|
|
133
|
+
type: "button",
|
|
134
|
+
tabIndex: -1,
|
|
135
|
+
"aria-hidden": "true",
|
|
136
|
+
title: open ? ctx.labels.collapse : ctx.labels.expand,
|
|
137
|
+
className: "uix-org-chart__toggle",
|
|
138
|
+
onMouseDown: (e) => e.preventDefault(),
|
|
139
|
+
onClick: (e) => {
|
|
140
|
+
e.stopPropagation();
|
|
141
|
+
ctx.onToggle(node.id);
|
|
142
|
+
},
|
|
143
|
+
children: [
|
|
144
|
+
/* @__PURE__ */ jsx(ChevronIcon, {}),
|
|
145
|
+
count > 0 ? count : null
|
|
146
|
+
]
|
|
147
|
+
}
|
|
148
|
+
)
|
|
149
|
+
) : null
|
|
150
|
+
] }),
|
|
151
|
+
status ? /* @__PURE__ */ jsx("ul", { role: "group", className: "uix-org-chart__group", children: /* @__PURE__ */ jsx(StatusItem, { parent: node.id, kind: status, level: level + 1, ctx }) }) : children ? /* @__PURE__ */ jsx("ul", { role: "group", className: "uix-org-chart__group", "data-stack": stack ? "" : void 0, children: children.map((child, i) => /* @__PURE__ */ jsx(OrgChartItem, { node: child, level: level + 1, size: children.length, pos: i + 1, ctx }, child.id)) }) : null
|
|
152
|
+
]
|
|
153
|
+
}
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
function OrgChart({
|
|
157
|
+
nodes,
|
|
158
|
+
expanded: controlledExpanded,
|
|
159
|
+
defaultExpanded,
|
|
160
|
+
onToggle,
|
|
161
|
+
selected,
|
|
162
|
+
onSelect,
|
|
163
|
+
loadChildren,
|
|
164
|
+
revealId,
|
|
165
|
+
onReveal,
|
|
166
|
+
stackLeaves = true,
|
|
167
|
+
defaultZoom = 1,
|
|
168
|
+
labels: labelOverrides,
|
|
169
|
+
className,
|
|
170
|
+
"aria-label": ariaLabel,
|
|
171
|
+
"aria-labelledby": ariaLabelledby,
|
|
172
|
+
...props
|
|
173
|
+
}) {
|
|
174
|
+
const labels = { ...DEFAULT_ORG_CHART_LABELS, ...useUixLabels().orgChart, ...labelOverrides };
|
|
175
|
+
const chartId = useId().replace(/[^A-Za-z0-9_-]/g, "");
|
|
176
|
+
const [internalExpanded, setInternalExpanded] = useState(() => defaultExpanded ?? /* @__PURE__ */ new Set());
|
|
177
|
+
const expanded = controlledExpanded ?? internalExpanded;
|
|
178
|
+
const treeRef = useRef(null);
|
|
179
|
+
const viewportRef = useRef(null);
|
|
180
|
+
const [focusedId, setFocusedId] = useState(void 0);
|
|
181
|
+
const selectable = selected !== void 0 || onSelect != null;
|
|
182
|
+
const lazy = Boolean(loadChildren);
|
|
183
|
+
const userMoved = useRef(false);
|
|
184
|
+
const placed = useRef(null);
|
|
185
|
+
const scrollFrame = (left, top) => {
|
|
186
|
+
const viewport = viewportRef.current;
|
|
187
|
+
if (!viewport) return;
|
|
188
|
+
viewport.scrollLeft = left;
|
|
189
|
+
viewport.scrollTop = top;
|
|
190
|
+
placed.current = { left: viewport.scrollLeft, top: viewport.scrollTop };
|
|
191
|
+
};
|
|
192
|
+
const [loaded, setLoaded] = useState(() => /* @__PURE__ */ new Map());
|
|
193
|
+
const [failed, setFailed] = useState(() => /* @__PURE__ */ new Set());
|
|
194
|
+
const [loading, setLoading] = useState(() => /* @__PURE__ */ new Set());
|
|
195
|
+
const [announcement, setAnnouncement] = useState("");
|
|
196
|
+
const inFlight = useRef(/* @__PURE__ */ new Set());
|
|
197
|
+
const loadRef = useRef(loadChildren);
|
|
198
|
+
loadRef.current = loadChildren;
|
|
199
|
+
const errorWord = useRef(labels.error);
|
|
200
|
+
errorWord.current = labels.error;
|
|
201
|
+
const refocus = useRef(null);
|
|
202
|
+
const load = useCallback((node) => {
|
|
203
|
+
const run = loadRef.current;
|
|
204
|
+
if (!run || inFlight.current.has(node.id)) return;
|
|
205
|
+
inFlight.current.add(node.id);
|
|
206
|
+
setFailed((prev) => {
|
|
207
|
+
if (!prev.has(node.id)) return prev;
|
|
208
|
+
const next = new Set(prev);
|
|
209
|
+
next.delete(node.id);
|
|
210
|
+
return next;
|
|
211
|
+
});
|
|
212
|
+
setLoading((prev) => new Set(prev).add(node.id));
|
|
213
|
+
setAnnouncement("");
|
|
214
|
+
const settle = (apply, focusNext) => {
|
|
215
|
+
inFlight.current.delete(node.id);
|
|
216
|
+
const active = typeof document !== "undefined" ? document.activeElement : null;
|
|
217
|
+
if (active?.dataset?.id === node.id + LOADING || active?.dataset?.id === node.id + FAILED) refocus.current = focusNext;
|
|
218
|
+
setLoading((prev) => {
|
|
219
|
+
const next = new Set(prev);
|
|
220
|
+
next.delete(node.id);
|
|
221
|
+
return next;
|
|
222
|
+
});
|
|
223
|
+
apply();
|
|
224
|
+
};
|
|
225
|
+
run(node).then(
|
|
226
|
+
// Focus that waited on the loading row moves on to the first report, the key's intent.
|
|
227
|
+
(children) => settle(() => setLoaded((prev) => new Map(prev).set(node.id, children)), children[0]?.id ?? node.id),
|
|
228
|
+
() => settle(() => {
|
|
229
|
+
setFailed((prev) => new Set(prev).add(node.id));
|
|
230
|
+
setAnnouncement(errorWord.current);
|
|
231
|
+
}, node.id + FAILED)
|
|
232
|
+
);
|
|
233
|
+
}, []);
|
|
234
|
+
useEffect(() => {
|
|
235
|
+
if (!loadChildren) return;
|
|
236
|
+
const visit = (list) => {
|
|
237
|
+
for (const node of list) {
|
|
238
|
+
if (!expanded.has(node.id)) continue;
|
|
239
|
+
const known = node.children ?? loaded.get(node.id);
|
|
240
|
+
if (known) visit(known);
|
|
241
|
+
else if ((node.reportCount ?? 0) > 0 && !failed.has(node.id)) load(node);
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
visit(nodes);
|
|
245
|
+
}, [nodes, expanded, loaded, failed, loadChildren, load]);
|
|
246
|
+
const itemEl = useCallback((id) => treeRef.current?.querySelector(`[role="treeitem"][data-id="${CSS.escape(id)}"]`) ?? null, []);
|
|
247
|
+
const cardOf = (el) => el?.querySelector(":scope > .uix-org-chart__node, :scope > .uix-org-chart__status") ?? el;
|
|
248
|
+
useEffect(() => {
|
|
249
|
+
const id = refocus.current;
|
|
250
|
+
if (id == null) return;
|
|
251
|
+
refocus.current = null;
|
|
252
|
+
setFocusedId(id);
|
|
253
|
+
itemEl(id)?.focus();
|
|
254
|
+
}, [loaded, failed, loading, itemEl]);
|
|
255
|
+
const findPath = useCallback((id, list = nodes, path = []) => {
|
|
256
|
+
for (const node of list) {
|
|
257
|
+
if (node.id === id) return [...path, node];
|
|
258
|
+
const inside = findPath(id, node.children ?? loaded.get(node.id) ?? [], [...path, node]);
|
|
259
|
+
if (inside) return inside;
|
|
260
|
+
}
|
|
261
|
+
return null;
|
|
262
|
+
}, [nodes, loaded]);
|
|
263
|
+
const anchor = useRef(null);
|
|
264
|
+
const viewportBox = () => viewportRef.current?.getBoundingClientRect();
|
|
265
|
+
const holdInPlace = (id) => {
|
|
266
|
+
const card = cardOf(itemEl(id));
|
|
267
|
+
const frame = viewportBox();
|
|
268
|
+
if (!card || !frame || !viewportRef.current || layoutOf(viewportRef.current) === "list") {
|
|
269
|
+
anchor.current = null;
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
const box = card.getBoundingClientRect();
|
|
273
|
+
anchor.current = { id, x: box.left - frame.left, y: box.top - frame.top };
|
|
274
|
+
};
|
|
275
|
+
useIsoLayoutEffect(() => {
|
|
276
|
+
const a = anchor.current;
|
|
277
|
+
const viewport = viewportRef.current;
|
|
278
|
+
if (!a || !viewport) return;
|
|
279
|
+
const card = cardOf(itemEl(a.id));
|
|
280
|
+
const frame = viewport.getBoundingClientRect();
|
|
281
|
+
if (card) {
|
|
282
|
+
const box = card.getBoundingClientRect();
|
|
283
|
+
scrollFrame(viewport.scrollLeft + box.left - frame.left - a.x, viewport.scrollTop + box.top - frame.top - a.y);
|
|
284
|
+
}
|
|
285
|
+
if (!card || !inFlight.current.has(a.id) && !itemEl(a.id + LOADING)) anchor.current = null;
|
|
286
|
+
});
|
|
287
|
+
const toggle = useCallback((id) => {
|
|
288
|
+
const opening = !expanded.has(id);
|
|
289
|
+
userMoved.current = true;
|
|
290
|
+
holdInPlace(id);
|
|
291
|
+
const el = itemEl(id);
|
|
292
|
+
const active = typeof document !== "undefined" ? document.activeElement : null;
|
|
293
|
+
if (!opening && el && active && active !== el && el.contains(active)) {
|
|
294
|
+
el.focus();
|
|
295
|
+
setFocusedId(id);
|
|
296
|
+
}
|
|
297
|
+
if (!controlledExpanded) {
|
|
298
|
+
setInternalExpanded((prev) => {
|
|
299
|
+
const next = new Set(prev);
|
|
300
|
+
if (next.has(id)) next.delete(id);
|
|
301
|
+
else next.add(id);
|
|
302
|
+
return next;
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
onToggle?.(id, opening);
|
|
306
|
+
}, [expanded, controlledExpanded, onToggle, itemEl]);
|
|
307
|
+
const retry = useCallback((parentId) => {
|
|
308
|
+
const path = findPath(parentId);
|
|
309
|
+
const parent = path ? path[path.length - 1] : void 0;
|
|
310
|
+
if (!parent) return;
|
|
311
|
+
refocus.current = parent.id + LOADING;
|
|
312
|
+
setFailed((prev) => {
|
|
313
|
+
const next = new Set(prev);
|
|
314
|
+
next.delete(parent.id);
|
|
315
|
+
return next;
|
|
316
|
+
});
|
|
317
|
+
load(parent);
|
|
318
|
+
}, [findPath, load]);
|
|
319
|
+
const select = useCallback((id) => {
|
|
320
|
+
const synthetic = syntheticOf(id);
|
|
321
|
+
if (!synthetic) {
|
|
322
|
+
onSelect?.(id);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
if (synthetic.kind === "error") retry(synthetic.parent);
|
|
326
|
+
}, [onSelect, retry]);
|
|
327
|
+
const rendered = useMemo(() => {
|
|
328
|
+
const ids = /* @__PURE__ */ new Set();
|
|
329
|
+
const visit = (list) => {
|
|
330
|
+
for (const node of list) {
|
|
331
|
+
ids.add(node.id);
|
|
332
|
+
if (!expanded.has(node.id)) continue;
|
|
333
|
+
const known = node.children ?? loaded.get(node.id);
|
|
334
|
+
if (known) visit(known);
|
|
335
|
+
else if (lazy && (node.reportCount ?? 0) > 0) ids.add(node.id + (failed.has(node.id) ? FAILED : LOADING));
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
visit(nodes);
|
|
339
|
+
return ids;
|
|
340
|
+
}, [nodes, expanded, loaded, failed, lazy]);
|
|
341
|
+
const tabbableId = (focusedId && rendered.has(focusedId) ? focusedId : void 0) ?? (selected && rendered.has(selected) ? selected : void 0) ?? nodes[0]?.id;
|
|
342
|
+
useIsoLayoutEffect(() => {
|
|
343
|
+
if (!focusedId || rendered.has(focusedId) || typeof document === "undefined") return;
|
|
344
|
+
const active = document.activeElement;
|
|
345
|
+
if (active && active !== document.body) return;
|
|
346
|
+
const target = syntheticOf(focusedId)?.parent ?? focusedId;
|
|
347
|
+
const line = findPath(target) ?? [];
|
|
348
|
+
const visible = [...line].reverse().find((n) => rendered.has(n.id));
|
|
349
|
+
if (visible) {
|
|
350
|
+
setFocusedId(visible.id);
|
|
351
|
+
itemEl(visible.id)?.focus();
|
|
352
|
+
}
|
|
353
|
+
}, [rendered]);
|
|
354
|
+
const activate = useCallback((id) => {
|
|
355
|
+
setFocusedId(id);
|
|
356
|
+
itemEl(id)?.focus({ preventScroll: true });
|
|
357
|
+
select(id);
|
|
358
|
+
}, [itemEl, select]);
|
|
359
|
+
const revealed = useRef(null);
|
|
360
|
+
useEffect(() => {
|
|
361
|
+
if (!revealId) {
|
|
362
|
+
revealed.current = null;
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
if (revealed.current === revealId) return;
|
|
366
|
+
const el = itemEl(revealId);
|
|
367
|
+
if (!el) return;
|
|
368
|
+
revealed.current = revealId;
|
|
369
|
+
userMoved.current = true;
|
|
370
|
+
cardOf(el)?.scrollIntoView?.({ block: "center", inline: "center" });
|
|
371
|
+
el.focus({ preventScroll: true });
|
|
372
|
+
setFocusedId(revealId);
|
|
373
|
+
onReveal?.(revealId);
|
|
374
|
+
});
|
|
375
|
+
const [zoom, setZoom] = useState(() => typeof defaultZoom === "number" ? clampZoom(defaultZoom) : 1);
|
|
376
|
+
const zoomCentre = useRef(null);
|
|
377
|
+
const zoomTo = (next) => {
|
|
378
|
+
const viewport = viewportRef.current;
|
|
379
|
+
if (viewport) {
|
|
380
|
+
zoomCentre.current = {
|
|
381
|
+
x: (viewport.scrollLeft + viewport.clientWidth / 2) / zoom,
|
|
382
|
+
y: (viewport.scrollTop + viewport.clientHeight / 2) / zoom,
|
|
383
|
+
from: zoom
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
setZoom(clampZoom(next));
|
|
387
|
+
};
|
|
388
|
+
useIsoLayoutEffect(() => {
|
|
389
|
+
const c = zoomCentre.current;
|
|
390
|
+
const viewport = viewportRef.current;
|
|
391
|
+
if (!c || !viewport || c.from === zoom) return;
|
|
392
|
+
zoomCentre.current = null;
|
|
393
|
+
scrollFrame(c.x * zoom - viewport.clientWidth / 2, c.y * zoom - viewport.clientHeight / 2);
|
|
394
|
+
}, [zoom]);
|
|
395
|
+
const fitZoom = useCallback((current) => {
|
|
396
|
+
const viewport = viewportRef.current;
|
|
397
|
+
const tree = treeRef.current;
|
|
398
|
+
if (!viewport || !tree) return null;
|
|
399
|
+
const cs = getComputedStyle(viewport);
|
|
400
|
+
const room = viewport.clientWidth - (parseFloat(cs.paddingLeft) || 0) - (parseFloat(cs.paddingRight) || 0);
|
|
401
|
+
const width = tree.getBoundingClientRect().width / current;
|
|
402
|
+
if (!(room > 0) || !(width > 0)) return null;
|
|
403
|
+
return clampZoom(Math.min(1, room / width));
|
|
404
|
+
}, []);
|
|
405
|
+
const settledWidth = useRef(-1);
|
|
406
|
+
useIsoLayoutEffect(() => {
|
|
407
|
+
const viewport = viewportRef.current;
|
|
408
|
+
const tree = treeRef.current;
|
|
409
|
+
if (userMoved.current || !viewport || !tree || nodes.length === 0) return;
|
|
410
|
+
const width = Math.round(tree.getBoundingClientRect().width);
|
|
411
|
+
if (width === settledWidth.current) return;
|
|
412
|
+
if (defaultZoom === "fit") {
|
|
413
|
+
const next = fitZoom(zoom);
|
|
414
|
+
if (next !== null && Math.abs(next - zoom) > 5e-3) {
|
|
415
|
+
setZoom(next);
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
settledWidth.current = width;
|
|
420
|
+
if (viewport.scrollWidth > viewport.clientWidth) scrollFrame((viewport.scrollWidth - viewport.clientWidth) / 2, viewport.scrollTop);
|
|
421
|
+
});
|
|
422
|
+
const onScroll = () => {
|
|
423
|
+
const viewport = viewportRef.current;
|
|
424
|
+
const at = placed.current;
|
|
425
|
+
if (!viewport) return;
|
|
426
|
+
if (at && Math.abs(viewport.scrollLeft - at.left) <= 1 && Math.abs(viewport.scrollTop - at.top) <= 1) return;
|
|
427
|
+
userMoved.current = true;
|
|
428
|
+
};
|
|
429
|
+
const pan = useRef(null);
|
|
430
|
+
const [panning, setPanning] = useState(false);
|
|
431
|
+
const onPointerDown = (e) => {
|
|
432
|
+
if (e.button !== 0 || e.pointerType === "touch") return;
|
|
433
|
+
if (e.target.closest(".uix-org-chart__node, .uix-org-chart__status")) return;
|
|
434
|
+
const viewport = viewportRef.current;
|
|
435
|
+
if (!viewport || layoutOf(viewport) === "list") return;
|
|
436
|
+
const frame = viewport.getBoundingClientRect();
|
|
437
|
+
const x = e.clientX - frame.left - viewport.clientLeft;
|
|
438
|
+
const y = e.clientY - frame.top - viewport.clientTop;
|
|
439
|
+
if (x > viewport.clientWidth || y > viewport.clientHeight) return;
|
|
440
|
+
pan.current = { x: e.clientX, y: e.clientY, left: viewport.scrollLeft, top: viewport.scrollTop, id: e.pointerId };
|
|
441
|
+
userMoved.current = true;
|
|
442
|
+
viewport.setPointerCapture?.(e.pointerId);
|
|
443
|
+
setPanning(true);
|
|
444
|
+
};
|
|
445
|
+
const onMouseDown = (e) => {
|
|
446
|
+
if (pan.current) e.preventDefault();
|
|
447
|
+
};
|
|
448
|
+
const onPointerMove = (e) => {
|
|
449
|
+
const p = pan.current;
|
|
450
|
+
const viewport = viewportRef.current;
|
|
451
|
+
if (!p || !viewport || e.pointerId !== p.id) return;
|
|
452
|
+
scrollFrame(p.left - (e.clientX - p.x), p.top - (e.clientY - p.y));
|
|
453
|
+
};
|
|
454
|
+
const endPan = (e) => {
|
|
455
|
+
if (!pan.current || e.pointerId !== pan.current.id) return;
|
|
456
|
+
viewportRef.current?.releasePointerCapture?.(e.pointerId);
|
|
457
|
+
pan.current = null;
|
|
458
|
+
setPanning(false);
|
|
459
|
+
};
|
|
460
|
+
const focusItem = (el) => {
|
|
461
|
+
if (el) {
|
|
462
|
+
el.focus();
|
|
463
|
+
setFocusedId(el.dataset.id);
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
const onKeyDown = (e) => {
|
|
467
|
+
const cur = e.target.closest('[role="treeitem"]');
|
|
468
|
+
const tree = treeRef.current;
|
|
469
|
+
if (!cur || !tree?.contains(cur) || e.altKey || e.ctrlKey || e.metaKey) return;
|
|
470
|
+
const items = Array.from(tree.querySelectorAll('[role="treeitem"]'));
|
|
471
|
+
const idx = items.indexOf(cur);
|
|
472
|
+
const id = cur.dataset.id;
|
|
473
|
+
const state = cur.getAttribute("aria-expanded");
|
|
474
|
+
const parent = cur.parentElement?.closest('[role="treeitem"]') ?? null;
|
|
475
|
+
const firstChild = cur.querySelector(':scope > [role="group"] > [role="treeitem"]');
|
|
476
|
+
const column = cur.parentElement;
|
|
477
|
+
const stacked = column?.hasAttribute("data-stack") ?? false;
|
|
478
|
+
const sameLevel = (dir) => {
|
|
479
|
+
const level = cur.getAttribute("aria-level");
|
|
480
|
+
const row = items.filter((el) => el.getAttribute("aria-level") === level);
|
|
481
|
+
const edge = stacked && column ? dir > 0 ? column.lastElementChild : column.firstElementChild : cur;
|
|
482
|
+
return row[row.indexOf(edge ?? cur) + dir];
|
|
483
|
+
};
|
|
484
|
+
const sibling = (dir) => dir > 0 ? cur.nextElementSibling : cur.previousElementSibling;
|
|
485
|
+
const list = viewportRef.current ? layoutOf(viewportRef.current) === "list" : false;
|
|
486
|
+
const synthetic = syntheticOf(id);
|
|
487
|
+
let handled = true;
|
|
488
|
+
switch (e.key) {
|
|
489
|
+
case "Home":
|
|
490
|
+
focusItem(items[0]);
|
|
491
|
+
break;
|
|
492
|
+
case "End":
|
|
493
|
+
focusItem(items[items.length - 1]);
|
|
494
|
+
break;
|
|
495
|
+
case "Enter":
|
|
496
|
+
select(id);
|
|
497
|
+
setFocusedId(id);
|
|
498
|
+
break;
|
|
499
|
+
case " ":
|
|
500
|
+
if (synthetic) select(id);
|
|
501
|
+
else if (state !== null) toggle(id);
|
|
502
|
+
else select(id);
|
|
503
|
+
setFocusedId(id);
|
|
504
|
+
break;
|
|
505
|
+
default:
|
|
506
|
+
if (list) {
|
|
507
|
+
switch (e.key) {
|
|
508
|
+
case "ArrowDown":
|
|
509
|
+
focusItem(items[Math.min(idx + 1, items.length - 1)]);
|
|
510
|
+
break;
|
|
511
|
+
case "ArrowUp":
|
|
512
|
+
focusItem(items[Math.max(idx - 1, 0)]);
|
|
513
|
+
break;
|
|
514
|
+
case "ArrowRight":
|
|
515
|
+
if (state === "false") toggle(id);
|
|
516
|
+
else if (state === "true") focusItem(firstChild);
|
|
517
|
+
break;
|
|
518
|
+
case "ArrowLeft":
|
|
519
|
+
if (state === "true") toggle(id);
|
|
520
|
+
else focusItem(parent);
|
|
521
|
+
break;
|
|
522
|
+
default:
|
|
523
|
+
handled = false;
|
|
524
|
+
}
|
|
525
|
+
} else {
|
|
526
|
+
switch (e.key) {
|
|
527
|
+
case "ArrowUp":
|
|
528
|
+
focusItem(stacked ? sibling(-1) ?? parent : parent);
|
|
529
|
+
break;
|
|
530
|
+
case "ArrowDown":
|
|
531
|
+
if (stacked && sibling(1) && state !== "true") focusItem(sibling(1));
|
|
532
|
+
else if (state === "false") toggle(id);
|
|
533
|
+
else if (state === "true") focusItem(firstChild);
|
|
534
|
+
break;
|
|
535
|
+
case "ArrowRight":
|
|
536
|
+
focusItem(sameLevel(1));
|
|
537
|
+
break;
|
|
538
|
+
case "ArrowLeft":
|
|
539
|
+
focusItem(sameLevel(-1));
|
|
540
|
+
break;
|
|
541
|
+
default:
|
|
542
|
+
handled = false;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
if (!handled && e.key.length === 1 && e.key.trim() !== "") {
|
|
546
|
+
const ch = e.key.toLocaleLowerCase();
|
|
547
|
+
const nameOf = (el) => document.getElementById(el.getAttribute("aria-labelledby") ?? "")?.textContent?.trim().toLocaleLowerCase() ?? "";
|
|
548
|
+
const people = items.filter((el) => !syntheticOf(el.dataset.id ?? ""));
|
|
549
|
+
const start = people.indexOf(cur);
|
|
550
|
+
const next = [...people.slice(start + 1), ...people.slice(0, start + 1)].find((el) => nameOf(el).startsWith(ch));
|
|
551
|
+
if (next) {
|
|
552
|
+
focusItem(next);
|
|
553
|
+
handled = true;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
if (handled) e.preventDefault();
|
|
558
|
+
};
|
|
559
|
+
const ctx = {
|
|
560
|
+
expanded,
|
|
561
|
+
loaded,
|
|
562
|
+
failed,
|
|
563
|
+
loading,
|
|
564
|
+
lazy,
|
|
565
|
+
selectable,
|
|
566
|
+
selected,
|
|
567
|
+
tabbableId,
|
|
568
|
+
stackLeaves,
|
|
569
|
+
labels,
|
|
570
|
+
chartId,
|
|
571
|
+
onToggle: toggle,
|
|
572
|
+
onActivate: activate,
|
|
573
|
+
onFocusItem: setFocusedId
|
|
574
|
+
};
|
|
575
|
+
const percent = fillLabel(labels.zoomLevel, { percent: Math.round(zoom * 100) });
|
|
576
|
+
const atMin = zoom <= MIN_ZOOM + 1e-3;
|
|
577
|
+
const atMax = zoom >= MAX_ZOOM - 1e-3;
|
|
578
|
+
return /* @__PURE__ */ jsxs("div", { className: cx("uix-org-chart", className), ...props, children: [
|
|
579
|
+
/* @__PURE__ */ jsxs("div", { className: "uix-org-chart__toolbar", role: "group", "aria-label": labels.toolbar, children: [
|
|
580
|
+
/* @__PURE__ */ jsx(
|
|
581
|
+
Button,
|
|
582
|
+
{
|
|
583
|
+
type: "button",
|
|
584
|
+
variant: "ghost",
|
|
585
|
+
size: "sm",
|
|
586
|
+
icon: true,
|
|
587
|
+
"aria-label": labels.zoomOut,
|
|
588
|
+
title: labels.zoomOut,
|
|
589
|
+
"aria-disabled": atMin || void 0,
|
|
590
|
+
onClick: () => {
|
|
591
|
+
if (!atMin) {
|
|
592
|
+
userMoved.current = true;
|
|
593
|
+
zoomTo(orgChartZoomStep(zoom, -1));
|
|
594
|
+
}
|
|
595
|
+
},
|
|
596
|
+
children: /* @__PURE__ */ jsx(MinusIcon, {})
|
|
597
|
+
}
|
|
598
|
+
),
|
|
599
|
+
/* @__PURE__ */ jsx(
|
|
600
|
+
Button,
|
|
601
|
+
{
|
|
602
|
+
type: "button",
|
|
603
|
+
variant: "ghost",
|
|
604
|
+
size: "sm",
|
|
605
|
+
className: "uix-org-chart__level",
|
|
606
|
+
"aria-label": fillLabel(labels.resetZoom, { percent }),
|
|
607
|
+
onClick: () => {
|
|
608
|
+
userMoved.current = true;
|
|
609
|
+
zoomTo(1);
|
|
610
|
+
},
|
|
611
|
+
children: percent
|
|
612
|
+
}
|
|
613
|
+
),
|
|
614
|
+
/* @__PURE__ */ jsx(
|
|
615
|
+
Button,
|
|
616
|
+
{
|
|
617
|
+
type: "button",
|
|
618
|
+
variant: "ghost",
|
|
619
|
+
size: "sm",
|
|
620
|
+
icon: true,
|
|
621
|
+
"aria-label": labels.zoomIn,
|
|
622
|
+
title: labels.zoomIn,
|
|
623
|
+
"aria-disabled": atMax || void 0,
|
|
624
|
+
onClick: () => {
|
|
625
|
+
if (!atMax) {
|
|
626
|
+
userMoved.current = true;
|
|
627
|
+
zoomTo(orgChartZoomStep(zoom, 1));
|
|
628
|
+
}
|
|
629
|
+
},
|
|
630
|
+
children: /* @__PURE__ */ jsx(PlusIcon, {})
|
|
631
|
+
}
|
|
632
|
+
),
|
|
633
|
+
/* @__PURE__ */ jsxs(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => {
|
|
634
|
+
userMoved.current = true;
|
|
635
|
+
const next = fitZoom(zoom);
|
|
636
|
+
if (next !== null) zoomTo(next);
|
|
637
|
+
}, children: [
|
|
638
|
+
/* @__PURE__ */ jsx(FitIcon, {}),
|
|
639
|
+
labels.fit
|
|
640
|
+
] })
|
|
641
|
+
] }),
|
|
642
|
+
/* @__PURE__ */ jsx(
|
|
643
|
+
"div",
|
|
644
|
+
{
|
|
645
|
+
ref: viewportRef,
|
|
646
|
+
className: "uix-org-chart__viewport",
|
|
647
|
+
"data-panning": panning ? "" : void 0,
|
|
648
|
+
onPointerDown,
|
|
649
|
+
onMouseDown,
|
|
650
|
+
onPointerMove,
|
|
651
|
+
onPointerUp: endPan,
|
|
652
|
+
onPointerCancel: endPan,
|
|
653
|
+
onWheel: () => {
|
|
654
|
+
userMoved.current = true;
|
|
655
|
+
},
|
|
656
|
+
onScroll,
|
|
657
|
+
children: /* @__PURE__ */ jsx("div", { className: "uix-org-chart__canvas", style: { "--org-chart-zoom": zoom }, children: /* @__PURE__ */ jsx(
|
|
658
|
+
"ul",
|
|
659
|
+
{
|
|
660
|
+
ref: treeRef,
|
|
661
|
+
role: "tree",
|
|
662
|
+
"aria-label": ariaLabel ?? (ariaLabelledby ? void 0 : labels.chart),
|
|
663
|
+
"aria-labelledby": ariaLabelledby,
|
|
664
|
+
className: "uix-org-chart__tree",
|
|
665
|
+
onKeyDown,
|
|
666
|
+
children: nodes.map((node, i) => /* @__PURE__ */ jsx(OrgChartItem, { node, level: 1, size: nodes.length, pos: i + 1, ctx }, node.id))
|
|
667
|
+
}
|
|
668
|
+
) })
|
|
669
|
+
}
|
|
670
|
+
),
|
|
671
|
+
/* @__PURE__ */ jsx("span", { className: "uix-visually-hidden", role: "status", children: announcement })
|
|
672
|
+
] });
|
|
673
|
+
}
|
|
674
|
+
function layoutOf(viewport) {
|
|
675
|
+
if (typeof getComputedStyle !== "function") return "chart";
|
|
676
|
+
return getComputedStyle(viewport).getPropertyValue("--org-chart-layout").trim() === "list" ? "list" : "chart";
|
|
677
|
+
}
|
|
678
|
+
export {
|
|
679
|
+
DEFAULT_ORG_CHART_LABELS,
|
|
680
|
+
OrgChart,
|
|
681
|
+
orgChartInitials,
|
|
682
|
+
orgChartZoomStep
|
|
683
|
+
};
|