@tensor_1/react 2.15.0 → 2.17.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 +5 -0
- package/dist/ChartCore-Do6gRBN3.d.cts +44 -0
- package/dist/chart-preset.cjs +56 -12
- package/dist/chart-preset.d.cts +2 -2
- package/dist/chart-preset.d.ts +1 -1
- package/dist/chart-preset.js +4 -1
- package/dist/chart.cjs +56 -12
- package/dist/chart.d.cts +2 -2
- package/dist/chart.d.ts +1 -1
- package/dist/chart.js +4 -1
- package/dist/components/Alert.js +15 -7
- package/dist/components/AppShell.d.ts +3 -1
- package/dist/components/AppShell.js +3 -1
- package/dist/components/Avatar.js +2 -1
- package/dist/components/Button.js +32 -16
- package/dist/components/Card.d.ts +3 -1
- package/dist/components/Card.js +2 -2
- package/dist/components/Chart.d.ts +1 -1
- package/dist/components/Chart.js +4 -1
- package/dist/components/ChartCore.d.ts +24 -3
- package/dist/components/ChartCore.js +53 -12
- package/dist/components/CommandPalette.d.ts +4 -2
- package/dist/components/CommandPalette.js +66 -13
- package/dist/components/Comments.d.ts +5 -1
- package/dist/components/Comments.js +1 -1
- package/dist/components/Composer.d.ts +8 -1
- package/dist/components/Composer.js +7 -2
- package/dist/components/DetailLayout.js +1 -1
- package/dist/components/Drawer.d.ts +3 -4
- package/dist/components/Drawer.js +6 -4
- package/dist/components/Field.js +6 -3
- package/dist/components/Flow.d.ts +26 -0
- package/dist/components/Flow.js +48 -0
- package/dist/components/Inbox.d.ts +13 -3
- package/dist/components/Inbox.js +62 -3
- package/dist/components/Input.d.ts +13 -2
- package/dist/components/Input.js +20 -5
- package/dist/components/Kanban.d.ts +15 -3
- package/dist/components/Kanban.js +42 -10
- package/dist/components/Meter.d.ts +3 -1
- package/dist/components/Meter.js +9 -1
- package/dist/components/Modal.d.ts +4 -5
- package/dist/components/Modal.js +4 -4
- package/dist/components/NavFavourites.js +11 -0
- package/dist/components/PageHeader.d.ts +3 -1
- package/dist/components/PageHeader.js +2 -2
- package/dist/components/Peek.d.ts +3 -4
- package/dist/components/Peek.js +9 -6
- package/dist/components/Pipeline.d.ts +25 -0
- package/dist/components/Pipeline.js +65 -0
- package/dist/components/Progress.d.ts +3 -1
- package/dist/components/Progress.js +5 -3
- package/dist/components/Radio.d.ts +3 -1
- package/dist/components/Radio.js +6 -2
- package/dist/components/Spinner.d.ts +3 -1
- package/dist/components/Spinner.js +3 -3
- package/dist/components/Stat.js +5 -1
- package/dist/components/States.d.ts +1 -0
- package/dist/components/States.js +7 -1
- package/dist/components/Table.d.ts +8 -1
- package/dist/components/Table.js +10 -6
- package/dist/components/Tabs.d.ts +13 -1
- package/dist/components/Tabs.js +79 -4
- package/dist/components/Timeline.d.ts +3 -3
- package/dist/components/Timeline.js +3 -3
- package/dist/components/Toast.js +42 -5
- package/dist/components/Tooltip.d.ts +3 -1
- package/dist/components/Tooltip.js +44 -5
- package/dist/components/Tree.d.ts +2 -0
- package/dist/components/Tree.js +26 -4
- package/dist/hooks/useDialog.d.ts +1 -1
- package/dist/hooks/useDialog.js +17 -2
- package/dist/hooks/useTable.d.ts +5 -0
- package/dist/hooks/useTable.js +20 -1
- package/dist/index.cjs +1191 -626
- package/dist/index.d.cts +168 -35
- package/dist/index.d.ts +4 -2
- package/dist/index.js +8 -1
- package/dist/tree-model.d.ts +4 -0
- package/dist/tree-model.js +8 -0
- package/package.json +1 -1
- package/dist/ChartCore-Bt1C84ty.d.cts +0 -22
|
@@ -15,5 +15,17 @@ interface TabProps extends Omit<HTMLAttributes<HTMLButtonElement>, 'onChange'> {
|
|
|
15
15
|
disabled?: boolean;
|
|
16
16
|
}
|
|
17
17
|
declare function Tab({ value, children, disabled, className, onClick, ...props }: TabProps): react.JSX.Element;
|
|
18
|
+
interface TabPanelProps extends HTMLAttributes<HTMLDivElement> {
|
|
19
|
+
/** The owning `<Tab>`'s value — the panel renders only while that tab is selected. */
|
|
20
|
+
value: string;
|
|
21
|
+
children?: ReactNode;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The tabpanel paired with a `<Tab>` by `value` (UIX-A11Y-2): labelled by its tab,
|
|
25
|
+
* `tabIndex 0` so Tab from the tablist lands in the content, rendered only while
|
|
26
|
+
* selected. Place it as a direct child of `<Tabs>` — it is hoisted out of the tablist —
|
|
27
|
+
* or anywhere the Tabs context reaches.
|
|
28
|
+
*/
|
|
29
|
+
declare function TabPanel({ value, children, ...props }: TabPanelProps): react.JSX.Element | null;
|
|
18
30
|
|
|
19
|
-
export { Tab, type TabProps, Tabs, type TabsProps };
|
|
31
|
+
export { Tab, TabPanel, type TabPanelProps, type TabProps, Tabs, type TabsProps };
|
package/dist/components/Tabs.js
CHANGED
|
@@ -1,11 +1,67 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx } from "react/jsx-runtime";
|
|
3
|
-
import { createContext, useContext, useMemo } from "react";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Children, createContext, isValidElement, useContext, useEffect, useId, useMemo, useRef } from "react";
|
|
4
4
|
import { cx } from "../cx.js";
|
|
5
5
|
const TabsCtx = createContext({});
|
|
6
6
|
function Tabs({ variant = "line", value, onChange, children, className }) {
|
|
7
|
-
const
|
|
8
|
-
|
|
7
|
+
const baseId = useId();
|
|
8
|
+
const listRef = useRef(null);
|
|
9
|
+
const contextValue = useMemo(() => ({ value, onChange, baseId }), [value, onChange, baseId]);
|
|
10
|
+
const kids = Children.toArray(children);
|
|
11
|
+
const panels = kids.filter((k) => isValidElement(k) && k.type === TabPanel);
|
|
12
|
+
const tabs = kids.filter((k) => !(isValidElement(k) && k.type === TabPanel));
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
const list = listRef.current;
|
|
15
|
+
if (!list) return;
|
|
16
|
+
const all = Array.from(list.querySelectorAll('[role="tab"]'));
|
|
17
|
+
const enabled = all.filter((t) => !t.matches(":disabled"));
|
|
18
|
+
const active = enabled.find((t) => t.getAttribute("aria-selected") === "true") ?? enabled[0];
|
|
19
|
+
for (const t of all) t.tabIndex = t === active ? 0 : -1;
|
|
20
|
+
});
|
|
21
|
+
const onKeyDown = (e) => {
|
|
22
|
+
const items = Array.from(
|
|
23
|
+
listRef.current?.querySelectorAll('[role="tab"]:not(:disabled)') ?? []
|
|
24
|
+
);
|
|
25
|
+
const cur = e.target.closest('[role="tab"]');
|
|
26
|
+
const idx = cur ? items.indexOf(cur) : -1;
|
|
27
|
+
if (idx < 0 || items.length === 0) return;
|
|
28
|
+
let next;
|
|
29
|
+
switch (e.key) {
|
|
30
|
+
case "ArrowRight":
|
|
31
|
+
next = (idx + 1) % items.length;
|
|
32
|
+
break;
|
|
33
|
+
case "ArrowLeft":
|
|
34
|
+
next = (idx - 1 + items.length) % items.length;
|
|
35
|
+
break;
|
|
36
|
+
case "Home":
|
|
37
|
+
next = 0;
|
|
38
|
+
break;
|
|
39
|
+
case "End":
|
|
40
|
+
next = items.length - 1;
|
|
41
|
+
break;
|
|
42
|
+
default:
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
e.preventDefault();
|
|
46
|
+
const target = items[next];
|
|
47
|
+
if (target) {
|
|
48
|
+
target.focus();
|
|
49
|
+
target.click();
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
return /* @__PURE__ */ jsxs(TabsCtx.Provider, { value: contextValue, children: [
|
|
53
|
+
/* @__PURE__ */ jsx(
|
|
54
|
+
"div",
|
|
55
|
+
{
|
|
56
|
+
ref: listRef,
|
|
57
|
+
role: "tablist",
|
|
58
|
+
className: cx("uix-tabs", `uix-tabs--${variant}`, className),
|
|
59
|
+
onKeyDown,
|
|
60
|
+
children: tabs
|
|
61
|
+
}
|
|
62
|
+
),
|
|
63
|
+
panels
|
|
64
|
+
] });
|
|
9
65
|
}
|
|
10
66
|
function Tab({ value, children, disabled, className, onClick, ...props }) {
|
|
11
67
|
const ctx = useContext(TabsCtx);
|
|
@@ -14,8 +70,11 @@ function Tab({ value, children, disabled, className, onClick, ...props }) {
|
|
|
14
70
|
"button",
|
|
15
71
|
{
|
|
16
72
|
role: "tab",
|
|
73
|
+
id: ctx.baseId ? `${ctx.baseId}-tab-${value}` : void 0,
|
|
74
|
+
"aria-controls": ctx.baseId ? `${ctx.baseId}-panel-${value}` : void 0,
|
|
17
75
|
className: cx("uix-tab", className),
|
|
18
76
|
"aria-selected": selected,
|
|
77
|
+
tabIndex: selected ? 0 : -1,
|
|
19
78
|
disabled,
|
|
20
79
|
onClick: (e) => {
|
|
21
80
|
ctx.onChange?.(value);
|
|
@@ -26,7 +85,23 @@ function Tab({ value, children, disabled, className, onClick, ...props }) {
|
|
|
26
85
|
}
|
|
27
86
|
);
|
|
28
87
|
}
|
|
88
|
+
function TabPanel({ value, children, ...props }) {
|
|
89
|
+
const ctx = useContext(TabsCtx);
|
|
90
|
+
if (ctx.value !== value) return null;
|
|
91
|
+
return /* @__PURE__ */ jsx(
|
|
92
|
+
"div",
|
|
93
|
+
{
|
|
94
|
+
role: "tabpanel",
|
|
95
|
+
id: ctx.baseId ? `${ctx.baseId}-panel-${value}` : void 0,
|
|
96
|
+
"aria-labelledby": ctx.baseId ? `${ctx.baseId}-tab-${value}` : void 0,
|
|
97
|
+
tabIndex: 0,
|
|
98
|
+
...props,
|
|
99
|
+
children
|
|
100
|
+
}
|
|
101
|
+
);
|
|
102
|
+
}
|
|
29
103
|
export {
|
|
30
104
|
Tab,
|
|
105
|
+
TabPanel,
|
|
31
106
|
Tabs
|
|
32
107
|
};
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
2
|
import { HTMLAttributes, ReactNode } from 'react';
|
|
3
3
|
|
|
4
|
-
interface TimelineProps extends HTMLAttributes<
|
|
4
|
+
interface TimelineProps extends HTMLAttributes<HTMLOListElement> {
|
|
5
5
|
children?: ReactNode;
|
|
6
6
|
}
|
|
7
|
-
/** Activity timeline container over `.uix-timeline`. */
|
|
7
|
+
/** Activity timeline container over `.uix-timeline`. Ordered list so AT reads entry count + sequence (UIX-A11Y-4). */
|
|
8
8
|
declare function Timeline({ children, className, ...props }: TimelineProps): react.JSX.Element;
|
|
9
|
-
interface TimelineItemProps extends HTMLAttributes<
|
|
9
|
+
interface TimelineItemProps extends HTMLAttributes<HTMLLIElement> {
|
|
10
10
|
/**
|
|
11
11
|
* Custom rail node — an icon puck, an avatar, a tinted dot. Defaults to the
|
|
12
12
|
* accent `.uix-timeline__node` dot. Supply this to make a heterogeneous feed
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { cx } from "../cx.js";
|
|
3
3
|
function Timeline({ children, className, ...props }) {
|
|
4
|
-
return /* @__PURE__ */ jsx("
|
|
4
|
+
return /* @__PURE__ */ jsx("ol", { className: cx("uix-timeline", className), ...props, children });
|
|
5
5
|
}
|
|
6
6
|
function TimelineItem({ node, meta, children, className, ...props }) {
|
|
7
|
-
return /* @__PURE__ */ jsxs("
|
|
8
|
-
/* @__PURE__ */ jsxs("div", { className: "uix-timeline__rail", children: [
|
|
7
|
+
return /* @__PURE__ */ jsxs("li", { className: cx("uix-timeline__item", className), ...props, children: [
|
|
8
|
+
/* @__PURE__ */ jsxs("div", { className: "uix-timeline__rail", "aria-hidden": "true", children: [
|
|
9
9
|
node ?? /* @__PURE__ */ jsx("span", { className: "uix-timeline__node" }),
|
|
10
10
|
/* @__PURE__ */ jsx("span", { className: "uix-timeline__line" })
|
|
11
11
|
] }),
|
package/dist/components/Toast.js
CHANGED
|
@@ -1,28 +1,65 @@
|
|
|
1
|
+
"use client";
|
|
1
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
|
2
4
|
import { cx } from "../cx.js";
|
|
5
|
+
const ToasterContext = createContext(null);
|
|
3
6
|
function Toast({ title, message, tone, icon, onClose, leaving, className, ...props }) {
|
|
4
7
|
const assertive = tone === "danger";
|
|
8
|
+
const toaster = useContext(ToasterContext);
|
|
9
|
+
const bodyRef = useRef(null);
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
if (assertive || !toaster) return;
|
|
12
|
+
const text = bodyRef.current?.textContent?.trim();
|
|
13
|
+
if (text) toaster.announce(text);
|
|
14
|
+
}, []);
|
|
15
|
+
const handleClose = (e) => {
|
|
16
|
+
const btn = e.currentTarget;
|
|
17
|
+
if (document.activeElement === btn) {
|
|
18
|
+
const toasterEl = btn.closest(".uix-toaster");
|
|
19
|
+
if (toasterEl) {
|
|
20
|
+
const closes = Array.from(toasterEl.querySelectorAll(".uix-toast__close"));
|
|
21
|
+
const i = closes.indexOf(btn);
|
|
22
|
+
(closes[i + 1] ?? closes[i - 1])?.focus();
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
onClose?.();
|
|
26
|
+
};
|
|
27
|
+
const ownLiveRegion = assertive || !toaster;
|
|
5
28
|
return /* @__PURE__ */ jsxs(
|
|
6
29
|
"div",
|
|
7
30
|
{
|
|
8
31
|
className: cx("uix-toast", tone && `uix-toast--${tone}`, className),
|
|
9
32
|
"data-leaving": leaving || void 0,
|
|
10
|
-
role: assertive ? "alert" : "status",
|
|
11
|
-
"aria-live": assertive ? "assertive" : "polite",
|
|
33
|
+
role: ownLiveRegion ? assertive ? "alert" : "status" : void 0,
|
|
34
|
+
"aria-live": ownLiveRegion ? assertive ? "assertive" : "polite" : void 0,
|
|
12
35
|
...props,
|
|
13
36
|
children: [
|
|
14
37
|
icon && /* @__PURE__ */ jsx("div", { className: "uix-toast__icon", "aria-hidden": "true", children: icon }),
|
|
15
|
-
/* @__PURE__ */ jsxs("div", { className: "uix-toast__body", children: [
|
|
38
|
+
/* @__PURE__ */ jsxs("div", { className: "uix-toast__body", ref: bodyRef, children: [
|
|
16
39
|
title && /* @__PURE__ */ jsx("div", { className: "uix-toast__title", children: title }),
|
|
17
40
|
message && /* @__PURE__ */ jsx("div", { className: "uix-toast__msg", children: message })
|
|
18
41
|
] }),
|
|
19
|
-
onClose && /* @__PURE__ */ jsx("button", { className: "uix-toast__close", onClick:
|
|
42
|
+
onClose && /* @__PURE__ */ jsx("button", { className: "uix-toast__close", onClick: handleClose, "aria-label": "Dismiss", children: "\xD7" })
|
|
20
43
|
]
|
|
21
44
|
}
|
|
22
45
|
);
|
|
23
46
|
}
|
|
24
47
|
function Toaster({ children, className, ...props }) {
|
|
25
|
-
|
|
48
|
+
const [announced, setAnnounced] = useState("");
|
|
49
|
+
const clearTimer = useRef(null);
|
|
50
|
+
const announce = useCallback((text) => {
|
|
51
|
+
setAnnounced((cur) => cur ? `${cur} ${text}` : text);
|
|
52
|
+
if (clearTimer.current != null) clearTimeout(clearTimer.current);
|
|
53
|
+
clearTimer.current = window.setTimeout(() => setAnnounced(""), 3e3);
|
|
54
|
+
}, []);
|
|
55
|
+
useEffect(() => () => {
|
|
56
|
+
if (clearTimer.current != null) clearTimeout(clearTimer.current);
|
|
57
|
+
}, []);
|
|
58
|
+
const ctx = useMemo(() => ({ announce }), [announce]);
|
|
59
|
+
return /* @__PURE__ */ jsx(ToasterContext.Provider, { value: ctx, children: /* @__PURE__ */ jsxs("div", { className: cx("uix-toaster", className), role: "region", "aria-label": "Notifications", ...props, children: [
|
|
60
|
+
/* @__PURE__ */ jsx("span", { className: "uix-visually-hidden", role: "status", "aria-live": "polite", children: announced }),
|
|
61
|
+
children
|
|
62
|
+
] }) });
|
|
26
63
|
}
|
|
27
64
|
export {
|
|
28
65
|
Toast,
|
|
@@ -13,7 +13,9 @@ interface TooltipProps extends HTMLAttributes<HTMLSpanElement> {
|
|
|
13
13
|
* Tooltip whose bubble renders in the top layer via the native Popover API, so it
|
|
14
14
|
* escapes any `overflow: hidden/scroll` ancestor that would clip the old CSS-only
|
|
15
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
|
|
16
|
+
* (flip/shift), shown on hover and keyboard focus, dismissed on blur or Escape
|
|
17
|
+
* (document-level, per WCAG 1.4.13), and hoverable: the bubble stays open while
|
|
18
|
+
* the pointer is over it (UIX-A11Y-1).
|
|
17
19
|
*/
|
|
18
20
|
declare function Tooltip({ label, placement, className, children, ...props }: TooltipProps): react.JSX.Element;
|
|
19
21
|
|
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useEffect, useId, useRef, useState } from "react";
|
|
3
|
+
import { cloneElement, isValidElement, useEffect, useId, useRef, useState } from "react";
|
|
4
4
|
import { cx } from "../cx.js";
|
|
5
5
|
import { useAnchoredPosition } from "../hooks/useAnchoredPosition.js";
|
|
6
6
|
function Tooltip({ label, placement = "top", className, children, ...props }) {
|
|
7
7
|
const triggerRef = useRef(null);
|
|
8
8
|
const bubbleRef = useRef(null);
|
|
9
|
+
const closeTimer = useRef(null);
|
|
9
10
|
const [open, setOpen] = useState(false);
|
|
10
11
|
const id = useId();
|
|
11
12
|
useAnchoredPosition(triggerRef, bubbleRef, { open, placement, offset: 6 });
|
|
13
|
+
const cancelClose = () => {
|
|
14
|
+
if (closeTimer.current != null) {
|
|
15
|
+
clearTimeout(closeTimer.current);
|
|
16
|
+
closeTimer.current = null;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
12
19
|
const show = () => {
|
|
20
|
+
cancelClose();
|
|
13
21
|
const b = bubbleRef.current;
|
|
14
22
|
if (b && !b.matches(":popover-open")) {
|
|
15
23
|
try {
|
|
@@ -20,6 +28,7 @@ function Tooltip({ label, placement = "top", className, children, ...props }) {
|
|
|
20
28
|
setOpen(true);
|
|
21
29
|
};
|
|
22
30
|
const hide = () => {
|
|
31
|
+
cancelClose();
|
|
23
32
|
const b = bubbleRef.current;
|
|
24
33
|
if (b && b.matches(":popover-open")) {
|
|
25
34
|
try {
|
|
@@ -29,7 +38,20 @@ function Tooltip({ label, placement = "top", className, children, ...props }) {
|
|
|
29
38
|
}
|
|
30
39
|
setOpen(false);
|
|
31
40
|
};
|
|
41
|
+
const scheduleHide = () => {
|
|
42
|
+
cancelClose();
|
|
43
|
+
closeTimer.current = window.setTimeout(hide, 100);
|
|
44
|
+
};
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
if (!open) return;
|
|
47
|
+
const onKey = (e) => {
|
|
48
|
+
if (e.key === "Escape") hide();
|
|
49
|
+
};
|
|
50
|
+
document.addEventListener("keydown", onKey);
|
|
51
|
+
return () => document.removeEventListener("keydown", onKey);
|
|
52
|
+
}, [open]);
|
|
32
53
|
useEffect(() => () => {
|
|
54
|
+
if (closeTimer.current != null) clearTimeout(closeTimer.current);
|
|
33
55
|
const b = bubbleRef.current;
|
|
34
56
|
if (b && b.matches(":popover-open")) {
|
|
35
57
|
try {
|
|
@@ -39,14 +61,19 @@ function Tooltip({ label, placement = "top", className, children, ...props }) {
|
|
|
39
61
|
}
|
|
40
62
|
}, []);
|
|
41
63
|
const bubbleAttr = { popover: "manual" };
|
|
64
|
+
const single = isValidElement(children);
|
|
65
|
+
const child = single ? cloneElement(children, {
|
|
66
|
+
"aria-describedby": [children.props["aria-describedby"], id].filter(Boolean).join(" ")
|
|
67
|
+
}) : children;
|
|
42
68
|
return /* @__PURE__ */ jsxs(
|
|
43
69
|
"span",
|
|
44
70
|
{
|
|
45
71
|
ref: triggerRef,
|
|
46
72
|
className: cx("uix-tooltip", className),
|
|
47
|
-
"aria-describedby": id,
|
|
73
|
+
"aria-describedby": single ? void 0 : id,
|
|
74
|
+
tabIndex: single ? void 0 : 0,
|
|
48
75
|
onMouseEnter: show,
|
|
49
|
-
onMouseLeave:
|
|
76
|
+
onMouseLeave: scheduleHide,
|
|
50
77
|
onFocus: show,
|
|
51
78
|
onBlur: hide,
|
|
52
79
|
onKeyDown: (e) => {
|
|
@@ -54,8 +81,20 @@ function Tooltip({ label, placement = "top", className, children, ...props }) {
|
|
|
54
81
|
},
|
|
55
82
|
...props,
|
|
56
83
|
children: [
|
|
57
|
-
|
|
58
|
-
/* @__PURE__ */ jsx(
|
|
84
|
+
child,
|
|
85
|
+
/* @__PURE__ */ jsx(
|
|
86
|
+
"span",
|
|
87
|
+
{
|
|
88
|
+
ref: bubbleRef,
|
|
89
|
+
id,
|
|
90
|
+
role: "tooltip",
|
|
91
|
+
className: "uix-tooltip__bubble",
|
|
92
|
+
onMouseEnter: cancelClose,
|
|
93
|
+
onMouseLeave: scheduleHide,
|
|
94
|
+
...bubbleAttr,
|
|
95
|
+
children: label
|
|
96
|
+
}
|
|
97
|
+
)
|
|
59
98
|
]
|
|
60
99
|
}
|
|
61
100
|
);
|
|
@@ -4,6 +4,8 @@ import { HTMLAttributes, ReactNode } from 'react';
|
|
|
4
4
|
interface TreeNodeData {
|
|
5
5
|
id: string;
|
|
6
6
|
label: ReactNode;
|
|
7
|
+
/** Plain-text label for keyboard typeahead when `label` is not a string (UIX-A11Y-2). */
|
|
8
|
+
typeaheadLabel?: string;
|
|
7
9
|
icon?: ReactNode;
|
|
8
10
|
children?: TreeNodeData[];
|
|
9
11
|
}
|
package/dist/components/Tree.js
CHANGED
|
@@ -152,6 +152,13 @@ function Tree({
|
|
|
152
152
|
setFocusedId(id);
|
|
153
153
|
break;
|
|
154
154
|
default:
|
|
155
|
+
if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
|
|
156
|
+
const act = treeNav(flat, id, e.key);
|
|
157
|
+
if (act.focusId !== void 0) {
|
|
158
|
+
e.preventDefault();
|
|
159
|
+
focusItem(rootRef.current?.querySelector(`[role="treeitem"][data-id="${CSS.escape(act.focusId)}"]`));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
155
162
|
break;
|
|
156
163
|
}
|
|
157
164
|
};
|
|
@@ -188,8 +195,10 @@ function VirtualTreeView({
|
|
|
188
195
|
const scrollRef = useRef(null);
|
|
189
196
|
const [scrollTop, setScrollTop] = useState(0);
|
|
190
197
|
const pendingFocus = useRef(false);
|
|
198
|
+
const hadFocus = useRef(false);
|
|
191
199
|
const win = virtualWindow(scrollTop, maxHeight, rowHeight, flat.length);
|
|
192
200
|
const rows = flat.slice(win.start, win.end);
|
|
201
|
+
const windowHasTabbable = tabbableId != null && rows.some((f) => f.node.id === tabbableId);
|
|
193
202
|
useEffect(() => {
|
|
194
203
|
if (!pendingFocus.current || focusedId == null) return;
|
|
195
204
|
const el = scrollRef.current?.querySelector(`[role="treeitem"][data-id="${CSS.escape(focusedId)}"]`);
|
|
@@ -198,6 +207,11 @@ function VirtualTreeView({
|
|
|
198
207
|
pendingFocus.current = false;
|
|
199
208
|
}
|
|
200
209
|
}, [focusedId, win.start, win.end]);
|
|
210
|
+
useEffect(() => {
|
|
211
|
+
if (!hadFocus.current || pendingFocus.current) return;
|
|
212
|
+
const active = document.activeElement;
|
|
213
|
+
if (active === document.body || active == null) scrollRef.current?.focus({ preventScroll: true });
|
|
214
|
+
});
|
|
201
215
|
const moveTo = (id) => {
|
|
202
216
|
const i = flat.findIndex((f) => f.node.id === id);
|
|
203
217
|
if (i < 0) return;
|
|
@@ -213,6 +227,7 @@ function VirtualTreeView({
|
|
|
213
227
|
setFocusedId(id);
|
|
214
228
|
};
|
|
215
229
|
const onKeyDown = (e) => {
|
|
230
|
+
if (e.key.length === 1 && (e.ctrlKey || e.metaKey || e.altKey)) return;
|
|
216
231
|
const curId = e.target.closest('[role="treeitem"]')?.dataset.id ?? focusedId;
|
|
217
232
|
const act = treeNav(flat, curId, e.key);
|
|
218
233
|
if (act.focusId === void 0 && act.toggleId === void 0 && act.selectId === void 0) return;
|
|
@@ -233,13 +248,20 @@ function VirtualTreeView({
|
|
|
233
248
|
ref: scrollRef,
|
|
234
249
|
className: cx("uix-tree", "uix-tree--virtual", className),
|
|
235
250
|
role: "tree",
|
|
251
|
+
tabIndex: -1,
|
|
236
252
|
style: { maxHeight, overflow: "auto" },
|
|
237
253
|
onScroll: (e) => setScrollTop(e.currentTarget.scrollTop),
|
|
238
254
|
onKeyDown,
|
|
255
|
+
onFocus: () => {
|
|
256
|
+
hadFocus.current = true;
|
|
257
|
+
},
|
|
258
|
+
onBlur: (e) => {
|
|
259
|
+
if (!e.currentTarget.contains(e.relatedTarget)) hadFocus.current = false;
|
|
260
|
+
},
|
|
239
261
|
...props,
|
|
240
262
|
children: [
|
|
241
|
-
win.padTop > 0 && /* @__PURE__ */ jsx("li", { "aria-hidden": "true", style: { height: win.padTop } }),
|
|
242
|
-
rows.map((f) => {
|
|
263
|
+
win.padTop > 0 && /* @__PURE__ */ jsx("li", { role: "presentation", "aria-hidden": "true", style: { height: win.padTop } }),
|
|
264
|
+
rows.map((f, i) => {
|
|
243
265
|
const rowStyle = { height: rowHeight, paddingLeft: `calc(${f.level - 1} * var(--uix-space-5))` };
|
|
244
266
|
return /* @__PURE__ */ jsx(
|
|
245
267
|
"li",
|
|
@@ -251,7 +273,7 @@ function VirtualTreeView({
|
|
|
251
273
|
"aria-expanded": f.hasChildren ? f.isExpanded : void 0,
|
|
252
274
|
"aria-selected": selectable ? selected === f.node.id : void 0,
|
|
253
275
|
"data-id": f.node.id,
|
|
254
|
-
tabIndex: tabbableId === f.node.id ? 0 : -1,
|
|
276
|
+
tabIndex: tabbableId === f.node.id || !windowHasTabbable && i === 0 ? 0 : -1,
|
|
255
277
|
className: "uix-tree__item",
|
|
256
278
|
onFocus: (e) => {
|
|
257
279
|
if (e.target === e.currentTarget) setFocusedId(f.node.id);
|
|
@@ -271,7 +293,7 @@ function VirtualTreeView({
|
|
|
271
293
|
f.node.id
|
|
272
294
|
);
|
|
273
295
|
}),
|
|
274
|
-
win.padBottom > 0 && /* @__PURE__ */ jsx("li", { "aria-hidden": "true", style: { height: win.padBottom } })
|
|
296
|
+
win.padBottom > 0 && /* @__PURE__ */ jsx("li", { role: "presentation", "aria-hidden": "true", style: { height: win.padBottom } })
|
|
275
297
|
]
|
|
276
298
|
}
|
|
277
299
|
);
|
package/dist/hooks/useDialog.js
CHANGED
|
@@ -30,8 +30,10 @@ function unlockBodyScroll() {
|
|
|
30
30
|
saved = null;
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
|
-
function useDialog(open) {
|
|
33
|
+
function useDialog(open, onClose) {
|
|
34
34
|
const ref = useRef(null);
|
|
35
|
+
const onCloseRef = useRef(onClose);
|
|
36
|
+
onCloseRef.current = onClose;
|
|
35
37
|
useEffect(() => {
|
|
36
38
|
const el = ref.current;
|
|
37
39
|
if (!el || !open) {
|
|
@@ -40,8 +42,21 @@ function useDialog(open) {
|
|
|
40
42
|
}
|
|
41
43
|
if (!el.open) el.showModal();
|
|
42
44
|
lockBodyScroll();
|
|
45
|
+
let locked = true;
|
|
46
|
+
const handleClose = () => {
|
|
47
|
+
if (locked) {
|
|
48
|
+
locked = false;
|
|
49
|
+
unlockBodyScroll();
|
|
50
|
+
}
|
|
51
|
+
onCloseRef.current?.();
|
|
52
|
+
};
|
|
53
|
+
el.addEventListener("close", handleClose);
|
|
43
54
|
return () => {
|
|
44
|
-
|
|
55
|
+
el.removeEventListener("close", handleClose);
|
|
56
|
+
if (locked) {
|
|
57
|
+
locked = false;
|
|
58
|
+
unlockBodyScroll();
|
|
59
|
+
}
|
|
45
60
|
if (el.open) el.close();
|
|
46
61
|
};
|
|
47
62
|
}, [open]);
|
package/dist/hooks/useTable.d.ts
CHANGED
|
@@ -21,6 +21,11 @@ interface UseTableResult<T> {
|
|
|
21
21
|
pinned: ReadonlySet<string>;
|
|
22
22
|
/** header select-all tri-state over the current derived rows. */
|
|
23
23
|
selectAll: SelectAllState;
|
|
24
|
+
/**
|
|
25
|
+
* Screen-reader status text reflecting the latest change — "Sorted by {field}, {dir}",
|
|
26
|
+
* "{n} results" (filter/search), "{n} selected". Pass to `<TableWrap announcement>` (UIX-A11Y-2).
|
|
27
|
+
*/
|
|
28
|
+
announcement: string;
|
|
24
29
|
/** cycle a column's sort; pass additive (⇧-click) to keep it as a secondary key. */
|
|
25
30
|
toggleSort: (field: string, additive?: boolean) => void;
|
|
26
31
|
setSort: (keys: SortKey[]) => void;
|
package/dist/hooks/useTable.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { useCallback, useMemo, useState } from "react";
|
|
2
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
3
|
import {
|
|
4
4
|
multiSort,
|
|
5
5
|
toggleSort,
|
|
@@ -38,6 +38,24 @@ function useTable(data, options = {}) {
|
|
|
38
38
|
const toggleAllRows = useCallback(() => setSelected((s) => togglePage(s, pageIds)), [pageIds]);
|
|
39
39
|
const clearSelection = useCallback(() => setSelected(/* @__PURE__ */ new Set()), []);
|
|
40
40
|
const togglePin = useCallback((id) => setPinned((s) => toggleId(s, id)), []);
|
|
41
|
+
const [announcement, setAnnouncement] = useState("");
|
|
42
|
+
const mounted = useRef(false);
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
if (!mounted.current) return;
|
|
45
|
+
const key = sort[0];
|
|
46
|
+
setAnnouncement(key ? `Sorted by ${key.field}, ${key.dir}` : "Sort cleared");
|
|
47
|
+
}, [sort]);
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (!mounted.current) return;
|
|
50
|
+
setAnnouncement(`${rows.length} result${rows.length === 1 ? "" : "s"}`);
|
|
51
|
+
}, [filters, query]);
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
if (!mounted.current) return;
|
|
54
|
+
setAnnouncement(`${selected.size} selected`);
|
|
55
|
+
}, [selected]);
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
mounted.current = true;
|
|
58
|
+
}, []);
|
|
41
59
|
const view = useMemo(() => ({ sort, filters, q: query }), [sort, filters, query]);
|
|
42
60
|
const viewQueryString = useMemo(() => serializeView(view), [view]);
|
|
43
61
|
const applyView = useCallback((queryString) => {
|
|
@@ -54,6 +72,7 @@ function useTable(data, options = {}) {
|
|
|
54
72
|
selected,
|
|
55
73
|
pinned,
|
|
56
74
|
selectAll,
|
|
75
|
+
announcement,
|
|
57
76
|
toggleSort: doToggleSort,
|
|
58
77
|
setSort,
|
|
59
78
|
setFilters,
|