@tensor_1/react 2.40.2 → 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/Table.d.ts +7 -1
- package/dist/components/Table.js +2 -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 +3186 -2521
- package/dist/index.d.cts +228 -40
- 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
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,67 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { HTMLAttributes, Ref } from 'react';
|
|
3
|
+
import { ButtonVariant } from './Button.js';
|
|
4
|
+
import '../link.js';
|
|
5
|
+
import './Kbd.js';
|
|
6
|
+
|
|
7
|
+
/** The words of a `SearchBar` (HAR-2030); the field's own name is its `label`. */
|
|
8
|
+
interface SearchBarLabels {
|
|
9
|
+
/** Accessible name of the clear button. */
|
|
10
|
+
clear: string;
|
|
11
|
+
/** Text of the submit button (`submitButton`). */
|
|
12
|
+
submit: string;
|
|
13
|
+
}
|
|
14
|
+
interface SearchBarProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onSubmit' | 'onChange' | 'defaultValue'> {
|
|
15
|
+
/**
|
|
16
|
+
* Accessible name of the field and of its search landmark, e.g. "Search services". It is not
|
|
17
|
+
* shown (the placeholder is not a label): the field shows `placeholder`.
|
|
18
|
+
*/
|
|
19
|
+
label: string;
|
|
20
|
+
/** The text in the field (controlled). Leave it out, with `defaultValue`, for an uncontrolled field. */
|
|
21
|
+
value?: string;
|
|
22
|
+
defaultValue?: string;
|
|
23
|
+
/** Called on every edit, and with `''` when the field is cleared. */
|
|
24
|
+
onValueChange?: (value: string) => void;
|
|
25
|
+
/** Enter in the field, or the submit button: search for this text. */
|
|
26
|
+
onSubmit?: (value: string) => void;
|
|
27
|
+
/** After the clear button or Escape emptied the field. Focus stays in the field. */
|
|
28
|
+
onClear?: () => void;
|
|
29
|
+
placeholder?: string;
|
|
30
|
+
/** `md` (default) for a header or a toolbar; `lg` for a page-level or public search: 44 px, the touch floor. */
|
|
31
|
+
size?: 'md' | 'lg';
|
|
32
|
+
/** A submit button after the field, with `labels.submit`. Enter submits either way. */
|
|
33
|
+
submitButton?: boolean;
|
|
34
|
+
/** The submit button's variant. Default `primary`. */
|
|
35
|
+
submitVariant?: ButtonVariant;
|
|
36
|
+
/**
|
|
37
|
+
* A `search` landmark named by `label` (default). Pass `false` for a field that filters a list
|
|
38
|
+
* or a table, where one more landmark per toolbar would crowd the landmark list. Never wrap it
|
|
39
|
+
* in a `<search>` or `role="search"` of your own: that nests two landmarks.
|
|
40
|
+
*/
|
|
41
|
+
landmark?: boolean;
|
|
42
|
+
/** Ids of elements that describe the field (a hint, a result count), as its `aria-describedby`. */
|
|
43
|
+
inputDescribedBy?: string;
|
|
44
|
+
/** The words. Default: `UixLabelsProvider` `searchBar`, then English. */
|
|
45
|
+
labels?: Partial<SearchBarLabels>;
|
|
46
|
+
/** The field's `name`. */
|
|
47
|
+
name?: string;
|
|
48
|
+
/** The field's `id`, e.g. for a "/" shortcut that focuses it. Default: generated. */
|
|
49
|
+
inputId?: string;
|
|
50
|
+
inputRef?: Ref<HTMLInputElement>;
|
|
51
|
+
autoFocus?: boolean;
|
|
52
|
+
disabled?: boolean;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* One search field look and behaviour (HAR-2030; TENSOR audit UXC-02) over the `.uix-searchbar`
|
|
56
|
+
* contract: a leading magnifier, a clear button while there is text, Enter to search, Escape to
|
|
57
|
+
* clear (an empty field lets Escape through, so it still closes the dialog it sits in). It is a
|
|
58
|
+
* `search` landmark named by `label`.
|
|
59
|
+
*
|
|
60
|
+
* It is not a `<form>`, so it can sit inside one: Enter calls `onSubmit` and never submits a
|
|
61
|
+
* surrounding form. For suggestions while typing, use `SearchSuggest`. Inside an overlay that
|
|
62
|
+
* takes Escape itself before the field (a ConfirmPopover, the table's view menu), Escape closes
|
|
63
|
+
* the overlay rather than clearing the text.
|
|
64
|
+
*/
|
|
65
|
+
declare function SearchBar({ label, value: valueProp, defaultValue, onValueChange, onSubmit, onClear, placeholder, size, submitButton, submitVariant, landmark, inputDescribedBy, labels: labelsProp, name, inputId, inputRef, autoFocus, disabled, className, ...props }: SearchBarProps): react.JSX.Element;
|
|
66
|
+
|
|
67
|
+
export { SearchBar, type SearchBarLabels, type SearchBarProps };
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useCallback, useId, useRef, useState } from "react";
|
|
4
|
+
import { cx } from "../cx.js";
|
|
5
|
+
import { useUixLabels } from "../labels-context.js";
|
|
6
|
+
import { Button } from "./Button.js";
|
|
7
|
+
const DEFAULT_SEARCH_BAR_LABELS = { clear: "Clear search", submit: "Search" };
|
|
8
|
+
const SearchIcon = () => /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", focusable: "false", children: [
|
|
9
|
+
/* @__PURE__ */ jsx("circle", { cx: "11", cy: "11", r: "8" }),
|
|
10
|
+
/* @__PURE__ */ jsx("path", { d: "m21 21-4.3-4.3" })
|
|
11
|
+
] });
|
|
12
|
+
const ClearIcon = () => /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", focusable: "false", children: /* @__PURE__ */ jsx("path", { d: "M18 6 6 18M6 6l12 12" }) });
|
|
13
|
+
function SearchBar({
|
|
14
|
+
label,
|
|
15
|
+
value: valueProp,
|
|
16
|
+
defaultValue = "",
|
|
17
|
+
onValueChange,
|
|
18
|
+
onSubmit,
|
|
19
|
+
onClear,
|
|
20
|
+
placeholder,
|
|
21
|
+
size = "md",
|
|
22
|
+
submitButton = false,
|
|
23
|
+
submitVariant = "primary",
|
|
24
|
+
landmark = true,
|
|
25
|
+
inputDescribedBy,
|
|
26
|
+
labels: labelsProp,
|
|
27
|
+
name,
|
|
28
|
+
inputId,
|
|
29
|
+
inputRef,
|
|
30
|
+
autoFocus,
|
|
31
|
+
disabled,
|
|
32
|
+
className,
|
|
33
|
+
...props
|
|
34
|
+
}) {
|
|
35
|
+
const uixLabels = useUixLabels();
|
|
36
|
+
const labels = { ...DEFAULT_SEARCH_BAR_LABELS, ...uixLabels.searchBar, ...labelsProp };
|
|
37
|
+
const id = useId();
|
|
38
|
+
const fieldId = inputId ?? `${id}-field`;
|
|
39
|
+
const labelId = `${id}-label`;
|
|
40
|
+
const field = useRef(null);
|
|
41
|
+
const [own, setOwn] = useState(defaultValue);
|
|
42
|
+
const value = valueProp ?? own;
|
|
43
|
+
const change = (next) => {
|
|
44
|
+
setOwn(next);
|
|
45
|
+
onValueChange?.(next);
|
|
46
|
+
};
|
|
47
|
+
const clear = () => {
|
|
48
|
+
change("");
|
|
49
|
+
field.current?.focus();
|
|
50
|
+
onClear?.();
|
|
51
|
+
};
|
|
52
|
+
const onKeyDown = (event) => {
|
|
53
|
+
if (event.nativeEvent.isComposing || event.keyCode === 229) return;
|
|
54
|
+
if (event.key === "Enter") {
|
|
55
|
+
event.preventDefault();
|
|
56
|
+
onSubmit?.(value);
|
|
57
|
+
} else if (event.key === "Escape" && value !== "") {
|
|
58
|
+
event.preventDefault();
|
|
59
|
+
event.stopPropagation();
|
|
60
|
+
clear();
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
const setField = useCallback((node) => {
|
|
64
|
+
field.current = node;
|
|
65
|
+
if (typeof inputRef === "function") inputRef(node);
|
|
66
|
+
else if (inputRef) inputRef.current = node;
|
|
67
|
+
}, [inputRef]);
|
|
68
|
+
return /* @__PURE__ */ jsxs(
|
|
69
|
+
"div",
|
|
70
|
+
{
|
|
71
|
+
role: landmark ? "search" : void 0,
|
|
72
|
+
"aria-labelledby": landmark ? labelId : void 0,
|
|
73
|
+
className: cx("uix-searchbar", size === "md" && "uix-searchbar--md", className),
|
|
74
|
+
...props,
|
|
75
|
+
children: [
|
|
76
|
+
/* @__PURE__ */ jsxs("div", { className: "uix-searchbar__wrap", children: [
|
|
77
|
+
/* @__PURE__ */ jsx("label", { id: labelId, htmlFor: fieldId, className: "uix-visually-hidden", children: label }),
|
|
78
|
+
/* @__PURE__ */ jsx(SearchIcon, {}),
|
|
79
|
+
/* @__PURE__ */ jsx(
|
|
80
|
+
"input",
|
|
81
|
+
{
|
|
82
|
+
ref: setField,
|
|
83
|
+
id: fieldId,
|
|
84
|
+
type: "search",
|
|
85
|
+
name,
|
|
86
|
+
className: "uix-input",
|
|
87
|
+
value,
|
|
88
|
+
placeholder,
|
|
89
|
+
autoComplete: "off",
|
|
90
|
+
enterKeyHint: "search",
|
|
91
|
+
"aria-describedby": inputDescribedBy,
|
|
92
|
+
autoFocus,
|
|
93
|
+
disabled,
|
|
94
|
+
onChange: (event) => change(event.currentTarget.value),
|
|
95
|
+
onKeyDown
|
|
96
|
+
}
|
|
97
|
+
),
|
|
98
|
+
value !== "" && !disabled && /* @__PURE__ */ jsx(
|
|
99
|
+
"button",
|
|
100
|
+
{
|
|
101
|
+
type: "button",
|
|
102
|
+
className: "uix-searchbar__clear",
|
|
103
|
+
"aria-label": labels.clear,
|
|
104
|
+
onMouseDown: (event) => event.preventDefault(),
|
|
105
|
+
onClick: clear,
|
|
106
|
+
children: /* @__PURE__ */ jsx(ClearIcon, {})
|
|
107
|
+
}
|
|
108
|
+
)
|
|
109
|
+
] }),
|
|
110
|
+
submitButton && /* @__PURE__ */ jsx(Button, { type: "button", variant: submitVariant, size: size === "lg" ? "lg" : "md", disabled, onClick: () => onSubmit?.(value), children: labels.submit })
|
|
111
|
+
]
|
|
112
|
+
}
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
export {
|
|
116
|
+
SearchBar
|
|
117
|
+
};
|
|
@@ -3,7 +3,7 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { Children, Fragment as Fragment2, forwardRef, isValidElement, useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
4
4
|
import { cx } from "../cx.js";
|
|
5
5
|
import { useUixLabels } from "../labels-context.js";
|
|
6
|
-
import { useAnchoredPosition } from "../hooks/useAnchoredPosition.js";
|
|
6
|
+
import { useAnchoredPosition, zoomOf } from "../hooks/useAnchoredPosition.js";
|
|
7
7
|
import { Drawer } from "./Drawer.js";
|
|
8
8
|
import {
|
|
9
9
|
buildSelectModel,
|
|
@@ -537,9 +537,10 @@ const Select = forwardRef(function Select2(props, forwardedRef) {
|
|
|
537
537
|
}
|
|
538
538
|
};
|
|
539
539
|
if (showing) {
|
|
540
|
+
const zoom = zoomOf(trigger);
|
|
540
541
|
const r = trigger.getBoundingClientRect();
|
|
541
|
-
const room = Math.max(r.top, window.innerHeight - r.bottom) - OFFSET - EDGE;
|
|
542
|
-
popup.style.minWidth = `${Math.round(r.width)}px`;
|
|
542
|
+
const room = Math.max(r.top, window.innerHeight - r.bottom) / zoom - OFFSET - EDGE;
|
|
543
|
+
popup.style.minWidth = `${Math.round(r.width / zoom)}px`;
|
|
543
544
|
popup.style.maxHeight = `${Math.max(96, Math.min(MAX_HEIGHT, Math.floor(room)))}px`;
|
|
544
545
|
try {
|
|
545
546
|
if (!isOpen()) popup.showPopover();
|
|
@@ -581,7 +582,7 @@ const Select = forwardRef(function Select2(props, forwardedRef) {
|
|
|
581
582
|
doc.addEventListener("pointerdown", onDown, true);
|
|
582
583
|
return () => doc.removeEventListener("pointerdown", onDown, true);
|
|
583
584
|
}, [showing]);
|
|
584
|
-
|
|
585
|
+
useIsomorphicLayoutEffect(() => {
|
|
585
586
|
if (!open) return;
|
|
586
587
|
if (active >= entries.length || active < 0 && entries.length) setActive(entries.length ? selectedIndexIn(entries) >= 0 ? selectedIndexIn(entries) : firstEnabled(entries) : -1);
|
|
587
588
|
}, [open, entries.length, query]);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { KeyShortcut } from './Kbd.js';
|
|
3
|
+
|
|
4
|
+
interface ShortcutHintProps {
|
|
5
|
+
keys: KeyShortcut;
|
|
6
|
+
/** Text before the keys, e.g. an icon button's name. */
|
|
7
|
+
label?: string;
|
|
8
|
+
/** Set the button's `aria-keyshortcuts`. `false` when the button already has its own (a prop, or a Tooltip's). */
|
|
9
|
+
ownsAria: boolean;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The hover / keyboard-focus hint of a `Button` with `shortcut` (HAR-2031). In the button it
|
|
13
|
+
* leaves only an empty, hidden marker, so the button's text, name, layout and selectors are
|
|
14
|
+
* unchanged (no wrapper, no sibling). The bubble itself is portalled after mount into the
|
|
15
|
+
* nearest `<dialog>` (outside one a modal dialog makes everything inert) or `<body>`, as a
|
|
16
|
+
* top-layer popover, `aria-hidden`: assistive technology gets the key from the button's
|
|
17
|
+
* `aria-keyshortcuts`, which this sets for the viewer's platform (Mod is Meta on Apple, Control
|
|
18
|
+
* elsewhere) unless the button already has its own.
|
|
19
|
+
*
|
|
20
|
+
* It opens on hover and on keyboard focus (`:focus-visible`), so focus a menu or dialog sends back
|
|
21
|
+
* after a mouse choice does not open it. It closes on leave, blur, Escape and when the button is
|
|
22
|
+
* disabled, and stays while the pointer is on it (WCAG 1.4.13). Inside a `Tooltip` it steps aside:
|
|
23
|
+
* the Tooltip shows the keys in its own bubble.
|
|
24
|
+
*
|
|
25
|
+
* It is its own client component so that `Button`, which has no hooks, still renders on a server.
|
|
26
|
+
*/
|
|
27
|
+
declare function ShortcutHint({ keys, label, ownsAria }: ShortcutHintProps): react.JSX.Element;
|
|
28
|
+
|
|
29
|
+
export { ShortcutHint, type ShortcutHintProps };
|