@tensor_1/react 2.41.0 → 2.42.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/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/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 +3184 -2521
- package/dist/index.d.cts +221 -39
- package/dist/index.d.ts +3 -1
- package/dist/index.js +6 -1
- package/dist/labels-context.d.ts +8 -0
- package/dist/rich-text.cjs +17 -10
- package/package.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
2
|
import { ButtonHTMLAttributes, ReactNode, AnchorHTMLAttributes } from 'react';
|
|
3
3
|
import { UixRenderLink } from '../link.js';
|
|
4
|
+
import { KeyShortcut } from './Kbd.js';
|
|
4
5
|
|
|
5
6
|
type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger' | 'link';
|
|
6
7
|
/** `xs` is 24 px: the smallest hit area WCAG 2.5.8 allows, for dense tool rows (HAR-1348). */
|
|
@@ -10,6 +11,21 @@ interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
|
10
11
|
size?: ButtonSize;
|
|
11
12
|
icon?: boolean;
|
|
12
13
|
loading?: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Let a long label wrap onto more lines: the size becomes a minimum height instead of a fixed
|
|
16
|
+
* one (HAR-1813, WCAG 1.4.10 reflow). For human-language labels on public or narrow screens;
|
|
17
|
+
* dense staff rows keep the one-line default. Ignored with `icon`.
|
|
18
|
+
*/
|
|
19
|
+
wrap?: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* The button's keyboard shortcut, in `KbdCombo` key names: `['A']`, `['Mod', 'Enter']`
|
|
22
|
+
* (HAR-2031). Hover or focus shows the keys in a hint above the button (an icon button's
|
|
23
|
+
* `aria-label` before them), and the button gets `aria-keyshortcuts` for the viewer's platform
|
|
24
|
+
* (⌘ / Meta on Apple, Ctrl / Control elsewhere), unless you pass `aria-keyshortcuts` yourself.
|
|
25
|
+
* Inside a `Tooltip` the Tooltip shows the keys in its own bubble. The kit only shows the
|
|
26
|
+
* shortcut: the product binds the key.
|
|
27
|
+
*/
|
|
28
|
+
shortcut?: KeyShortcut;
|
|
13
29
|
children?: ReactNode;
|
|
14
30
|
}
|
|
15
31
|
declare const Button: react.ForwardRefExoticComponent<ButtonProps & react.RefAttributes<HTMLButtonElement>>;
|
|
@@ -19,6 +35,8 @@ interface ButtonLinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
|
|
|
19
35
|
size?: ButtonSize;
|
|
20
36
|
/** Icon-only link: square, and needs `aria-label`. */
|
|
21
37
|
icon?: boolean;
|
|
38
|
+
/** Let a long label wrap (see `ButtonProps.wrap`, HAR-1813). Ignored with `icon`. */
|
|
39
|
+
wrap?: boolean;
|
|
22
40
|
/**
|
|
23
41
|
* Renders the link without `href`, with `aria-disabled="true"`, so it is announced
|
|
24
42
|
* as an unavailable link and cannot be followed or focused.
|
|
@@ -32,7 +50,7 @@ interface ButtonLinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
|
|
|
32
50
|
* A link that looks like a `Button` (HAR-1348; TENSOR A4/B5, MOTUS C-3). Use it when the
|
|
33
51
|
* action navigates; use `Button` when it acts in place.
|
|
34
52
|
*/
|
|
35
|
-
declare function ButtonLink({ href, variant, size, icon, disabled, renderLink, className, children, onClick, ...props }: ButtonLinkProps): react.JSX.Element;
|
|
53
|
+
declare function ButtonLink({ href, variant, size, icon, wrap, disabled, renderLink, className, children, onClick, ...props }: ButtonLinkProps): react.JSX.Element;
|
|
36
54
|
interface ButtonGroupProps {
|
|
37
55
|
children?: ReactNode;
|
|
38
56
|
className?: string;
|
|
@@ -1,26 +1,28 @@
|
|
|
1
|
-
import { Fragment, jsx } from "react/jsx-runtime";
|
|
1
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { forwardRef } from "react";
|
|
3
3
|
import { cx } from "../cx.js";
|
|
4
4
|
import { renderUixLink } from "../link.js";
|
|
5
|
-
|
|
5
|
+
import { ShortcutHint } from "./ShortcutHint.js";
|
|
6
|
+
const buttonClasses = (variant, size, icon, wrap, className) => cx(
|
|
6
7
|
"uix-btn",
|
|
7
8
|
`uix-btn--${variant}`,
|
|
8
9
|
size !== "md" && `uix-btn--${size}`,
|
|
9
10
|
icon && "uix-btn--icon",
|
|
11
|
+
wrap && !icon && "uix-btn--wrap",
|
|
10
12
|
className
|
|
11
13
|
);
|
|
12
14
|
let warnedIconButton = false;
|
|
13
15
|
const Button = forwardRef(
|
|
14
|
-
({ variant = "secondary", size = "md", icon = false, loading = false, className, children, disabled, onClick, ...props }, ref) => {
|
|
16
|
+
({ variant = "secondary", size = "md", icon = false, loading = false, wrap = false, shortcut, className, children, disabled, onClick, ...props }, ref) => {
|
|
15
17
|
if (!warnedIconButton && icon && children == null && !props["aria-label"] && !props["aria-labelledby"] && typeof process !== "undefined" && process.env.NODE_ENV !== "production") {
|
|
16
18
|
warnedIconButton = true;
|
|
17
19
|
console.warn("uix: icon-only <Button> has no accessible name \u2014 pass aria-label or aria-labelledby.");
|
|
18
20
|
}
|
|
19
|
-
return /* @__PURE__ */
|
|
21
|
+
return /* @__PURE__ */ jsxs(
|
|
20
22
|
"button",
|
|
21
23
|
{
|
|
22
24
|
ref,
|
|
23
|
-
className: buttonClasses(variant, size, icon, className),
|
|
25
|
+
className: buttonClasses(variant, size, icon, wrap, className),
|
|
24
26
|
disabled,
|
|
25
27
|
"aria-disabled": loading || void 0,
|
|
26
28
|
"aria-busy": loading || void 0,
|
|
@@ -33,14 +35,17 @@ const Button = forwardRef(
|
|
|
33
35
|
onClick?.(e);
|
|
34
36
|
} : void 0,
|
|
35
37
|
...props,
|
|
36
|
-
children
|
|
38
|
+
children: [
|
|
39
|
+
children,
|
|
40
|
+
shortcut && shortcut.length > 0 && /* @__PURE__ */ jsx(ShortcutHint, { keys: shortcut, label: icon ? props["aria-label"] : void 0, ownsAria: props["aria-keyshortcuts"] === void 0 })
|
|
41
|
+
]
|
|
37
42
|
}
|
|
38
43
|
);
|
|
39
44
|
}
|
|
40
45
|
);
|
|
41
46
|
Button.displayName = "Button";
|
|
42
|
-
function ButtonLink({ href, variant = "secondary", size = "md", icon = false, disabled = false, renderLink, className, children, onClick, ...props }) {
|
|
43
|
-
const classes = buttonClasses(variant, size, icon, className);
|
|
47
|
+
function ButtonLink({ href, variant = "secondary", size = "md", icon = false, wrap = false, disabled = false, renderLink, className, children, onClick, ...props }) {
|
|
48
|
+
const classes = buttonClasses(variant, size, icon, wrap, className);
|
|
44
49
|
if (disabled) {
|
|
45
50
|
return /* @__PURE__ */ jsx("a", { ...props, className: classes, role: "link", "aria-disabled": "true", children });
|
|
46
51
|
}
|
|
@@ -20,6 +20,17 @@ import { ReactNode, HTMLAttributes } from 'react';
|
|
|
20
20
|
* Use `items` for data-driven lists (the common case) or the `DescriptionItem`
|
|
21
21
|
* compound child for hand-authored content. Both render `<dt>`/`<dd>` pairs as
|
|
22
22
|
* direct children of the grid `<dl>`, so they align into the same columns.
|
|
23
|
+
*
|
|
24
|
+
* Below 22rem of its own width (a phone, a side rail, a narrow column) each label
|
|
25
|
+
* sits above its value and both take the full width (HAR-2032), so a long label
|
|
26
|
+
* never squeezes the value. A `DetailLayout` side column always stacks.
|
|
27
|
+
*
|
|
28
|
+
* The list is an inline-size container for this, so its content no longer sizes
|
|
29
|
+
* it: in a box sized by its content (a flex row, a `Popover` or hover card, a
|
|
30
|
+
* `max-content` tooltip, an auto-layout table cell, inline-block, a float) it is
|
|
31
|
+
* 12rem wide, and stacked, unless you size it (`flex: 1`, a width). And an
|
|
32
|
+
* unnamed `@container` query or a `cqi` unit inside a value now measures the
|
|
33
|
+
* list: name the container you mean (`@container/card`).
|
|
23
34
|
*/
|
|
24
35
|
interface DescriptionListItem {
|
|
25
36
|
/** Stable identity for the row; falls back to the list index when omitted. */
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { RefObject, KeyboardEvent, ReactNode } from 'react';
|
|
3
|
+
import { ButtonProps } from './Button.js';
|
|
4
|
+
import '../link.js';
|
|
5
|
+
import './Kbd.js';
|
|
6
|
+
|
|
7
|
+
/** The words of the "Discard changes?" step a `dirty` Drawer or Modal asks before it closes (HAR-2029). */
|
|
8
|
+
interface DiscardPromptLabels {
|
|
9
|
+
/** The question, e.g. "Discard changes?". It names the step. */
|
|
10
|
+
title: string;
|
|
11
|
+
/** What discarding means, under the question. */
|
|
12
|
+
description: string;
|
|
13
|
+
/** The safe choice. It has focus when the step opens. */
|
|
14
|
+
keepEditing: string;
|
|
15
|
+
/** Close and throw the input away. */
|
|
16
|
+
discard: string;
|
|
17
|
+
}
|
|
18
|
+
type DialogCloseProps = ButtonProps;
|
|
19
|
+
/**
|
|
20
|
+
* A button that closes the surrounding `Drawer` or `Modal` the way its × does (HAR-2029): at once
|
|
21
|
+
* when the form is clean, through the "Discard changes?" step (placed against this button) when
|
|
22
|
+
* the dialog is `dirty`, and not at all while it is `pending`. Use it for the footer's Cancel.
|
|
23
|
+
* Its own `onClick` runs first; `event.preventDefault()` there keeps the dialog open.
|
|
24
|
+
*/
|
|
25
|
+
declare const DialogClose: react.ForwardRefExoticComponent<ButtonProps & react.RefAttributes<HTMLButtonElement>>;
|
|
26
|
+
interface GuardedDialogOptions {
|
|
27
|
+
open: boolean;
|
|
28
|
+
onClose?: () => void;
|
|
29
|
+
dirty?: boolean;
|
|
30
|
+
pending?: boolean;
|
|
31
|
+
labels?: Partial<DiscardPromptLabels>;
|
|
32
|
+
/** The dialog's own × button: the step is placed against it unless a DialogClose asked. */
|
|
33
|
+
closeRef: RefObject<HTMLButtonElement | null>;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* `useDialog` with the dismissal guard of a form dialog (HAR-2029). Every way out (Escape, the
|
|
37
|
+
* ×, a backdrop click, a `DialogClose`) goes through `requestClose`: a clean dialog closes at
|
|
38
|
+
* once, a `dirty` one opens the anchored "Discard changes?" step, a `pending` one does nothing.
|
|
39
|
+
*
|
|
40
|
+
* Escape is taken on the dialog's own keydown, after the controls inside it had their turn (a
|
|
41
|
+
* Select closing its list cancels the event first), and cancelling that keydown is what keeps the
|
|
42
|
+
* browser from closing the dialog. A close request that was not a keydown (the Android back
|
|
43
|
+
* gesture) arrives as `cancel`. If the browser still closes a guarded dialog by force (it may
|
|
44
|
+
* ignore a cancelled `cancel` without user activation), the dialog reopens at once and asks.
|
|
45
|
+
*/
|
|
46
|
+
declare function useGuardedDialog({ open, onClose, dirty, pending, labels: labelsProp, closeRef }: GuardedDialogOptions): {
|
|
47
|
+
ref: RefObject<HTMLDialogElement>;
|
|
48
|
+
requestClose: (anchor?: HTMLElement | null) => void;
|
|
49
|
+
onKeyDown: (event: KeyboardEvent<HTMLDialogElement>) => void;
|
|
50
|
+
prompt: react.JSX.Element | null;
|
|
51
|
+
provide: (children: ReactNode) => react.JSX.Element;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export { DialogClose, type DialogCloseProps, type DiscardPromptLabels, type GuardedDialogOptions, useGuardedDialog };
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { createContext, forwardRef, useCallback, useContext, useEffect, useId, useLayoutEffect, useRef, useState } from "react";
|
|
4
|
+
import { Button } from "./Button.js";
|
|
5
|
+
import { Popover } from "./Popover.js";
|
|
6
|
+
import { useUixLabels } from "../labels-context.js";
|
|
7
|
+
import { useDialog } from "../hooks/useDialog.js";
|
|
8
|
+
const useIsomorphicLayoutEffect = typeof document !== "undefined" ? useLayoutEffect : useEffect;
|
|
9
|
+
const anyAutoPopoverOpen = () => {
|
|
10
|
+
try {
|
|
11
|
+
return !!document.querySelector('[popover]:not([popover="manual"]):popover-open');
|
|
12
|
+
} catch {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
const DEFAULT_DISCARD_LABELS = {
|
|
17
|
+
title: "Discard changes?",
|
|
18
|
+
description: "What you entered here will be lost.",
|
|
19
|
+
keepEditing: "Keep editing",
|
|
20
|
+
discard: "Discard"
|
|
21
|
+
};
|
|
22
|
+
const DialogCloseContext = createContext(null);
|
|
23
|
+
const DialogClose = forwardRef(({ onClick, type = "button", ...props }, ref) => {
|
|
24
|
+
const dialog = useContext(DialogCloseContext);
|
|
25
|
+
const pending = !!dialog?.pending;
|
|
26
|
+
return /* @__PURE__ */ jsx(
|
|
27
|
+
Button,
|
|
28
|
+
{
|
|
29
|
+
ref,
|
|
30
|
+
type,
|
|
31
|
+
...props,
|
|
32
|
+
...pending ? { "aria-disabled": true } : {},
|
|
33
|
+
onClick: (event) => {
|
|
34
|
+
if (pending) {
|
|
35
|
+
event.preventDefault();
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
onClick?.(event);
|
|
39
|
+
if (event.defaultPrevented) return;
|
|
40
|
+
dialog?.requestClose(event.currentTarget);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
DialogClose.displayName = "DialogClose";
|
|
46
|
+
function useGuardedDialog({ open, onClose, dirty = false, pending = false, labels: labelsProp, closeRef }) {
|
|
47
|
+
const uixLabels = useUixLabels();
|
|
48
|
+
const labels = { ...DEFAULT_DISCARD_LABELS, ...uixLabels.discardPrompt, ...labelsProp };
|
|
49
|
+
const panelId = `${useId()}-discard`;
|
|
50
|
+
const [prompt, setPromptState] = useState(null);
|
|
51
|
+
const latest = useRef({ open, onClose, dirty, pending, prompt });
|
|
52
|
+
useIsomorphicLayoutEffect(() => {
|
|
53
|
+
latest.current = { ...latest.current, open, onClose, dirty, pending };
|
|
54
|
+
});
|
|
55
|
+
const setPrompt = useCallback((next) => {
|
|
56
|
+
latest.current.prompt = next;
|
|
57
|
+
setPromptState(next);
|
|
58
|
+
}, []);
|
|
59
|
+
const guarded = () => !!latest.current.onClose && (latest.current.dirty || latest.current.pending);
|
|
60
|
+
const requestClose = useCallback((anchor) => {
|
|
61
|
+
const { open: isOpen, onClose: close, dirty: isDirty, pending: isPending, prompt: asking } = latest.current;
|
|
62
|
+
if (!isOpen || !close || isPending || asking) return;
|
|
63
|
+
if (!isDirty) {
|
|
64
|
+
close();
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const active = document.activeElement;
|
|
68
|
+
setPrompt({ anchor: anchor ?? closeRef.current, returnTo: active instanceof HTMLElement ? active : null });
|
|
69
|
+
}, [closeRef, setPrompt]);
|
|
70
|
+
const dialogRef = useRef(null);
|
|
71
|
+
const backOut = useRef({ focus: null, forced: false });
|
|
72
|
+
const keepEditing = useCallback(() => {
|
|
73
|
+
const asked = latest.current.prompt;
|
|
74
|
+
setPrompt(null);
|
|
75
|
+
const target = asked?.returnTo;
|
|
76
|
+
const dialog = dialogRef.current;
|
|
77
|
+
const back = target && target !== dialog && target.isConnected && dialog?.contains(target) ? target : asked?.anchor ?? closeRef.current;
|
|
78
|
+
backOut.current.focus = back ?? null;
|
|
79
|
+
back?.focus();
|
|
80
|
+
}, [closeRef, setPrompt]);
|
|
81
|
+
const ref = useDialog(open, onClose, () => {
|
|
82
|
+
if (!guarded()) return false;
|
|
83
|
+
if (backOut.current.forced) {
|
|
84
|
+
backOut.current.forced = false;
|
|
85
|
+
const field = backOut.current.focus;
|
|
86
|
+
queueMicrotask(() => field?.focus());
|
|
87
|
+
} else if (latest.current.prompt) keepEditing();
|
|
88
|
+
else requestClose();
|
|
89
|
+
return true;
|
|
90
|
+
});
|
|
91
|
+
dialogRef.current = ref.current;
|
|
92
|
+
const discard = useCallback(() => {
|
|
93
|
+
setPrompt(null);
|
|
94
|
+
latest.current.onClose?.();
|
|
95
|
+
}, [setPrompt]);
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
if (open && dirty && !pending) return;
|
|
98
|
+
if (!latest.current.prompt) return;
|
|
99
|
+
if (open && document.getElementById(panelId)?.contains(document.activeElement)) keepEditing();
|
|
100
|
+
else setPrompt(null);
|
|
101
|
+
}, [open, dirty, pending, panelId, keepEditing, setPrompt]);
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
const el = ref.current;
|
|
104
|
+
dialogRef.current = el;
|
|
105
|
+
if (!el || !open) return;
|
|
106
|
+
const ours = (event) => event.target?.closest?.("dialog") === el;
|
|
107
|
+
const onCancel = (event) => {
|
|
108
|
+
if (!guarded()) return;
|
|
109
|
+
event.preventDefault();
|
|
110
|
+
if (latest.current.prompt) {
|
|
111
|
+
keepEditing();
|
|
112
|
+
backOut.current.forced = !event.cancelable;
|
|
113
|
+
} else requestClose();
|
|
114
|
+
};
|
|
115
|
+
const outside = (event) => !document.getElementById(panelId)?.contains(event.target);
|
|
116
|
+
let swallow = null;
|
|
117
|
+
const forget = () => {
|
|
118
|
+
swallow = null;
|
|
119
|
+
};
|
|
120
|
+
const onPointerDownCapture = (event) => {
|
|
121
|
+
swallow = latest.current.prompt && ours(event) && outside(event) ? event.target : null;
|
|
122
|
+
if (swallow) setPrompt(null);
|
|
123
|
+
};
|
|
124
|
+
const onPointerUpCapture = () => {
|
|
125
|
+
window.setTimeout(forget, 0);
|
|
126
|
+
};
|
|
127
|
+
const onClickCapture = (event) => {
|
|
128
|
+
const pressed = swallow != null && swallow === event.target;
|
|
129
|
+
swallow = null;
|
|
130
|
+
if (!pressed && !(latest.current.prompt && ours(event) && outside(event))) return;
|
|
131
|
+
event.preventDefault();
|
|
132
|
+
event.stopPropagation();
|
|
133
|
+
setPrompt(null);
|
|
134
|
+
};
|
|
135
|
+
el.addEventListener("cancel", onCancel);
|
|
136
|
+
el.addEventListener("pointerdown", onPointerDownCapture, true);
|
|
137
|
+
el.addEventListener("pointerup", onPointerUpCapture, true);
|
|
138
|
+
el.addEventListener("pointercancel", forget, true);
|
|
139
|
+
el.addEventListener("contextmenu", forget, true);
|
|
140
|
+
el.addEventListener("click", onClickCapture, true);
|
|
141
|
+
el.addEventListener("keydown", forget, true);
|
|
142
|
+
return () => {
|
|
143
|
+
el.removeEventListener("cancel", onCancel);
|
|
144
|
+
el.removeEventListener("pointerdown", onPointerDownCapture, true);
|
|
145
|
+
el.removeEventListener("pointerup", onPointerUpCapture, true);
|
|
146
|
+
el.removeEventListener("pointercancel", forget, true);
|
|
147
|
+
el.removeEventListener("contextmenu", forget, true);
|
|
148
|
+
el.removeEventListener("click", onClickCapture, true);
|
|
149
|
+
el.removeEventListener("keydown", forget, true);
|
|
150
|
+
};
|
|
151
|
+
}, [open, ref, requestClose, keepEditing, setPrompt, panelId]);
|
|
152
|
+
const onKeyDown = (event) => {
|
|
153
|
+
if (event.key !== "Escape" || event.defaultPrevented) return;
|
|
154
|
+
if (event.target.closest("dialog") !== event.currentTarget || anyAutoPopoverOpen()) return;
|
|
155
|
+
if (latest.current.prompt) {
|
|
156
|
+
event.preventDefault();
|
|
157
|
+
keepEditing();
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (!guarded()) return;
|
|
161
|
+
event.preventDefault();
|
|
162
|
+
requestClose();
|
|
163
|
+
};
|
|
164
|
+
const element = dirty || prompt ? /* @__PURE__ */ jsx(
|
|
165
|
+
DiscardPrompt,
|
|
166
|
+
{
|
|
167
|
+
id: panelId,
|
|
168
|
+
prompt,
|
|
169
|
+
labels,
|
|
170
|
+
onKeepEditing: keepEditing,
|
|
171
|
+
onDismiss: () => setPrompt(null),
|
|
172
|
+
onDiscard: discard
|
|
173
|
+
}
|
|
174
|
+
) : null;
|
|
175
|
+
const provide = (children) => /* @__PURE__ */ jsx(DialogCloseContext.Provider, { value: { requestClose, pending }, children });
|
|
176
|
+
return { ref, requestClose, onKeyDown, prompt: element, provide };
|
|
177
|
+
}
|
|
178
|
+
function DiscardPrompt({ id, prompt, labels, onKeepEditing, onDismiss, onDiscard }) {
|
|
179
|
+
const keepRef = useRef(null);
|
|
180
|
+
const open = prompt != null;
|
|
181
|
+
useEffect(() => {
|
|
182
|
+
const panel = document.getElementById(id);
|
|
183
|
+
if (!panel) return;
|
|
184
|
+
const shown = () => {
|
|
185
|
+
try {
|
|
186
|
+
return panel.matches(":popover-open");
|
|
187
|
+
} catch {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
if (!open) {
|
|
192
|
+
if (shown()) {
|
|
193
|
+
try {
|
|
194
|
+
panel.hidePopover();
|
|
195
|
+
} catch {
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if (!shown()) {
|
|
201
|
+
try {
|
|
202
|
+
panel.showPopover();
|
|
203
|
+
} catch {
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
keepRef.current?.focus();
|
|
207
|
+
}, [open, id]);
|
|
208
|
+
return /* @__PURE__ */ jsxs(
|
|
209
|
+
Popover,
|
|
210
|
+
{
|
|
211
|
+
id,
|
|
212
|
+
popover: "manual",
|
|
213
|
+
anchor: prompt?.anchor ?? null,
|
|
214
|
+
placement: "bottom-end",
|
|
215
|
+
role: "alertdialog",
|
|
216
|
+
"aria-labelledby": `${id}-title`,
|
|
217
|
+
"aria-describedby": `${id}-body`,
|
|
218
|
+
className: "uix-popconfirm",
|
|
219
|
+
"data-uix-discard-prompt": "",
|
|
220
|
+
tabIndex: -1,
|
|
221
|
+
onKeyDown: (event) => {
|
|
222
|
+
if (event.key !== "Escape") return;
|
|
223
|
+
event.preventDefault();
|
|
224
|
+
event.stopPropagation();
|
|
225
|
+
onKeepEditing();
|
|
226
|
+
},
|
|
227
|
+
onBlur: (event) => {
|
|
228
|
+
const next = event.relatedTarget;
|
|
229
|
+
if (open && next && !event.currentTarget.contains(next)) onDismiss();
|
|
230
|
+
},
|
|
231
|
+
children: [
|
|
232
|
+
/* @__PURE__ */ jsx("div", { id: `${id}-title`, className: "uix-popconfirm__title", children: labels.title }),
|
|
233
|
+
/* @__PURE__ */ jsx("div", { id: `${id}-body`, className: "uix-popconfirm__body", children: labels.description }),
|
|
234
|
+
/* @__PURE__ */ jsxs("div", { className: "uix-popconfirm__actions", children: [
|
|
235
|
+
/* @__PURE__ */ jsx(Button, { ref: keepRef, type: "button", size: "sm", onClick: onKeepEditing, children: labels.keepEditing }),
|
|
236
|
+
/* @__PURE__ */ jsx(Button, { type: "button", size: "sm", variant: "danger", onClick: onDiscard, children: labels.discard })
|
|
237
|
+
] })
|
|
238
|
+
]
|
|
239
|
+
}
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
export {
|
|
243
|
+
DialogClose,
|
|
244
|
+
useGuardedDialog
|
|
245
|
+
};
|
|
@@ -1,15 +1,31 @@
|
|
|
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 DrawerProps extends Omit<HTMLAttributes<HTMLDialogElement>, 'title'> {
|
|
5
9
|
/** TENSOR RX-125 (UIX-04): translatable; English default. */
|
|
6
10
|
closeLabel?: string;
|
|
7
11
|
open: boolean;
|
|
8
12
|
onClose?: () => void;
|
|
13
|
+
/**
|
|
14
|
+
* The drawer holds input that is not saved (HAR-2029), e.g. react-hook-form's
|
|
15
|
+
* `formState.isDirty`. Escape, the close button, a backdrop click and a `DialogClose` then open
|
|
16
|
+
* an anchored "Discard changes?" step with focus on "Keep editing" instead of calling `onClose`;
|
|
17
|
+
* "Discard" calls it. A clean drawer closes at once. A `<form method="dialog">` submit counts
|
|
18
|
+
* as a way out too: save with your own handler instead.
|
|
19
|
+
*/
|
|
20
|
+
dirty?: boolean;
|
|
21
|
+
/** A save is in flight: Escape, the close button, a backdrop click and a `DialogClose` do nothing. */
|
|
22
|
+
pending?: boolean;
|
|
23
|
+
/** The words of the discard step. Default: `UixLabelsProvider` `discardPrompt`, then English. */
|
|
24
|
+
discardLabels?: Partial<DiscardPromptLabels>;
|
|
9
25
|
/**
|
|
10
26
|
* A click on the dimmed backdrop calls `onClose`, like Escape and the close button
|
|
11
|
-
* (TENSOR HAR-547)
|
|
12
|
-
*
|
|
27
|
+
* (TENSOR HAR-547), or asks first when the drawer is `dirty`. `false` ignores backdrop clicks
|
|
28
|
+
* altogether. Default `true`.
|
|
13
29
|
*/
|
|
14
30
|
dismissOnBackdrop?: boolean;
|
|
15
31
|
/**
|
|
@@ -21,6 +37,6 @@ interface DrawerProps extends Omit<HTMLAttributes<HTMLDialogElement>, 'title'> {
|
|
|
21
37
|
children?: ReactNode;
|
|
22
38
|
footer?: ReactNode;
|
|
23
39
|
}
|
|
24
|
-
declare function Drawer({ open, onClose, dismissOnBackdrop, side, title, children, footer, closeLabel: closeLabelProp, className, onClick, ...rest }: DrawerProps): react.JSX.Element;
|
|
40
|
+
declare function Drawer({ open, onClose, dirty, pending, discardLabels, dismissOnBackdrop, side, title, children, footer, closeLabel: closeLabelProp, className, onClick, onKeyDown, ...rest }: DrawerProps): react.JSX.Element;
|
|
25
41
|
|
|
26
42
|
export { Drawer, type DrawerProps };
|
|
@@ -1,32 +1,71 @@
|
|
|
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 { useDialog } from "../hooks/useDialog.js";
|
|
7
6
|
import { backdropDismiss } from "../hooks/backdropDismiss.js";
|
|
7
|
+
import { useGuardedDialog } from "./DiscardPrompt.js";
|
|
8
8
|
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" }) });
|
|
9
|
-
function Drawer({
|
|
9
|
+
function Drawer({
|
|
10
|
+
open,
|
|
11
|
+
onClose,
|
|
12
|
+
dirty = false,
|
|
13
|
+
pending = false,
|
|
14
|
+
discardLabels,
|
|
15
|
+
dismissOnBackdrop = true,
|
|
16
|
+
side = "end",
|
|
17
|
+
title,
|
|
18
|
+
children,
|
|
19
|
+
footer,
|
|
20
|
+
closeLabel: closeLabelProp,
|
|
21
|
+
className,
|
|
22
|
+
onClick,
|
|
23
|
+
onKeyDown,
|
|
24
|
+
...rest
|
|
25
|
+
}) {
|
|
10
26
|
const uixLabels = useUixLabels();
|
|
11
27
|
const closeLabel = closeLabelProp ?? uixLabels.drawer?.close ?? "Close drawer";
|
|
12
|
-
const
|
|
28
|
+
const closeRef = useRef(null);
|
|
29
|
+
const guard = useGuardedDialog({ open, onClose, dirty, pending, labels: discardLabels, closeRef });
|
|
13
30
|
const titleId = useId();
|
|
14
|
-
return /* @__PURE__ */ jsxs(
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
31
|
+
return /* @__PURE__ */ jsxs(
|
|
32
|
+
"dialog",
|
|
33
|
+
{
|
|
34
|
+
ref: guard.ref,
|
|
35
|
+
className: cx("uix-drawer", side !== "end" && `uix-drawer--${side}`, className),
|
|
36
|
+
"aria-labelledby": title ? titleId : void 0,
|
|
37
|
+
...rest,
|
|
38
|
+
onClick: backdropDismiss(onClose && (() => guard.requestClose()), onClick, dismissOnBackdrop),
|
|
39
|
+
onKeyDown: (event) => {
|
|
40
|
+
onKeyDown?.(event);
|
|
41
|
+
guard.onKeyDown(event);
|
|
42
|
+
},
|
|
43
|
+
children: [
|
|
44
|
+
guard.provide(
|
|
45
|
+
/* @__PURE__ */ jsxs(Fragment, { children: [
|
|
46
|
+
(title != null || onClose) && /* @__PURE__ */ jsxs("div", { className: "uix-drawer__header", children: [
|
|
47
|
+
title && /* @__PURE__ */ jsx("h2", { id: titleId, style: { fontWeight: 600, fontSize: "var(--uix-text-h3)", fontFamily: "inherit", letterSpacing: "inherit", lineHeight: "inherit" }, children: title }),
|
|
48
|
+
onClose && /* @__PURE__ */ jsx(
|
|
49
|
+
"button",
|
|
50
|
+
{
|
|
51
|
+
ref: closeRef,
|
|
52
|
+
type: "button",
|
|
53
|
+
className: "uix-drawer__close",
|
|
54
|
+
onClick: () => guard.requestClose(),
|
|
55
|
+
"aria-label": closeLabel,
|
|
56
|
+
"aria-disabled": pending || void 0,
|
|
57
|
+
children: /* @__PURE__ */ jsx(CloseIcon, {})
|
|
58
|
+
}
|
|
59
|
+
)
|
|
60
|
+
] }),
|
|
61
|
+
children != null && /* @__PURE__ */ jsx("div", { className: "uix-drawer__body", children }),
|
|
62
|
+
footer != null && /* @__PURE__ */ jsx("div", { className: "uix-drawer__footer", children: footer })
|
|
63
|
+
] })
|
|
64
|
+
),
|
|
65
|
+
guard.prompt
|
|
66
|
+
]
|
|
67
|
+
}
|
|
68
|
+
);
|
|
30
69
|
}
|
|
31
70
|
export {
|
|
32
71
|
Drawer
|
|
@@ -9,7 +9,7 @@ interface PageIntroProps extends Omit<HTMLAttributes<HTMLElement>, 'title'> {
|
|
|
9
9
|
lede?: ReactNode;
|
|
10
10
|
/** Label shown above the search well. */
|
|
11
11
|
searchLabel?: ReactNode;
|
|
12
|
-
/** The search well itself (
|
|
12
|
+
/** The search well itself: a `SearchBar size="lg"` (HAR-2030), or `.uix-searchbar` markup. */
|
|
13
13
|
search?: ReactNode;
|
|
14
14
|
/** Shortcut buttons; rendered inside the `.uix-shortcut-grid` full-width row. */
|
|
15
15
|
shortcuts?: ReactNode;
|
|
@@ -5,6 +5,12 @@ interface FieldProps {
|
|
|
5
5
|
label?: string;
|
|
6
6
|
hint?: string;
|
|
7
7
|
error?: string;
|
|
8
|
+
/**
|
|
9
|
+
* A value to check, not an error (HAR-1814): shown under the control in the warning colour,
|
|
10
|
+
* announced politely, and marks the control `data-warning`. `error` wins over it; it wins
|
|
11
|
+
* over `success` and `hint`.
|
|
12
|
+
*/
|
|
13
|
+
warning?: string;
|
|
8
14
|
success?: string;
|
|
9
15
|
required?: boolean;
|
|
10
16
|
/**
|
|
@@ -19,6 +25,6 @@ interface FieldProps {
|
|
|
19
25
|
className?: string;
|
|
20
26
|
htmlFor?: string;
|
|
21
27
|
}
|
|
22
|
-
declare function Field({ label, hint, error, success, required, help, helpLabel, children, className, htmlFor }: FieldProps): react.JSX.Element;
|
|
28
|
+
declare function Field({ label, hint, error, warning, success, required, help, helpLabel, children, className, htmlFor }: FieldProps): react.JSX.Element;
|
|
23
29
|
|
|
24
30
|
export { Field, type FieldProps };
|
package/dist/components/Field.js
CHANGED
|
@@ -3,16 +3,17 @@ import { cloneElement, isValidElement, useId } from "react";
|
|
|
3
3
|
import { cx } from "../cx.js";
|
|
4
4
|
import { InfoTip } from "./InfoTip.js";
|
|
5
5
|
import { hasHelp, helpLabelFor } from "../info-tip-model.js";
|
|
6
|
-
function Field({ label, hint, error, success, required, help, helpLabel, children, className, htmlFor }) {
|
|
6
|
+
function Field({ label, hint, error, warning, success, required, help, helpLabel, children, className, htmlFor }) {
|
|
7
7
|
const uid = useId();
|
|
8
8
|
const msgId = `${uid}-msg`;
|
|
9
|
-
const describedBy = error || success || hint ? msgId : void 0;
|
|
9
|
+
const describedBy = error || warning || success || hint ? msgId : void 0;
|
|
10
10
|
const controlId = isValidElement(children) ? children.props.id ?? `${uid}-ctl` : void 0;
|
|
11
11
|
const control = isValidElement(children) ? cloneElement(children, {
|
|
12
12
|
id: controlId,
|
|
13
13
|
"aria-describedby": [children.props["aria-describedby"], describedBy].filter(Boolean).join(" ") || void 0,
|
|
14
14
|
"aria-invalid": error ? true : children.props["aria-invalid"],
|
|
15
|
-
"aria-required": children.props["aria-required"] ?? (required || void 0)
|
|
15
|
+
"aria-required": children.props["aria-required"] ?? (required || void 0),
|
|
16
|
+
"data-warning": children.props["data-warning"] ?? (warning && !error || void 0)
|
|
16
17
|
}) : children;
|
|
17
18
|
return /* @__PURE__ */ jsxs("div", { className: cx("uix-field", className), children: [
|
|
18
19
|
label && hasHelp(help) ? /* @__PURE__ */ jsxs("div", { className: "uix-field__label-row", children: [
|
|
@@ -21,7 +22,7 @@ function Field({ label, hint, error, success, required, help, helpLabel, childre
|
|
|
21
22
|
required && /* @__PURE__ */ jsx("span", { className: "uix-field__required", "aria-hidden": "true", children: "*" })
|
|
22
23
|
] }) : label && /* @__PURE__ */ jsx("label", { className: "uix-field__label", htmlFor: htmlFor ?? controlId, "data-required": required || void 0, children: label }),
|
|
23
24
|
control,
|
|
24
|
-
/* @__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", role: "status", children: success }) : hint ? /* @__PURE__ */ jsx("span", { className: "uix-field__hint", children: hint }) : null })
|
|
25
|
+
/* @__PURE__ */ jsx("div", { className: "uix-field__msg", id: msgId, children: error ? /* @__PURE__ */ jsx("span", { className: "uix-field__error", role: "alert", children: error }) : warning ? /* @__PURE__ */ jsx("span", { className: "uix-field__warning", role: "status", children: warning }) : success ? /* @__PURE__ */ jsx("span", { className: "uix-field__success", role: "status", children: success }) : hint ? /* @__PURE__ */ jsx("span", { className: "uix-field__hint", children: hint }) : null })
|
|
25
26
|
] });
|
|
26
27
|
}
|
|
27
28
|
export {
|
|
@@ -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
|
)
|