@tensor_1/react 2.41.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/Button.d.ts +19 -1
- package/dist/components/Button.js +13 -8
- package/dist/components/DescriptionList.d.ts +11 -0
- package/dist/components/DiscardPrompt.d.ts +54 -0
- package/dist/components/DiscardPrompt.js +245 -0
- package/dist/components/Drawer.d.ts +19 -3
- package/dist/components/Drawer.js +60 -21
- package/dist/components/EditorialHome.d.ts +1 -1
- package/dist/components/Field.d.ts +7 -1
- package/dist/components/Field.js +5 -4
- package/dist/components/Input.d.ts +5 -0
- package/dist/components/Input.js +2 -1
- package/dist/components/Kbd.d.ts +20 -1
- package/dist/components/Kbd.js +30 -7
- package/dist/components/Menu.d.ts +11 -4
- package/dist/components/Menu.js +17 -3
- package/dist/components/MetricInput.d.ts +8 -1
- package/dist/components/MetricInput.js +11 -3
- package/dist/components/Modal.d.ts +15 -1
- package/dist/components/Modal.js +58 -13
- package/dist/components/OrgChart.d.ts +121 -0
- package/dist/components/OrgChart.js +683 -0
- package/dist/components/SearchBar.d.ts +67 -0
- package/dist/components/SearchBar.js +117 -0
- package/dist/components/Select.js +5 -4
- package/dist/components/ShortcutHint.d.ts +29 -0
- package/dist/components/ShortcutHint.js +149 -0
- package/dist/components/Tooltip.d.ts +10 -1
- package/dist/components/Tooltip.js +15 -4
- package/dist/emoji.cjs +108 -31
- package/dist/hooks/useAnchoredPosition.d.ts +13 -1
- package/dist/hooks/useAnchoredPosition.js +19 -11
- package/dist/hooks/useDialog.d.ts +6 -1
- package/dist/hooks/useDialog.js +10 -1
- package/dist/index.cjs +3911 -2567
- package/dist/index.d.cts +340 -39
- package/dist/index.d.ts +4 -1
- package/dist/index.js +11 -1
- package/dist/labels-context.d.ts +11 -0
- package/dist/rich-text.cjs +17 -10
- package/package.json +1 -1
|
@@ -5,6 +5,11 @@ interface InputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'size'>
|
|
|
5
5
|
size?: 'sm' | 'md';
|
|
6
6
|
invalid?: boolean;
|
|
7
7
|
valid?: boolean;
|
|
8
|
+
/**
|
|
9
|
+
* "Check this": a value the reader should confirm, not an error, e.g. one an assistant filled
|
|
10
|
+
* in (HAR-1814). Draws the warning border; `invalid` wins when both are set.
|
|
11
|
+
*/
|
|
12
|
+
warning?: boolean;
|
|
8
13
|
}
|
|
9
14
|
declare const Input: react.ForwardRefExoticComponent<InputProps & react.RefAttributes<HTMLInputElement>>;
|
|
10
15
|
interface InputGroupProps {
|
package/dist/components/Input.js
CHANGED
|
@@ -2,13 +2,14 @@ import { jsx, jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { cloneElement, forwardRef, isValidElement } from "react";
|
|
3
3
|
import { cx } from "../cx.js";
|
|
4
4
|
const Input = forwardRef(
|
|
5
|
-
({ size = "md", invalid, valid, className, ...props }, ref) => /* @__PURE__ */ jsx(
|
|
5
|
+
({ size = "md", invalid, valid, warning, className, ...props }, ref) => /* @__PURE__ */ jsx(
|
|
6
6
|
"input",
|
|
7
7
|
{
|
|
8
8
|
ref,
|
|
9
9
|
className: cx("uix-input", size === "sm" && "uix-input--sm", className),
|
|
10
10
|
"aria-invalid": invalid || void 0,
|
|
11
11
|
"data-valid": valid || void 0,
|
|
12
|
+
"data-warning": warning || void 0,
|
|
12
13
|
...props
|
|
13
14
|
}
|
|
14
15
|
)
|
package/dist/components/Kbd.d.ts
CHANGED
|
@@ -7,6 +7,25 @@ interface KbdProps extends HTMLAttributes<HTMLElement> {
|
|
|
7
7
|
/** One keyboard key over `.uix-kbd` (HAR-1361; TENSOR C14, MOTUS B-A16). */
|
|
8
8
|
declare function Kbd({ className, children, ...props }: KbdProps): react.JSX.Element;
|
|
9
9
|
type KbdPlatform = 'mac' | 'other';
|
|
10
|
+
/**
|
|
11
|
+
* The platform for glyphs and `aria-keyshortcuts`: `platform` when given, else the browser's. The
|
|
12
|
+
* server and the hydrating render use `other`; a component mounted later (a menu row on open)
|
|
13
|
+
* reads the platform at once, so it never flashes Ctrl before ⌘.
|
|
14
|
+
*/
|
|
15
|
+
declare function useKbdPlatform(platform?: KbdPlatform): KbdPlatform;
|
|
16
|
+
/**
|
|
17
|
+
* A keyboard shortcut (HAR-2031): the keys pressed together, in `KbdCombo`'s names, e.g.
|
|
18
|
+
* `['Mod', 'K']`, `['Shift', '?']` or `['A']`. `Mod` is ⌘ on Apple and Ctrl elsewhere.
|
|
19
|
+
*/
|
|
20
|
+
type KeyShortcut = readonly string[];
|
|
21
|
+
/**
|
|
22
|
+
* The `aria-keyshortcuts` value of a shortcut on a platform (HAR-2031): `['Mod', 'K']` is
|
|
23
|
+
* `Meta+K` on Apple and `Control+K` elsewhere; a single letter is upper-cased (`['a']` is `A`).
|
|
24
|
+
* For a control the kit does not render.
|
|
25
|
+
*/
|
|
26
|
+
declare function ariaKeyShortcuts(keys: KeyShortcut, platform: KbdPlatform): string;
|
|
27
|
+
/** A `shortcut` prop that is key names rather than a node (HAR-2031). */
|
|
28
|
+
declare const isKeyShortcut: (value: unknown) => value is KeyShortcut;
|
|
10
29
|
interface KbdComboProps extends Omit<HTMLAttributes<HTMLSpanElement>, 'children'> {
|
|
11
30
|
/**
|
|
12
31
|
* Keys in order, e.g. `['Mod', 'K']`. `Mod` is ⌘ on Apple and Ctrl elsewhere; `Shift`,
|
|
@@ -24,4 +43,4 @@ interface KbdComboProps extends Omit<HTMLAttributes<HTMLSpanElement>, 'children'
|
|
|
24
43
|
*/
|
|
25
44
|
declare function KbdCombo({ keys, platform, keyLabels, className, ...props }: KbdComboProps): react.JSX.Element;
|
|
26
45
|
|
|
27
|
-
export { Kbd, KbdCombo, type KbdComboProps, type KbdPlatform, type KbdProps };
|
|
46
|
+
export { Kbd, KbdCombo, type KbdComboProps, type KbdPlatform, type KbdProps, type KeyShortcut, ariaKeyShortcuts, isKeyShortcut, useKbdPlatform };
|
package/dist/components/Kbd.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
-
import {
|
|
3
|
+
import { useSyncExternalStore } from "react";
|
|
4
4
|
import { cx } from "../cx.js";
|
|
5
5
|
function Kbd({ className, children, ...props }) {
|
|
6
6
|
return /* @__PURE__ */ jsx("kbd", { className: cx("uix-kbd", className), ...props, children });
|
|
@@ -12,6 +12,11 @@ const KEYS = {
|
|
|
12
12
|
Alt: { mac: ["\u2325", "Option"], other: ["Alt", "Alt"] },
|
|
13
13
|
Enter: { mac: ["\u21B5", "Enter"], other: ["Enter", "Enter"] },
|
|
14
14
|
Escape: { mac: ["Esc", "Escape"], other: ["Esc", "Escape"] },
|
|
15
|
+
Esc: { mac: ["Esc", "Escape"], other: ["Esc", "Escape"] },
|
|
16
|
+
Delete: { mac: ["Del", "Delete"], other: ["Del", "Delete"] },
|
|
17
|
+
Del: { mac: ["Del", "Delete"], other: ["Del", "Delete"] },
|
|
18
|
+
Space: { mac: ["Space", "Space"], other: ["Space", "Space"] },
|
|
19
|
+
" ": { mac: ["Space", "Space"], other: ["Space", "Space"] },
|
|
15
20
|
Backspace: { mac: ["\u232B", "Backspace"], other: ["Backspace", "Backspace"] },
|
|
16
21
|
Tab: { mac: ["\u21E5", "Tab"], other: ["Tab", "Tab"] },
|
|
17
22
|
ArrowUp: { mac: ["\u2191", "Up arrow"], other: ["\u2191", "Up arrow"] },
|
|
@@ -24,12 +29,27 @@ const detectPlatform = () => {
|
|
|
24
29
|
const nav = navigator;
|
|
25
30
|
return /mac|iphone|ipad|ipod/i.test(nav.userAgentData?.platform ?? navigator.platform ?? "") ? "mac" : "other";
|
|
26
31
|
};
|
|
32
|
+
const noSubscription = () => () => {
|
|
33
|
+
};
|
|
34
|
+
const serverPlatform = () => "other";
|
|
35
|
+
function useKbdPlatform(platform) {
|
|
36
|
+
const detected = useSyncExternalStore(noSubscription, detectPlatform, serverPlatform);
|
|
37
|
+
return platform ?? detected;
|
|
38
|
+
}
|
|
39
|
+
const ARIA_KEYS = {
|
|
40
|
+
Mod: { mac: "Meta", other: "Control" },
|
|
41
|
+
Ctrl: { mac: "Control", other: "Control" },
|
|
42
|
+
Esc: { mac: "Escape", other: "Escape" },
|
|
43
|
+
Del: { mac: "Delete", other: "Delete" },
|
|
44
|
+
" ": { mac: "Space", other: "Space" },
|
|
45
|
+
"+": { mac: "Plus", other: "Plus" }
|
|
46
|
+
};
|
|
47
|
+
function ariaKeyShortcuts(keys, platform) {
|
|
48
|
+
return keys.map((key) => ARIA_KEYS[key]?.[platform] ?? (key.length === 1 ? key.toUpperCase() : key)).join("+");
|
|
49
|
+
}
|
|
50
|
+
const isKeyShortcut = (value) => Array.isArray(value) && value.length > 0 && value.every((key) => typeof key === "string");
|
|
27
51
|
function KbdCombo({ keys, platform, keyLabels, className, ...props }) {
|
|
28
|
-
const
|
|
29
|
-
useEffect(() => {
|
|
30
|
-
if (!platform) setDetected(detectPlatform());
|
|
31
|
-
}, [platform]);
|
|
32
|
-
const active = platform ?? detected;
|
|
52
|
+
const active = useKbdPlatform(platform);
|
|
33
53
|
const parts = keys.map((key) => {
|
|
34
54
|
const known = KEYS[key]?.[active];
|
|
35
55
|
return { glyph: known?.[0] ?? key, spoken: keyLabels?.[key] ?? known?.[1] ?? key };
|
|
@@ -41,5 +61,8 @@ function KbdCombo({ keys, platform, keyLabels, className, ...props }) {
|
|
|
41
61
|
}
|
|
42
62
|
export {
|
|
43
63
|
Kbd,
|
|
44
|
-
KbdCombo
|
|
64
|
+
KbdCombo,
|
|
65
|
+
ariaKeyShortcuts,
|
|
66
|
+
isKeyShortcut,
|
|
67
|
+
useKbdPlatform
|
|
45
68
|
};
|
|
@@ -2,6 +2,7 @@ import * as react from 'react';
|
|
|
2
2
|
import { ReactElement, ReactNode } from 'react';
|
|
3
3
|
import { UixRenderLink } from '../link.js';
|
|
4
4
|
import { Placement } from '../overlay-position.js';
|
|
5
|
+
import { KeyShortcut } from './Kbd.js';
|
|
5
6
|
|
|
6
7
|
/** `id` and `data-*` for the item's own element (HAR-1629): a stable hook for tests, journeys and analytics. */
|
|
7
8
|
interface MenuItemAttributes {
|
|
@@ -40,8 +41,12 @@ interface MenuItemProps extends MenuItemAttributes {
|
|
|
40
41
|
href?: string;
|
|
41
42
|
renderLink?: UixRenderLink;
|
|
42
43
|
icon?: ReactNode;
|
|
43
|
-
/**
|
|
44
|
-
|
|
44
|
+
/**
|
|
45
|
+
* The row's keyboard shortcut, at its end. Key names (`['Mod', 'D']`, `['A']`) render with
|
|
46
|
+
* `KbdCombo` and set the row's `aria-keyshortcuts` for the viewer's platform (HAR-2031); any
|
|
47
|
+
* other node renders as given.
|
|
48
|
+
*/
|
|
49
|
+
shortcut?: KeyShortcut | ReactNode;
|
|
45
50
|
tone?: 'default' | 'danger';
|
|
46
51
|
disabled?: boolean;
|
|
47
52
|
className?: string;
|
|
@@ -70,7 +75,8 @@ interface MenuItemRadioProps extends MenuItemAttributes {
|
|
|
70
75
|
closeOnSelect?: boolean;
|
|
71
76
|
children?: ReactNode;
|
|
72
77
|
icon?: ReactNode;
|
|
73
|
-
shortcut
|
|
78
|
+
/** Key names (`['Shift', 'W']`) or a node; see `MenuItemProps.shortcut`. */
|
|
79
|
+
shortcut?: KeyShortcut | ReactNode;
|
|
74
80
|
disabled?: boolean;
|
|
75
81
|
className?: string;
|
|
76
82
|
}
|
|
@@ -83,7 +89,8 @@ interface MenuItemCheckboxProps extends MenuItemAttributes {
|
|
|
83
89
|
closeOnSelect?: boolean;
|
|
84
90
|
children?: ReactNode;
|
|
85
91
|
icon?: ReactNode;
|
|
86
|
-
shortcut
|
|
92
|
+
/** Key names (`['Shift', 'W']`) or a node; see `MenuItemProps.shortcut`. */
|
|
93
|
+
shortcut?: KeyShortcut | ReactNode;
|
|
87
94
|
disabled?: boolean;
|
|
88
95
|
className?: string;
|
|
89
96
|
}
|
package/dist/components/Menu.js
CHANGED
|
@@ -5,6 +5,16 @@ import { cx } from "../cx.js";
|
|
|
5
5
|
import { Popover } from "./Popover.js";
|
|
6
6
|
import { CheckIcon } from "../icons/components.js";
|
|
7
7
|
import { renderUixLink } from "../link.js";
|
|
8
|
+
import { KbdCombo, ariaKeyShortcuts, isKeyShortcut, useKbdPlatform } from "./Kbd.js";
|
|
9
|
+
function useRowShortcut(shortcut) {
|
|
10
|
+
const platform = useKbdPlatform();
|
|
11
|
+
if (shortcut == null) return { node: null, aria: {} };
|
|
12
|
+
if (!isKeyShortcut(shortcut)) return { node: /* @__PURE__ */ jsx("span", { className: "uix-menu__shortcut", children: shortcut }), aria: {} };
|
|
13
|
+
return {
|
|
14
|
+
node: /* @__PURE__ */ jsx("span", { className: "uix-menu__shortcut", children: /* @__PURE__ */ jsx(KbdCombo, { keys: [...shortcut], platform }) }),
|
|
15
|
+
aria: { "aria-keyshortcuts": ariaKeyShortcuts(shortcut, platform) }
|
|
16
|
+
};
|
|
17
|
+
}
|
|
8
18
|
const MenuContext = createContext(null);
|
|
9
19
|
const MenuRadioContext = createContext(null);
|
|
10
20
|
const ITEM = '[role^="menuitem"]:not([aria-disabled="true"])';
|
|
@@ -150,18 +160,20 @@ function MenuItem({ children, onSelect, href, renderLink, icon, shortcut, tone =
|
|
|
150
160
|
const menu = useContext(MenuContext);
|
|
151
161
|
const classes = cx("uix-menu__item", tone === "danger" && "uix-menu__item--danger", className);
|
|
152
162
|
const attributes = itemAttributes(rest);
|
|
163
|
+
const keys = useRowShortcut(shortcut);
|
|
153
164
|
const body = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
154
165
|
icon != null && /* @__PURE__ */ jsx("span", { className: "uix-menu__icon", "aria-hidden": "true", children: icon }),
|
|
155
166
|
/* @__PURE__ */ jsx("span", { className: "uix-menu__text", children }),
|
|
156
|
-
|
|
167
|
+
keys.node
|
|
157
168
|
] });
|
|
158
169
|
if (href != null && !disabled) {
|
|
159
|
-
return /* @__PURE__ */ jsx(Fragment, { children: renderUixLink(renderLink, { ...attributes, href, role: "menuitem", tabIndex: -1, className: classes, onClick: () => menu?.close(false), children: body }) });
|
|
170
|
+
return /* @__PURE__ */ jsx(Fragment, { children: renderUixLink(renderLink, { ...attributes, ...keys.aria, href, role: "menuitem", tabIndex: -1, className: classes, onClick: () => menu?.close(false), children: body }) });
|
|
160
171
|
}
|
|
161
172
|
return /* @__PURE__ */ jsx(
|
|
162
173
|
"button",
|
|
163
174
|
{
|
|
164
175
|
...attributes,
|
|
176
|
+
...keys.aria,
|
|
165
177
|
type: "button",
|
|
166
178
|
role: "menuitem",
|
|
167
179
|
tabIndex: -1,
|
|
@@ -189,10 +201,12 @@ function CheckableItem({
|
|
|
189
201
|
...rest
|
|
190
202
|
}) {
|
|
191
203
|
const menu = useContext(MenuContext);
|
|
204
|
+
const keys = useRowShortcut(shortcut);
|
|
192
205
|
return /* @__PURE__ */ jsxs(
|
|
193
206
|
"button",
|
|
194
207
|
{
|
|
195
208
|
...itemAttributes(rest),
|
|
209
|
+
...keys.aria,
|
|
196
210
|
type: "button",
|
|
197
211
|
role,
|
|
198
212
|
"aria-checked": checked,
|
|
@@ -216,7 +230,7 @@ function CheckableItem({
|
|
|
216
230
|
/* @__PURE__ */ jsx("span", { className: "uix-menu__check", "aria-hidden": "true", children: checked && /* @__PURE__ */ jsx(CheckIcon, {}) }),
|
|
217
231
|
icon != null && /* @__PURE__ */ jsx("span", { className: "uix-menu__icon", "aria-hidden": "true", children: icon }),
|
|
218
232
|
/* @__PURE__ */ jsx("span", { className: "uix-menu__text", children }),
|
|
219
|
-
|
|
233
|
+
keys.node
|
|
220
234
|
]
|
|
221
235
|
}
|
|
222
236
|
);
|
|
@@ -10,6 +10,13 @@ interface MetricInputProps {
|
|
|
10
10
|
label: string;
|
|
11
11
|
invalid?: boolean;
|
|
12
12
|
errorMessage?: string;
|
|
13
|
+
/**
|
|
14
|
+
* A required value (HAR-1819): the label shows the same marker as `Field`, and the input
|
|
15
|
+
* gets native `required` and `aria-required="true"`.
|
|
16
|
+
*/
|
|
17
|
+
required?: boolean;
|
|
18
|
+
/** Guidance under the control (a range, a unit rule), tied to the input by `aria-describedby`. */
|
|
19
|
+
hint?: string;
|
|
13
20
|
disabled?: boolean;
|
|
14
21
|
readOnly?: boolean;
|
|
15
22
|
parse?: (input: string) => number | null;
|
|
@@ -18,6 +25,6 @@ interface MetricInputProps {
|
|
|
18
25
|
id?: string;
|
|
19
26
|
}
|
|
20
27
|
/** Accessible numeric recipe composed from Input and the UIx stepper controls. */
|
|
21
|
-
declare function MetricInput({ value, onValueChange, min, max, step, unit, label, invalid, errorMessage, disabled, readOnly, parse, format, className, id: providedId, }: MetricInputProps): react.JSX.Element;
|
|
28
|
+
declare function MetricInput({ value, onValueChange, min, max, step, unit, label, invalid, errorMessage, required, hint, disabled, readOnly, parse, format, className, id: providedId, }: MetricInputProps): react.JSX.Element;
|
|
22
29
|
|
|
23
30
|
export { MetricInput, type MetricInputProps };
|
|
@@ -15,6 +15,8 @@ function MetricInput({
|
|
|
15
15
|
label,
|
|
16
16
|
invalid,
|
|
17
17
|
errorMessage,
|
|
18
|
+
required,
|
|
19
|
+
hint,
|
|
18
20
|
disabled,
|
|
19
21
|
readOnly,
|
|
20
22
|
parse = parseMetricValue,
|
|
@@ -25,6 +27,9 @@ function MetricInput({
|
|
|
25
27
|
const generatedId = useId();
|
|
26
28
|
const id = providedId ?? generatedId;
|
|
27
29
|
const errorId = `${id}-error`;
|
|
30
|
+
const hintId = `${id}-hint`;
|
|
31
|
+
const showError = Boolean(invalid && errorMessage);
|
|
32
|
+
const describedBy = [hint ? hintId : null, showError ? errorId : null].filter(Boolean).join(" ") || void 0;
|
|
28
33
|
const [text, setText] = useState(() => format(value));
|
|
29
34
|
useEffect(() => setText(format(value)), [value, format]);
|
|
30
35
|
const commit = (input) => {
|
|
@@ -53,7 +58,7 @@ function MetricInput({
|
|
|
53
58
|
setText(parsed === null && event.currentTarget.value.trim() !== "" ? format(value) : format(parsed));
|
|
54
59
|
};
|
|
55
60
|
return /* @__PURE__ */ jsxs("div", { className: cx("uix-metric-input", invalid && "uix-metric-input--invalid", className), children: [
|
|
56
|
-
/* @__PURE__ */ jsx("label", { className: "uix-field__label", htmlFor: id, children: label }),
|
|
61
|
+
/* @__PURE__ */ jsx("label", { className: "uix-field__label", htmlFor: id, "data-required": required || void 0, children: label }),
|
|
57
62
|
/* @__PURE__ */ jsxs("div", { className: "uix-metric-input__control", children: [
|
|
58
63
|
/* @__PURE__ */ jsx(
|
|
59
64
|
Input,
|
|
@@ -68,7 +73,9 @@ function MetricInput({
|
|
|
68
73
|
"aria-valuenow": value ?? void 0,
|
|
69
74
|
"aria-valuemin": min,
|
|
70
75
|
"aria-valuemax": max,
|
|
71
|
-
"aria-describedby":
|
|
76
|
+
"aria-describedby": describedBy,
|
|
77
|
+
required,
|
|
78
|
+
"aria-required": required || void 0,
|
|
72
79
|
invalid,
|
|
73
80
|
disabled,
|
|
74
81
|
readOnly
|
|
@@ -84,7 +91,8 @@ function MetricInput({
|
|
|
84
91
|
"Unit: ",
|
|
85
92
|
unit
|
|
86
93
|
] }),
|
|
87
|
-
|
|
94
|
+
hint && /* @__PURE__ */ jsx("span", { id: hintId, className: "uix-field__hint", children: hint }),
|
|
95
|
+
showError && /* @__PURE__ */ jsx("span", { id: errorId, className: "uix-field__error", children: errorMessage })
|
|
88
96
|
] });
|
|
89
97
|
}
|
|
90
98
|
export {
|
|
@@ -1,9 +1,23 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
2
|
import { HTMLAttributes, ReactNode } from 'react';
|
|
3
|
+
import { DiscardPromptLabels } from './DiscardPrompt.js';
|
|
4
|
+
import './Button.js';
|
|
5
|
+
import '../link.js';
|
|
6
|
+
import './Kbd.js';
|
|
3
7
|
|
|
4
8
|
interface ModalProps extends Omit<HTMLAttributes<HTMLDialogElement>, 'title'> {
|
|
5
9
|
open: boolean;
|
|
6
10
|
onClose?: () => void;
|
|
11
|
+
/**
|
|
12
|
+
* The dialog holds input that is not saved (HAR-2029). Escape, the close button and a
|
|
13
|
+
* `DialogClose` then open an anchored "Discard changes?" step with focus on "Keep editing"
|
|
14
|
+
* instead of calling `onClose`; "Discard" calls it. A clean dialog closes at once.
|
|
15
|
+
*/
|
|
16
|
+
dirty?: boolean;
|
|
17
|
+
/** A save is in flight: Escape, the close button and a `DialogClose` do nothing. */
|
|
18
|
+
pending?: boolean;
|
|
19
|
+
/** The words of the discard step. Default: `UixLabelsProvider` `discardPrompt`, then English. */
|
|
20
|
+
discardLabels?: Partial<DiscardPromptLabels>;
|
|
7
21
|
title?: ReactNode;
|
|
8
22
|
children?: ReactNode;
|
|
9
23
|
footer?: ReactNode;
|
|
@@ -11,6 +25,6 @@ interface ModalProps extends Omit<HTMLAttributes<HTMLDialogElement>, 'title'> {
|
|
|
11
25
|
closeLabel?: string;
|
|
12
26
|
role?: 'dialog' | 'alertdialog';
|
|
13
27
|
}
|
|
14
|
-
declare function Modal({ open, onClose, title, children, footer, className, closeLabel: closeLabelProp, role, ...rest }: ModalProps): react.JSX.Element;
|
|
28
|
+
declare function Modal({ open, onClose, dirty, pending, discardLabels, title, children, footer, className, closeLabel: closeLabelProp, role, onKeyDown, ...rest }: ModalProps): react.JSX.Element;
|
|
15
29
|
|
|
16
30
|
export { Modal, type ModalProps };
|
package/dist/components/Modal.js
CHANGED
|
@@ -1,23 +1,68 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useId } from "react";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useId, useRef } from "react";
|
|
4
4
|
import { cx } from "../cx.js";
|
|
5
5
|
import { useUixLabels } from "../labels-context.js";
|
|
6
|
-
import {
|
|
6
|
+
import { useGuardedDialog } from "./DiscardPrompt.js";
|
|
7
7
|
const CloseIcon = () => /* @__PURE__ */ jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: "M4 4l8 8M12 4l-8 8" }) });
|
|
8
|
-
function Modal({
|
|
8
|
+
function Modal({
|
|
9
|
+
open,
|
|
10
|
+
onClose,
|
|
11
|
+
dirty = false,
|
|
12
|
+
pending = false,
|
|
13
|
+
discardLabels,
|
|
14
|
+
title,
|
|
15
|
+
children,
|
|
16
|
+
footer,
|
|
17
|
+
className,
|
|
18
|
+
closeLabel: closeLabelProp,
|
|
19
|
+
role = "dialog",
|
|
20
|
+
onKeyDown,
|
|
21
|
+
...rest
|
|
22
|
+
}) {
|
|
9
23
|
const uixLabels = useUixLabels();
|
|
10
24
|
const closeLabel = closeLabelProp ?? uixLabels.modal?.close ?? "Close dialog";
|
|
11
|
-
const
|
|
25
|
+
const closeRef = useRef(null);
|
|
26
|
+
const guard = useGuardedDialog({ open, onClose, dirty, pending, labels: discardLabels, closeRef });
|
|
12
27
|
const titleId = useId();
|
|
13
|
-
return /* @__PURE__ */ jsxs(
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
28
|
+
return /* @__PURE__ */ jsxs(
|
|
29
|
+
"dialog",
|
|
30
|
+
{
|
|
31
|
+
ref: guard.ref,
|
|
32
|
+
className: cx("uix-dialog", className),
|
|
33
|
+
role,
|
|
34
|
+
"aria-labelledby": title != null ? titleId : void 0,
|
|
35
|
+
...rest,
|
|
36
|
+
onKeyDown: (event) => {
|
|
37
|
+
onKeyDown?.(event);
|
|
38
|
+
guard.onKeyDown(event);
|
|
39
|
+
},
|
|
40
|
+
children: [
|
|
41
|
+
guard.provide(
|
|
42
|
+
/* @__PURE__ */ jsxs(Fragment, { children: [
|
|
43
|
+
(title != null || onClose) && /* @__PURE__ */ jsxs("div", { className: "uix-dialog__header", children: [
|
|
44
|
+
title && /* @__PURE__ */ jsx("h2", { className: "uix-dialog__title", id: titleId, children: title }),
|
|
45
|
+
onClose && /* @__PURE__ */ jsx(
|
|
46
|
+
"button",
|
|
47
|
+
{
|
|
48
|
+
ref: closeRef,
|
|
49
|
+
type: "button",
|
|
50
|
+
className: "uix-dialog__close",
|
|
51
|
+
onClick: () => guard.requestClose(),
|
|
52
|
+
"aria-label": closeLabel,
|
|
53
|
+
"aria-disabled": pending || void 0,
|
|
54
|
+
children: /* @__PURE__ */ jsx(CloseIcon, {})
|
|
55
|
+
}
|
|
56
|
+
)
|
|
57
|
+
] }),
|
|
58
|
+
children != null && /* @__PURE__ */ jsx("div", { className: "uix-dialog__body", children }),
|
|
59
|
+
footer != null && /* @__PURE__ */ jsx("div", { className: "uix-dialog__footer", children: footer })
|
|
60
|
+
] })
|
|
61
|
+
),
|
|
62
|
+
guard.prompt
|
|
63
|
+
]
|
|
64
|
+
}
|
|
65
|
+
);
|
|
21
66
|
}
|
|
22
67
|
export {
|
|
23
68
|
Modal
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { HTMLAttributes } from 'react';
|
|
3
|
+
|
|
4
|
+
/** One person (or position) in an {@link OrgChart}. */
|
|
5
|
+
interface OrgChartNode {
|
|
6
|
+
id: string;
|
|
7
|
+
/** The person's name; also the card's accessible name. */
|
|
8
|
+
name: string;
|
|
9
|
+
/** Job title, under the name. */
|
|
10
|
+
title?: string;
|
|
11
|
+
team?: string;
|
|
12
|
+
location?: string;
|
|
13
|
+
/** Photo URL. Without one the card shows {@link OrgChartNode.initials}, else the name's initials. */
|
|
14
|
+
photo?: string;
|
|
15
|
+
initials?: string;
|
|
16
|
+
/** A short marker on the card, such as "You". It is read with the person's description. */
|
|
17
|
+
badge?: string;
|
|
18
|
+
/**
|
|
19
|
+
* How many people report directly to this one. With `loadChildren`, a count above 0 shows the
|
|
20
|
+
* count on the card and makes the person expandable before {@link OrgChartNode.children} is
|
|
21
|
+
* known. Once the reports are known, their number wins.
|
|
22
|
+
*/
|
|
23
|
+
reportCount?: number;
|
|
24
|
+
/** The direct reports, when known. Otherwise `loadChildren` loads them on the first expand. */
|
|
25
|
+
children?: OrgChartNode[];
|
|
26
|
+
}
|
|
27
|
+
/** The words of an {@link OrgChart}. `{count}` and `{percent}` are filled in. */
|
|
28
|
+
interface OrgChartLabels {
|
|
29
|
+
/** The tree's name when the chart gets no `aria-label` / `aria-labelledby`. */
|
|
30
|
+
chart: string;
|
|
31
|
+
/** The row in place of reports that are loading. */
|
|
32
|
+
loading: string;
|
|
33
|
+
/** The row in place of reports that could not be loaded; also announced. */
|
|
34
|
+
error: string;
|
|
35
|
+
/** The action in that row. Enter, Space or a click on the row runs it. */
|
|
36
|
+
retry: string;
|
|
37
|
+
/** Read after the person: one direct report. */
|
|
38
|
+
reportsOne: string;
|
|
39
|
+
/** Read after the person: `{count}` direct reports. */
|
|
40
|
+
reportsMany: string;
|
|
41
|
+
/** The report count's tooltip while the reports are hidden. */
|
|
42
|
+
expand: string;
|
|
43
|
+
/** The report count's tooltip while the reports are shown. */
|
|
44
|
+
collapse: string;
|
|
45
|
+
/** The zoom controls' group name. */
|
|
46
|
+
toolbar: string;
|
|
47
|
+
zoomIn: string;
|
|
48
|
+
zoomOut: string;
|
|
49
|
+
/** Fit the chart's width into the frame. */
|
|
50
|
+
fit: string;
|
|
51
|
+
/** The current zoom, shown on the reset button: `{percent}`. */
|
|
52
|
+
zoomLevel: string;
|
|
53
|
+
/** The reset button's name; it should start with the visible level. `{percent}`. */
|
|
54
|
+
resetZoom: string;
|
|
55
|
+
}
|
|
56
|
+
declare const DEFAULT_ORG_CHART_LABELS: OrgChartLabels;
|
|
57
|
+
interface OrgChartProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onSelect' | 'onToggle' | 'children'> {
|
|
58
|
+
/** The top of the chart: the people without a manager, or wherever the chart should start. */
|
|
59
|
+
nodes: OrgChartNode[];
|
|
60
|
+
/** Expanded people (controlled). Expanding one whose reports are not known loads them. */
|
|
61
|
+
expanded?: ReadonlySet<string>;
|
|
62
|
+
/** Expanded people at first render (uncontrolled). */
|
|
63
|
+
defaultExpanded?: ReadonlySet<string>;
|
|
64
|
+
/** A person was expanded or collapsed, by a click on the report count or by the keyboard. */
|
|
65
|
+
onToggle?: (id: string, expanded: boolean) => void;
|
|
66
|
+
/** The chosen person: highlighted, and the tree's entry point for the Tab key. */
|
|
67
|
+
selected?: string | null;
|
|
68
|
+
/** A card was clicked, or Enter pressed on it: open the person. */
|
|
69
|
+
onSelect?: (id: string) => void;
|
|
70
|
+
/**
|
|
71
|
+
* Loads a person's direct reports the first time they are expanded, for a chart that grows on
|
|
72
|
+
* demand. While it runs a loading row stands in for the reports; a rejection shows an error row
|
|
73
|
+
* whose Enter, Space or click retries, and is announced. The result is kept for the life of the
|
|
74
|
+
* chart.
|
|
75
|
+
*/
|
|
76
|
+
loadChildren?: (node: OrgChartNode) => Promise<OrgChartNode[]>;
|
|
77
|
+
/**
|
|
78
|
+
* Bring this person into view and focus them once they are rendered: the target of a search.
|
|
79
|
+
* Expand their reporting line first (`expanded`); the chart waits until the card exists. Set it
|
|
80
|
+
* back to null in `onReveal`, so the same person can be revealed again later.
|
|
81
|
+
*/
|
|
82
|
+
revealId?: string | null;
|
|
83
|
+
/** Called once the {@link OrgChartProps.revealId} person has been scrolled to and focused. */
|
|
84
|
+
onReveal?: (id: string) => void;
|
|
85
|
+
/**
|
|
86
|
+
* Stack a row of people who have no reports of their own in one column under their manager,
|
|
87
|
+
* instead of side by side. Default true: a team of eight then takes one card's width.
|
|
88
|
+
*/
|
|
89
|
+
stackLeaves?: boolean;
|
|
90
|
+
/**
|
|
91
|
+
* Start at this zoom (0.25–1.5), or `'fit'` to fit the chart's width into the frame; the fit is
|
|
92
|
+
* kept as reports load until the reader zooms, pans or opens someone. Default 1.
|
|
93
|
+
*/
|
|
94
|
+
defaultZoom?: number | 'fit';
|
|
95
|
+
/** Words of the chart. Default: `UixLabelsProvider` `orgChart`, then English. */
|
|
96
|
+
labels?: Partial<OrgChartLabels>;
|
|
97
|
+
}
|
|
98
|
+
/** The next zoom step above (`dir` 1) or below (`dir` -1) `zoom`. */
|
|
99
|
+
declare const orgChartZoomStep: (zoom: number, dir: 1 | -1) => number;
|
|
100
|
+
/** "Mira Kovač" → "MK"; one word → its first letter. */
|
|
101
|
+
declare const orgChartInitials: (name: string) => string;
|
|
102
|
+
/**
|
|
103
|
+
* An org chart over `.uix-org-chart` (HAR-1807): managers above their direct reports, joined by
|
|
104
|
+
* lines, each person a card with photo or initials, name, title, team, location and their number
|
|
105
|
+
* of direct reports. Reports load on expand (`loadChildren`); a person can be revealed by id for
|
|
106
|
+
* search. The frame zooms (buttons, and Fit for the chart's width) around its centre, pans by
|
|
107
|
+
* dragging its background, scrolls, and keeps an opened manager's card in place while their
|
|
108
|
+
* reports appear. Below 48rem of its own width it becomes an indented vertical tree.
|
|
109
|
+
*
|
|
110
|
+
* A WAI-ARIA tree with one tab stop. In the chart: ↑ manager, ↓ first report (expanding them
|
|
111
|
+
* first), ← → the previous and next person on the same level (from a stacked column, the next
|
|
112
|
+
* column); in a stacked column ↑ ↓ move along it. In the list: the tree keys (↑ ↓ visible order,
|
|
113
|
+
* → expand / first report, ← collapse / manager). Both: Home / End, a letter jumps to the next
|
|
114
|
+
* visible person whose name starts with it, Enter opens the person (`onSelect`), Space shows or
|
|
115
|
+
* hides their reports.
|
|
116
|
+
*
|
|
117
|
+
* The root is an inline-size container: give it a width (a block, or `flex: 1` in a flex row).
|
|
118
|
+
*/
|
|
119
|
+
declare function OrgChart({ nodes, expanded: controlledExpanded, defaultExpanded, onToggle, selected, onSelect, loadChildren, revealId, onReveal, stackLeaves, defaultZoom, labels: labelOverrides, className, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledby, ...props }: OrgChartProps): react.JSX.Element;
|
|
120
|
+
|
|
121
|
+
export { DEFAULT_ORG_CHART_LABELS, OrgChart, type OrgChartLabels, type OrgChartNode, type OrgChartProps, orgChartInitials, orgChartZoomStep };
|