@tensor_1/react 2.6.0 → 2.7.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/dist/components/Field.js +10 -4
- package/dist/components/Popover.d.ts +18 -4
- package/dist/components/Popover.js +31 -2
- package/dist/components/Toast.js +5 -4
- package/dist/components/Tooltip.d.ts +9 -5
- package/dist/components/Tooltip.js +61 -3
- package/dist/components/Tree.d.ts +15 -1
- package/dist/components/Tree.js +247 -35
- package/dist/hooks/useAnchoredPosition.d.ts +22 -0
- package/dist/hooks/useAnchoredPosition.js +44 -0
- package/dist/hooks/useDialog.js +36 -5
- package/dist/index.cjs +657 -100
- package/dist/index.d.cts +164 -11
- package/dist/index.d.ts +3 -0
- package/dist/index.js +7 -0
- package/dist/overlay-position.d.ts +59 -0
- package/dist/overlay-position.js +62 -0
- package/dist/table-engine.js +72 -6
- package/dist/tree-model.d.ts +48 -0
- package/dist/tree-model.js +49 -0
- package/package.json +1 -1
package/dist/components/Field.js
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cloneElement, isValidElement, useId } from "react";
|
|
2
3
|
import { cx } from "../cx.js";
|
|
3
4
|
function Field({ label, hint, error, success, required, children, className, htmlFor }) {
|
|
5
|
+
const uid = useId();
|
|
6
|
+
const msgId = `${uid}-msg`;
|
|
7
|
+
const describedBy = error || success || hint ? msgId : void 0;
|
|
8
|
+
const control = isValidElement(children) ? cloneElement(children, {
|
|
9
|
+
"aria-describedby": [children.props["aria-describedby"], describedBy].filter(Boolean).join(" ") || void 0,
|
|
10
|
+
"aria-invalid": error ? true : children.props["aria-invalid"]
|
|
11
|
+
}) : children;
|
|
4
12
|
return /* @__PURE__ */ jsxs("div", { className: cx("uix-field", className), children: [
|
|
5
13
|
label && /* @__PURE__ */ jsx("label", { className: "uix-field__label", htmlFor, "data-required": required || void 0, children: label }),
|
|
6
|
-
|
|
7
|
-
error
|
|
8
|
-
success && !error && /* @__PURE__ */ jsx("span", { className: "uix-field__success", children: success }),
|
|
9
|
-
hint && !error && !success && /* @__PURE__ */ jsx("span", { className: "uix-field__hint", children: hint })
|
|
14
|
+
control,
|
|
15
|
+
/* @__PURE__ */ jsx("div", { className: "uix-field__msg", id: msgId, children: error ? /* @__PURE__ */ jsx("span", { className: "uix-field__error", role: "alert", children: error }) : success ? /* @__PURE__ */ jsx("span", { className: "uix-field__success", children: success }) : hint ? /* @__PURE__ */ jsx("span", { className: "uix-field__hint", children: hint }) : null })
|
|
10
16
|
] });
|
|
11
17
|
}
|
|
12
18
|
export {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
|
-
import { HTMLAttributes, ReactNode } from 'react';
|
|
2
|
+
import { HTMLAttributes, RefObject, ReactNode } from 'react';
|
|
3
|
+
import { Placement } from '../overlay-position.js';
|
|
3
4
|
|
|
4
5
|
interface PopoverProps extends HTMLAttributes<HTMLDivElement> {
|
|
5
6
|
/**
|
|
@@ -7,12 +8,25 @@ interface PopoverProps extends HTMLAttributes<HTMLDivElement> {
|
|
|
7
8
|
* wires to it via `popoverTarget={id}`. Pass `"manual"` to control open state yourself.
|
|
8
9
|
*/
|
|
9
10
|
popover?: 'auto' | 'manual';
|
|
11
|
+
/**
|
|
12
|
+
* Element (or ref) to anchor against. When set, the popover is placed with
|
|
13
|
+
* cross-browser JS positioning — flip when it won't fit, shift to stay on-screen —
|
|
14
|
+
* instead of relying on CSS `anchor()`, which is Chromium-only and detaches the
|
|
15
|
+
* popover from its trigger everywhere else. The native Popover API still provides
|
|
16
|
+
* the top layer (so it escapes `overflow` clipping) and light-dismiss.
|
|
17
|
+
*/
|
|
18
|
+
anchor?: RefObject<HTMLElement | null> | HTMLElement | null;
|
|
19
|
+
/** Preferred placement when `anchor` is set. Default `'bottom-start'`. */
|
|
20
|
+
placement?: Placement;
|
|
21
|
+
/** Gap between the trigger and the popover, in px. Default 6. */
|
|
22
|
+
offset?: number;
|
|
10
23
|
children?: ReactNode;
|
|
11
24
|
}
|
|
12
25
|
/**
|
|
13
|
-
* Anchored surface over `.uix-popover`, using the native Popover API.
|
|
14
|
-
*
|
|
26
|
+
* Anchored surface over `.uix-popover`, using the native Popover API. Compose ITSM
|
|
27
|
+
* things like a filter popover, menu, or rich select by putting controls inside.
|
|
28
|
+
* Pass `anchor` to get collision-aware, cross-browser placement (UIX-FIX-02).
|
|
15
29
|
*/
|
|
16
|
-
declare function Popover({ popover,
|
|
30
|
+
declare function Popover({ popover, anchor, placement, offset, className, children, ...props }: PopoverProps): react.JSX.Element;
|
|
17
31
|
|
|
18
32
|
export { Popover, type PopoverProps };
|
|
@@ -1,8 +1,37 @@
|
|
|
1
|
+
"use client";
|
|
1
2
|
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
4
|
import { cx } from "../cx.js";
|
|
3
|
-
|
|
5
|
+
import { useAnchoredPosition } from "../hooks/useAnchoredPosition.js";
|
|
6
|
+
function Popover({
|
|
7
|
+
popover = "auto",
|
|
8
|
+
anchor,
|
|
9
|
+
placement = "bottom-start",
|
|
10
|
+
offset = 6,
|
|
11
|
+
className,
|
|
12
|
+
children,
|
|
13
|
+
...props
|
|
14
|
+
}) {
|
|
15
|
+
const ref = useRef(null);
|
|
16
|
+
const [open, setOpen] = useState(false);
|
|
4
17
|
const popoverAttr = { popover };
|
|
5
|
-
|
|
18
|
+
const reposition = useAnchoredPosition(anchor ?? null, ref, {
|
|
19
|
+
open: open && !!anchor,
|
|
20
|
+
placement,
|
|
21
|
+
offset
|
|
22
|
+
});
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
const el = ref.current;
|
|
25
|
+
if (!el || !anchor) return;
|
|
26
|
+
const sync = () => {
|
|
27
|
+
const isOpen = el.matches(":popover-open");
|
|
28
|
+
if (isOpen) reposition();
|
|
29
|
+
setOpen(isOpen);
|
|
30
|
+
};
|
|
31
|
+
el.addEventListener("toggle", sync);
|
|
32
|
+
return () => el.removeEventListener("toggle", sync);
|
|
33
|
+
}, [anchor, reposition]);
|
|
34
|
+
return /* @__PURE__ */ jsx("div", { ref, className: cx("uix-popover", className), ...popoverAttr, ...props, children });
|
|
6
35
|
}
|
|
7
36
|
export {
|
|
8
37
|
Popover
|
package/dist/components/Toast.js
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { cx } from "../cx.js";
|
|
3
3
|
function Toast({ title, message, tone, icon, onClose, leaving, className, ...props }) {
|
|
4
|
+
const assertive = tone === "danger";
|
|
4
5
|
return /* @__PURE__ */ jsxs(
|
|
5
6
|
"div",
|
|
6
7
|
{
|
|
7
8
|
className: cx("uix-toast", tone && `uix-toast--${tone}`, className),
|
|
8
9
|
"data-leaving": leaving || void 0,
|
|
9
|
-
role: "status",
|
|
10
|
-
"aria-live": "polite",
|
|
10
|
+
role: assertive ? "alert" : "status",
|
|
11
|
+
"aria-live": assertive ? "assertive" : "polite",
|
|
11
12
|
...props,
|
|
12
13
|
children: [
|
|
13
|
-
icon && /* @__PURE__ */ jsx("div", { className: "uix-toast__icon", children: icon }),
|
|
14
|
+
icon && /* @__PURE__ */ jsx("div", { className: "uix-toast__icon", "aria-hidden": "true", children: icon }),
|
|
14
15
|
/* @__PURE__ */ jsxs("div", { className: "uix-toast__body", children: [
|
|
15
16
|
title && /* @__PURE__ */ jsx("div", { className: "uix-toast__title", children: title }),
|
|
16
17
|
message && /* @__PURE__ */ jsx("div", { className: "uix-toast__msg", children: message })
|
|
@@ -21,7 +22,7 @@ function Toast({ title, message, tone, icon, onClose, leaving, className, ...pro
|
|
|
21
22
|
);
|
|
22
23
|
}
|
|
23
24
|
function Toaster({ children, className, ...props }) {
|
|
24
|
-
return /* @__PURE__ */ jsx("div", { className: cx("uix-toaster", className),
|
|
25
|
+
return /* @__PURE__ */ jsx("div", { className: cx("uix-toaster", className), ...props, children });
|
|
25
26
|
}
|
|
26
27
|
export {
|
|
27
28
|
Toast,
|
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
2
|
import { HTMLAttributes, ReactNode } from 'react';
|
|
3
|
+
import { Placement } from '../overlay-position.js';
|
|
3
4
|
|
|
4
5
|
interface TooltipProps extends HTMLAttributes<HTMLSpanElement> {
|
|
5
|
-
/** Tooltip text.
|
|
6
|
+
/** Tooltip text. */
|
|
6
7
|
label: string;
|
|
8
|
+
/** Preferred placement. Default `'top'`. */
|
|
9
|
+
placement?: Placement;
|
|
7
10
|
children?: ReactNode;
|
|
8
11
|
}
|
|
9
12
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
+
* Tooltip whose bubble renders in the top layer via the native Popover API, so it
|
|
14
|
+
* escapes any `overflow: hidden/scroll` ancestor that would clip the old CSS-only
|
|
15
|
+
* `[data-uix-tip]` tooltip (UIX-FIX-02). Positioned by the shared overlay engine
|
|
16
|
+
* (flip/shift), shown on hover and keyboard focus, dismissed on blur or Escape.
|
|
13
17
|
*/
|
|
14
|
-
declare function Tooltip({ label, children, ...props }: TooltipProps): react.JSX.Element;
|
|
18
|
+
declare function Tooltip({ label, placement, className, children, ...props }: TooltipProps): react.JSX.Element;
|
|
15
19
|
|
|
16
20
|
export { Tooltip, type TooltipProps };
|
|
@@ -1,6 +1,64 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useId, useRef, useState } from "react";
|
|
4
|
+
import { cx } from "../cx.js";
|
|
5
|
+
import { useAnchoredPosition } from "../hooks/useAnchoredPosition.js";
|
|
6
|
+
function Tooltip({ label, placement = "top", className, children, ...props }) {
|
|
7
|
+
const triggerRef = useRef(null);
|
|
8
|
+
const bubbleRef = useRef(null);
|
|
9
|
+
const [open, setOpen] = useState(false);
|
|
10
|
+
const id = useId();
|
|
11
|
+
useAnchoredPosition(triggerRef, bubbleRef, { open, placement, offset: 6 });
|
|
12
|
+
const show = () => {
|
|
13
|
+
const b = bubbleRef.current;
|
|
14
|
+
if (b && !b.matches(":popover-open")) {
|
|
15
|
+
try {
|
|
16
|
+
b.showPopover();
|
|
17
|
+
} catch {
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
setOpen(true);
|
|
21
|
+
};
|
|
22
|
+
const hide = () => {
|
|
23
|
+
const b = bubbleRef.current;
|
|
24
|
+
if (b && b.matches(":popover-open")) {
|
|
25
|
+
try {
|
|
26
|
+
b.hidePopover();
|
|
27
|
+
} catch {
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
setOpen(false);
|
|
31
|
+
};
|
|
32
|
+
useEffect(() => () => {
|
|
33
|
+
const b = bubbleRef.current;
|
|
34
|
+
if (b && b.matches(":popover-open")) {
|
|
35
|
+
try {
|
|
36
|
+
b.hidePopover();
|
|
37
|
+
} catch {
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}, []);
|
|
41
|
+
const bubbleAttr = { popover: "manual" };
|
|
42
|
+
return /* @__PURE__ */ jsxs(
|
|
43
|
+
"span",
|
|
44
|
+
{
|
|
45
|
+
ref: triggerRef,
|
|
46
|
+
className: cx("uix-tooltip", className),
|
|
47
|
+
"aria-describedby": id,
|
|
48
|
+
onMouseEnter: show,
|
|
49
|
+
onMouseLeave: hide,
|
|
50
|
+
onFocus: show,
|
|
51
|
+
onBlur: hide,
|
|
52
|
+
onKeyDown: (e) => {
|
|
53
|
+
if (e.key === "Escape") hide();
|
|
54
|
+
},
|
|
55
|
+
...props,
|
|
56
|
+
children: [
|
|
57
|
+
children,
|
|
58
|
+
/* @__PURE__ */ jsx("span", { ref: bubbleRef, id, role: "tooltip", className: "uix-tooltip__bubble", ...bubbleAttr, children: label })
|
|
59
|
+
]
|
|
60
|
+
}
|
|
61
|
+
);
|
|
4
62
|
}
|
|
5
63
|
export {
|
|
6
64
|
Tooltip
|
|
@@ -11,8 +11,11 @@ interface TreeNodeProps {
|
|
|
11
11
|
node: TreeNodeData;
|
|
12
12
|
expanded: Set<string>;
|
|
13
13
|
selected?: string;
|
|
14
|
+
selectable: boolean;
|
|
15
|
+
tabbableId?: string;
|
|
14
16
|
onToggle: (id: string) => void;
|
|
15
17
|
onSelect?: (id: string) => void;
|
|
18
|
+
onFocusNode: (id: string) => void;
|
|
16
19
|
level?: number;
|
|
17
20
|
}
|
|
18
21
|
interface TreeProps extends Omit<HTMLAttributes<HTMLUListElement>, 'onSelect' | 'onChange'> {
|
|
@@ -22,7 +25,18 @@ interface TreeProps extends Omit<HTMLAttributes<HTMLUListElement>, 'onSelect' |
|
|
|
22
25
|
selected?: string;
|
|
23
26
|
onToggle?: (id: string) => void;
|
|
24
27
|
onSelect?: (id: string) => void;
|
|
28
|
+
/**
|
|
29
|
+
* Virtualize a large tree — render only the rows in view (UIX-FIX-05). Defaults to auto
|
|
30
|
+
* (on past the engine's `shouldVirtualize` threshold of visible rows). When on, the tree is
|
|
31
|
+
* flattened to a scrollable list of `maxHeight` with fixed-height rows; a11y is preserved via
|
|
32
|
+
* `aria-level` / `aria-setsize` / `aria-posinset` instead of nested `role="group"`s.
|
|
33
|
+
*/
|
|
34
|
+
virtualize?: boolean;
|
|
35
|
+
/** Fixed row height (px) used for virtualization. Default 32. */
|
|
36
|
+
rowHeight?: number;
|
|
37
|
+
/** Scroll-viewport height (px) when virtualizing. Default 384. */
|
|
38
|
+
maxHeight?: number;
|
|
25
39
|
}
|
|
26
|
-
declare function Tree({ nodes, expanded: controlledExpanded, defaultExpanded, selected, onToggle, onSelect, className, ...props }: TreeProps): react.JSX.Element;
|
|
40
|
+
declare function Tree({ nodes, expanded: controlledExpanded, defaultExpanded, selected, onToggle, onSelect, virtualize, rowHeight, maxHeight, className, ...props }: TreeProps): react.JSX.Element;
|
|
27
41
|
|
|
28
42
|
export { Tree, type TreeNodeData, type TreeNodeProps, type TreeProps };
|
package/dist/components/Tree.js
CHANGED
|
@@ -1,47 +1,76 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useState } from "react";
|
|
3
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
4
4
|
import { cx } from "../cx.js";
|
|
5
|
+
import { flattenTree, treeNav } from "../tree-model.js";
|
|
6
|
+
import { virtualWindow, shouldVirtualize } from "../table-engine.js";
|
|
5
7
|
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" }) });
|
|
6
|
-
function TreeNode({ node, expanded, selected, onToggle, onSelect, level =
|
|
8
|
+
function TreeNode({ node, expanded, selected, selectable, tabbableId, onToggle, onSelect, onFocusNode, level = 1 }) {
|
|
7
9
|
const hasChildren = Array.isArray(node.children) && node.children.length > 0;
|
|
8
10
|
const isExpanded = expanded.has(node.id);
|
|
9
|
-
return /* @__PURE__ */ jsxs(
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
11
|
+
return /* @__PURE__ */ jsxs(
|
|
12
|
+
"li",
|
|
13
|
+
{
|
|
14
|
+
role: "treeitem",
|
|
15
|
+
"aria-level": level,
|
|
16
|
+
"aria-expanded": hasChildren ? isExpanded : void 0,
|
|
17
|
+
"aria-selected": selectable ? selected === node.id : void 0,
|
|
18
|
+
"data-id": node.id,
|
|
19
|
+
tabIndex: tabbableId === node.id ? 0 : -1,
|
|
20
|
+
className: "uix-tree__item",
|
|
21
|
+
onFocus: (e) => {
|
|
22
|
+
if (e.target === e.currentTarget) onFocusNode(node.id);
|
|
23
|
+
},
|
|
24
|
+
onClick: (e) => {
|
|
25
|
+
e.stopPropagation();
|
|
26
|
+
if (hasChildren) onToggle(node.id);
|
|
27
|
+
onSelect?.(node.id);
|
|
28
|
+
onFocusNode(node.id);
|
|
29
|
+
},
|
|
30
|
+
children: [
|
|
31
|
+
/* @__PURE__ */ jsxs("span", { className: "uix-tree__row", children: [
|
|
21
32
|
/* @__PURE__ */ jsx("span", { className: "uix-tree__toggle", children: hasChildren && /* @__PURE__ */ jsx(ChevronIcon, {}) }),
|
|
22
33
|
node.icon && /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: node.icon }),
|
|
23
|
-
node.label
|
|
24
|
-
]
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
34
|
+
/* @__PURE__ */ jsx("span", { className: "uix-tree__label", children: node.label })
|
|
35
|
+
] }),
|
|
36
|
+
hasChildren && isExpanded && /* @__PURE__ */ jsx("ul", { role: "group", className: "uix-tree__group", children: node.children.map((child) => /* @__PURE__ */ jsx(
|
|
37
|
+
TreeNode,
|
|
38
|
+
{
|
|
39
|
+
node: child,
|
|
40
|
+
expanded,
|
|
41
|
+
selected,
|
|
42
|
+
selectable,
|
|
43
|
+
tabbableId,
|
|
44
|
+
onToggle,
|
|
45
|
+
onSelect,
|
|
46
|
+
onFocusNode,
|
|
47
|
+
level: level + 1
|
|
48
|
+
},
|
|
49
|
+
child.id
|
|
50
|
+
)) })
|
|
51
|
+
]
|
|
52
|
+
}
|
|
53
|
+
);
|
|
40
54
|
}
|
|
41
|
-
function Tree({
|
|
55
|
+
function Tree({
|
|
56
|
+
nodes,
|
|
57
|
+
expanded: controlledExpanded,
|
|
58
|
+
defaultExpanded,
|
|
59
|
+
selected,
|
|
60
|
+
onToggle,
|
|
61
|
+
onSelect,
|
|
62
|
+
virtualize,
|
|
63
|
+
rowHeight = 32,
|
|
64
|
+
maxHeight = 384,
|
|
65
|
+
className,
|
|
66
|
+
...props
|
|
67
|
+
}) {
|
|
42
68
|
const [internalExpanded, setInternalExpanded] = useState(defaultExpanded ?? /* @__PURE__ */ new Set());
|
|
43
69
|
const expanded = controlledExpanded ?? internalExpanded;
|
|
44
|
-
|
|
70
|
+
const rootRef = useRef(null);
|
|
71
|
+
const [focusedId, setFocusedId] = useState(void 0);
|
|
72
|
+
const selectable = selected !== void 0 || onSelect != null;
|
|
73
|
+
const handleToggle = useCallback((id) => {
|
|
45
74
|
if (!controlledExpanded) {
|
|
46
75
|
setInternalExpanded((prev) => {
|
|
47
76
|
const next = new Set(prev);
|
|
@@ -51,19 +80,202 @@ function Tree({ nodes, expanded: controlledExpanded, defaultExpanded, selected,
|
|
|
51
80
|
});
|
|
52
81
|
}
|
|
53
82
|
onToggle?.(id);
|
|
83
|
+
}, [controlledExpanded, onToggle]);
|
|
84
|
+
const flat = useMemo(() => flattenTree(nodes, expanded), [nodes, expanded]);
|
|
85
|
+
const useVirtual = virtualize ?? shouldVirtualize(flat.length);
|
|
86
|
+
const tabbableId = focusedId ?? selected ?? nodes[0]?.id;
|
|
87
|
+
if (useVirtual) {
|
|
88
|
+
return /* @__PURE__ */ jsx(
|
|
89
|
+
VirtualTreeView,
|
|
90
|
+
{
|
|
91
|
+
flat,
|
|
92
|
+
rowHeight,
|
|
93
|
+
maxHeight,
|
|
94
|
+
selectable,
|
|
95
|
+
selected,
|
|
96
|
+
tabbableId,
|
|
97
|
+
onToggle: handleToggle,
|
|
98
|
+
onSelect,
|
|
99
|
+
focusedId,
|
|
100
|
+
setFocusedId,
|
|
101
|
+
className,
|
|
102
|
+
...props
|
|
103
|
+
}
|
|
104
|
+
);
|
|
54
105
|
}
|
|
55
|
-
|
|
106
|
+
const treeItems = () => Array.from(rootRef.current?.querySelectorAll('[role="treeitem"]') ?? []);
|
|
107
|
+
const focusItem = (el) => {
|
|
108
|
+
if (el) {
|
|
109
|
+
el.focus();
|
|
110
|
+
setFocusedId(el.dataset.id);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
const onKeyDown = (e) => {
|
|
114
|
+
const cur = e.target.closest('[role="treeitem"]');
|
|
115
|
+
if (!cur || !rootRef.current?.contains(cur)) return;
|
|
116
|
+
const list = treeItems();
|
|
117
|
+
const idx = list.indexOf(cur);
|
|
118
|
+
const id = cur.dataset.id;
|
|
119
|
+
const exp = cur.getAttribute("aria-expanded");
|
|
120
|
+
switch (e.key) {
|
|
121
|
+
case "ArrowDown":
|
|
122
|
+
e.preventDefault();
|
|
123
|
+
focusItem(list[Math.min(idx + 1, list.length - 1)]);
|
|
124
|
+
break;
|
|
125
|
+
case "ArrowUp":
|
|
126
|
+
e.preventDefault();
|
|
127
|
+
focusItem(list[Math.max(idx - 1, 0)]);
|
|
128
|
+
break;
|
|
129
|
+
case "Home":
|
|
130
|
+
e.preventDefault();
|
|
131
|
+
focusItem(list[0]);
|
|
132
|
+
break;
|
|
133
|
+
case "End":
|
|
134
|
+
e.preventDefault();
|
|
135
|
+
focusItem(list[list.length - 1]);
|
|
136
|
+
break;
|
|
137
|
+
case "ArrowRight":
|
|
138
|
+
e.preventDefault();
|
|
139
|
+
if (exp === "false") handleToggle(id);
|
|
140
|
+
else if (exp === "true") focusItem(cur.querySelector(':scope > [role="group"] > [role="treeitem"]'));
|
|
141
|
+
break;
|
|
142
|
+
case "ArrowLeft":
|
|
143
|
+
e.preventDefault();
|
|
144
|
+
if (exp === "true") handleToggle(id);
|
|
145
|
+
else focusItem(cur.parentElement?.closest('[role="treeitem"]'));
|
|
146
|
+
break;
|
|
147
|
+
case "Enter":
|
|
148
|
+
case " ":
|
|
149
|
+
e.preventDefault();
|
|
150
|
+
if (exp !== null) handleToggle(id);
|
|
151
|
+
onSelect?.(id);
|
|
152
|
+
setFocusedId(id);
|
|
153
|
+
break;
|
|
154
|
+
default:
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
return /* @__PURE__ */ jsx("ul", { ref: rootRef, className: cx("uix-tree", className), role: "tree", onKeyDown, ...props, children: nodes.map((node) => /* @__PURE__ */ jsx(
|
|
56
159
|
TreeNode,
|
|
57
160
|
{
|
|
58
161
|
node,
|
|
59
162
|
expanded,
|
|
60
163
|
selected,
|
|
164
|
+
selectable,
|
|
165
|
+
tabbableId,
|
|
61
166
|
onToggle: handleToggle,
|
|
62
|
-
onSelect
|
|
167
|
+
onSelect,
|
|
168
|
+
onFocusNode: setFocusedId,
|
|
169
|
+
level: 1
|
|
63
170
|
},
|
|
64
171
|
node.id
|
|
65
172
|
)) });
|
|
66
173
|
}
|
|
174
|
+
function VirtualTreeView({
|
|
175
|
+
flat,
|
|
176
|
+
rowHeight,
|
|
177
|
+
maxHeight,
|
|
178
|
+
selectable,
|
|
179
|
+
selected,
|
|
180
|
+
tabbableId,
|
|
181
|
+
onToggle,
|
|
182
|
+
onSelect,
|
|
183
|
+
focusedId,
|
|
184
|
+
setFocusedId,
|
|
185
|
+
className,
|
|
186
|
+
...props
|
|
187
|
+
}) {
|
|
188
|
+
const scrollRef = useRef(null);
|
|
189
|
+
const [scrollTop, setScrollTop] = useState(0);
|
|
190
|
+
const pendingFocus = useRef(false);
|
|
191
|
+
const win = virtualWindow(scrollTop, maxHeight, rowHeight, flat.length);
|
|
192
|
+
const rows = flat.slice(win.start, win.end);
|
|
193
|
+
useEffect(() => {
|
|
194
|
+
if (!pendingFocus.current || focusedId == null) return;
|
|
195
|
+
const el = scrollRef.current?.querySelector(`[role="treeitem"][data-id="${CSS.escape(focusedId)}"]`);
|
|
196
|
+
if (el) {
|
|
197
|
+
el.focus();
|
|
198
|
+
pendingFocus.current = false;
|
|
199
|
+
}
|
|
200
|
+
}, [focusedId, win.start, win.end]);
|
|
201
|
+
const moveTo = (id) => {
|
|
202
|
+
const i = flat.findIndex((f) => f.node.id === id);
|
|
203
|
+
if (i < 0) return;
|
|
204
|
+
const top = i * rowHeight;
|
|
205
|
+
let next = scrollTop;
|
|
206
|
+
if (top < scrollTop) next = top;
|
|
207
|
+
else if (top + rowHeight > scrollTop + maxHeight) next = top + rowHeight - maxHeight;
|
|
208
|
+
if (next !== scrollTop) {
|
|
209
|
+
if (scrollRef.current) scrollRef.current.scrollTop = next;
|
|
210
|
+
setScrollTop(next);
|
|
211
|
+
}
|
|
212
|
+
pendingFocus.current = true;
|
|
213
|
+
setFocusedId(id);
|
|
214
|
+
};
|
|
215
|
+
const onKeyDown = (e) => {
|
|
216
|
+
const curId = e.target.closest('[role="treeitem"]')?.dataset.id ?? focusedId;
|
|
217
|
+
const act = treeNav(flat, curId, e.key);
|
|
218
|
+
if (act.focusId === void 0 && act.toggleId === void 0 && act.selectId === void 0) return;
|
|
219
|
+
e.preventDefault();
|
|
220
|
+
if (act.toggleId !== void 0) onToggle(act.toggleId);
|
|
221
|
+
if (act.selectId !== void 0) {
|
|
222
|
+
onSelect?.(act.selectId);
|
|
223
|
+
if (act.focusId === void 0) {
|
|
224
|
+
pendingFocus.current = true;
|
|
225
|
+
setFocusedId(act.selectId);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
if (act.focusId !== void 0) moveTo(act.focusId);
|
|
229
|
+
};
|
|
230
|
+
return /* @__PURE__ */ jsxs(
|
|
231
|
+
"ul",
|
|
232
|
+
{
|
|
233
|
+
ref: scrollRef,
|
|
234
|
+
className: cx("uix-tree", "uix-tree--virtual", className),
|
|
235
|
+
role: "tree",
|
|
236
|
+
style: { maxHeight, overflow: "auto" },
|
|
237
|
+
onScroll: (e) => setScrollTop(e.currentTarget.scrollTop),
|
|
238
|
+
onKeyDown,
|
|
239
|
+
...props,
|
|
240
|
+
children: [
|
|
241
|
+
win.padTop > 0 && /* @__PURE__ */ jsx("li", { "aria-hidden": "true", style: { height: win.padTop } }),
|
|
242
|
+
rows.map((f) => {
|
|
243
|
+
const rowStyle = { height: rowHeight, paddingLeft: `calc(${f.level - 1} * var(--uix-space-5))` };
|
|
244
|
+
return /* @__PURE__ */ jsx(
|
|
245
|
+
"li",
|
|
246
|
+
{
|
|
247
|
+
role: "treeitem",
|
|
248
|
+
"aria-level": f.level,
|
|
249
|
+
"aria-setsize": f.setsize,
|
|
250
|
+
"aria-posinset": f.posinset,
|
|
251
|
+
"aria-expanded": f.hasChildren ? f.isExpanded : void 0,
|
|
252
|
+
"aria-selected": selectable ? selected === f.node.id : void 0,
|
|
253
|
+
"data-id": f.node.id,
|
|
254
|
+
tabIndex: tabbableId === f.node.id ? 0 : -1,
|
|
255
|
+
className: "uix-tree__item",
|
|
256
|
+
onFocus: (e) => {
|
|
257
|
+
if (e.target === e.currentTarget) setFocusedId(f.node.id);
|
|
258
|
+
},
|
|
259
|
+
onClick: (e) => {
|
|
260
|
+
e.stopPropagation();
|
|
261
|
+
if (f.hasChildren) onToggle(f.node.id);
|
|
262
|
+
onSelect?.(f.node.id);
|
|
263
|
+
setFocusedId(f.node.id);
|
|
264
|
+
},
|
|
265
|
+
children: /* @__PURE__ */ jsxs("span", { className: "uix-tree__row", style: rowStyle, children: [
|
|
266
|
+
/* @__PURE__ */ jsx("span", { className: "uix-tree__toggle", children: f.hasChildren && /* @__PURE__ */ jsx(ChevronIcon, {}) }),
|
|
267
|
+
f.node.icon && /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: f.node.icon }),
|
|
268
|
+
/* @__PURE__ */ jsx("span", { className: "uix-tree__label", children: f.node.label })
|
|
269
|
+
] })
|
|
270
|
+
},
|
|
271
|
+
f.node.id
|
|
272
|
+
);
|
|
273
|
+
}),
|
|
274
|
+
win.padBottom > 0 && /* @__PURE__ */ jsx("li", { "aria-hidden": "true", style: { height: win.padBottom } })
|
|
275
|
+
]
|
|
276
|
+
}
|
|
277
|
+
);
|
|
278
|
+
}
|
|
67
279
|
export {
|
|
68
280
|
Tree
|
|
69
281
|
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { RefObject } from 'react';
|
|
2
|
+
import { Placement } from '../overlay-position.js';
|
|
3
|
+
|
|
4
|
+
interface UseAnchoredPositionOptions {
|
|
5
|
+
/** Reposition while true, and (re)attach scroll/resize listeners. */
|
|
6
|
+
open: boolean;
|
|
7
|
+
placement?: Placement;
|
|
8
|
+
offset?: number;
|
|
9
|
+
padding?: number;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Position a floating element (a native-popover overlay) against an anchor with
|
|
13
|
+
* cross-browser flip/shift — the DOM half of the overlay-position engine (UIX-FIX-02).
|
|
14
|
+
*
|
|
15
|
+
* Measures both elements with getBoundingClientRect, applies `position: fixed` + left/top
|
|
16
|
+
* (overriding the popover's default centered `inset:0; margin:auto`), and keeps the overlay
|
|
17
|
+
* glued to its anchor on scroll/resize while `open`. Returns a `reposition` fn for callers
|
|
18
|
+
* that want to nudge it manually (e.g. right when a native popover's `toggle` fires).
|
|
19
|
+
*/
|
|
20
|
+
declare function useAnchoredPosition(anchor: RefObject<HTMLElement | null> | HTMLElement | null | undefined, floatingRef: RefObject<HTMLElement | null>, { open, placement, offset, padding }: UseAnchoredPositionOptions): () => void;
|
|
21
|
+
|
|
22
|
+
export { type UseAnchoredPositionOptions, useAnchoredPosition };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useCallback, useEffect, useLayoutEffect, useRef } from "react";
|
|
3
|
+
import { computePosition } from "../overlay-position.js";
|
|
4
|
+
const useIsomorphicLayoutEffect = typeof document !== "undefined" ? useLayoutEffect : useEffect;
|
|
5
|
+
const resolve = (a) => a && typeof a === "object" && "current" in a ? a.current : a ?? null;
|
|
6
|
+
function useAnchoredPosition(anchor, floatingRef, { open, placement = "bottom-start", offset = 6, padding = 8 }) {
|
|
7
|
+
const opts = useRef({ placement, offset, padding });
|
|
8
|
+
opts.current = { placement, offset, padding };
|
|
9
|
+
const reposition = useCallback(() => {
|
|
10
|
+
const anchorEl = resolve(anchor);
|
|
11
|
+
const floating = floatingRef.current;
|
|
12
|
+
if (!anchorEl || !floating) return;
|
|
13
|
+
const s = floating.style;
|
|
14
|
+
s.position = "fixed";
|
|
15
|
+
s.inset = "auto";
|
|
16
|
+
s.margin = "0";
|
|
17
|
+
const a = anchorEl.getBoundingClientRect();
|
|
18
|
+
const { placement: p, offset: o, padding: pad } = opts.current;
|
|
19
|
+
const pos = computePosition(
|
|
20
|
+
{ x: a.x, y: a.y, width: a.width, height: a.height },
|
|
21
|
+
{ width: floating.offsetWidth, height: floating.offsetHeight },
|
|
22
|
+
{ width: window.innerWidth, height: window.innerHeight },
|
|
23
|
+
{ placement: p, offset: o, padding: pad }
|
|
24
|
+
);
|
|
25
|
+
s.left = `${Math.round(pos.x)}px`;
|
|
26
|
+
s.top = `${Math.round(pos.y)}px`;
|
|
27
|
+
floating.dataset.placement = pos.placement;
|
|
28
|
+
}, [anchor, floatingRef]);
|
|
29
|
+
useIsomorphicLayoutEffect(() => {
|
|
30
|
+
if (!open) return;
|
|
31
|
+
reposition();
|
|
32
|
+
const onScroll = () => reposition();
|
|
33
|
+
window.addEventListener("scroll", onScroll, { passive: true, capture: true });
|
|
34
|
+
window.addEventListener("resize", onScroll);
|
|
35
|
+
return () => {
|
|
36
|
+
window.removeEventListener("scroll", onScroll, true);
|
|
37
|
+
window.removeEventListener("resize", onScroll);
|
|
38
|
+
};
|
|
39
|
+
}, [open, reposition]);
|
|
40
|
+
return reposition;
|
|
41
|
+
}
|
|
42
|
+
export {
|
|
43
|
+
useAnchoredPosition
|
|
44
|
+
};
|