@tensor_1/react 2.26.2 → 2.27.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 +51 -0
- package/dist/calendar-model.d.ts +4 -2
- package/dist/components/AppShell.d.ts +1 -1
- package/dist/components/AppShell.js +6 -2
- package/dist/components/Button.d.ts +28 -4
- package/dist/components/Button.js +19 -9
- package/dist/components/Card.d.ts +7 -1
- package/dist/components/Card.js +3 -3
- package/dist/components/Chip.d.ts +43 -0
- package/dist/components/Chip.js +73 -0
- package/dist/components/CollapsibleSection.d.ts +19 -1
- package/dist/components/CollapsibleSection.js +5 -2
- package/dist/components/CollapsibleSectionState.d.ts +11 -0
- package/dist/components/CollapsibleSectionState.js +67 -0
- package/dist/components/CommandPalette.d.ts +1 -1
- package/dist/components/CommandPalette.js +4 -1
- package/dist/components/DateRangePicker.js +3 -1
- package/dist/components/Dialogs.d.ts +54 -3
- package/dist/components/Dialogs.js +167 -6
- package/dist/components/Drawer.d.ts +6 -1
- package/dist/components/Drawer.js +5 -2
- package/dist/components/EmojiPicker.d.ts +3 -0
- package/dist/components/EmojiPicker.js +109 -54
- package/dist/components/Kbd.d.ts +27 -0
- package/dist/components/Kbd.js +45 -0
- package/dist/components/Menu.d.ts +51 -0
- package/dist/components/Menu.js +177 -0
- package/dist/components/Modal.d.ts +1 -1
- package/dist/components/Modal.js +4 -1
- package/dist/components/Pagination.d.ts +36 -5
- package/dist/components/Pagination.js +59 -37
- package/dist/components/Peek.d.ts +1 -1
- package/dist/components/Peek.js +6 -1
- package/dist/components/Popover.d.ts +15 -5
- package/dist/components/Popover.js +66 -6
- package/dist/components/SchedulingCalendar.d.ts +47 -2
- package/dist/components/SchedulingCalendar.js +69 -22
- package/dist/components/SearchSuggest.d.ts +1 -1
- package/dist/components/SearchSuggest.js +4 -1
- package/dist/components/Sidebar.d.ts +7 -2
- package/dist/components/Sidebar.js +15 -4
- package/dist/components/Stat.d.ts +16 -0
- package/dist/components/Stat.js +12 -5
- package/dist/components/Steps.d.ts +48 -0
- package/dist/components/Steps.js +42 -0
- package/dist/components/Toast.d.ts +2 -2
- package/dist/components/Toast.js +7 -2
- package/dist/components/Tooltip.d.ts +10 -4
- package/dist/components/Tooltip.js +2 -2
- package/dist/{emoji-image-WQLh8ZQP.d.cts → emoji-image-6mPZ15Zy.d.cts} +4 -1
- package/dist/emoji.cjs +326 -76
- package/dist/emoji.d.cts +3 -3
- package/dist/hooks/useAnchoredPosition.d.ts +13 -5
- package/dist/hooks/useAnchoredPosition.js +146 -15
- package/dist/index.cjs +2278 -1372
- package/dist/index.d.cts +622 -176
- package/dist/index.d.ts +10 -4
- package/dist/index.js +24 -2
- package/dist/labels-context.d.ts +72 -0
- package/dist/labels-context.js +23 -0
- package/dist/link.d.ts +20 -0
- package/dist/link.js +9 -0
- package/dist/{overlay-position-CEGSz6Z2.d.cts → overlay-position-Cjv5E7mn.d.cts} +7 -0
- package/dist/overlay-position.d.ts +7 -0
- package/dist/overlay-position.js +7 -2
- package/dist/rich-text.cjs +324 -74
- package/dist/rich-text.d.cts +3 -3
- package/package.json +1 -1
|
@@ -1,10 +1,55 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useEffect, useId, useState } from "react";
|
|
3
|
+
import { useEffect, useId, useRef, useState } from "react";
|
|
4
4
|
import { Button } from "./Button.js";
|
|
5
5
|
import { Input } from "./Input.js";
|
|
6
6
|
import { Modal } from "./Modal.js";
|
|
7
|
-
|
|
7
|
+
import { Popover } from "./Popover.js";
|
|
8
|
+
import { fillLabel } from "../fill-label.js";
|
|
9
|
+
import { resolveAnchor } from "../hooks/useAnchoredPosition.js";
|
|
10
|
+
import { useUixLabels } from "../labels-context.js";
|
|
11
|
+
function ConfirmDialog({
|
|
12
|
+
open,
|
|
13
|
+
title,
|
|
14
|
+
description,
|
|
15
|
+
confirmLabel,
|
|
16
|
+
cancelLabel,
|
|
17
|
+
closeLabel,
|
|
18
|
+
onConfirm,
|
|
19
|
+
onCancel,
|
|
20
|
+
pending,
|
|
21
|
+
destructive,
|
|
22
|
+
children,
|
|
23
|
+
error,
|
|
24
|
+
initialFocus,
|
|
25
|
+
tertiaryLabel,
|
|
26
|
+
onTertiary,
|
|
27
|
+
typeToConfirm,
|
|
28
|
+
typeToConfirmLabel: typeToConfirmLabelProp,
|
|
29
|
+
compensation,
|
|
30
|
+
compensationLabel: compensationLabelProp
|
|
31
|
+
}) {
|
|
32
|
+
const uixLabels = useUixLabels();
|
|
33
|
+
const typeToConfirmLabel = typeToConfirmLabelProp ?? uixLabels.confirmDialog?.typeToConfirm ?? "Type {value} to confirm";
|
|
34
|
+
const compensationLabel = compensationLabelProp ?? uixLabels.confirmDialog?.compensation ?? "How to undo this";
|
|
35
|
+
const id = useId();
|
|
36
|
+
const bodyId = `${id}-body`;
|
|
37
|
+
const typedId = `${id}-typed`;
|
|
38
|
+
const cancelRef = useRef(null);
|
|
39
|
+
const confirmRef = useRef(null);
|
|
40
|
+
const typedRef = useRef(null);
|
|
41
|
+
const [typed, setTyped] = useState("");
|
|
42
|
+
const typedMatches = typeToConfirm == null || typed === typeToConfirm;
|
|
43
|
+
const hasBody = description != null || children != null || compensation != null;
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (open) setTyped("");
|
|
46
|
+
}, [open, typeToConfirm]);
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (!open) return;
|
|
49
|
+
const focus = initialFocus ?? (destructive ? "cancel" : void 0);
|
|
50
|
+
const target = typeToConfirm != null ? typedRef.current : focus === "cancel" ? cancelRef.current : focus === "confirm" ? confirmRef.current : null;
|
|
51
|
+
target?.focus();
|
|
52
|
+
}, [open, initialFocus, destructive, typeToConfirm]);
|
|
8
53
|
return /* @__PURE__ */ jsx(
|
|
9
54
|
Modal,
|
|
10
55
|
{
|
|
@@ -13,11 +58,126 @@ function ConfirmDialog({ open, title, description, confirmLabel, cancelLabel, cl
|
|
|
13
58
|
title,
|
|
14
59
|
closeLabel,
|
|
15
60
|
role: "alertdialog",
|
|
61
|
+
"aria-describedby": hasBody ? bodyId : void 0,
|
|
16
62
|
footer: /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
17
|
-
/* @__PURE__ */ jsx(Button, { type: "button", onClick:
|
|
18
|
-
/* @__PURE__ */ jsx(Button, {
|
|
63
|
+
tertiaryLabel != null && onTertiary && /* @__PURE__ */ jsx(Button, { type: "button", className: "uix-confirm__tertiary", onClick: () => void onTertiary(), disabled: pending, children: tertiaryLabel }),
|
|
64
|
+
/* @__PURE__ */ jsx(Button, { ref: cancelRef, type: "button", onClick: onCancel, disabled: pending, children: cancelLabel }),
|
|
65
|
+
/* @__PURE__ */ jsx(Button, { ref: confirmRef, type: "button", variant: destructive ? "danger" : "primary", onClick: () => {
|
|
66
|
+
if (typedMatches) void onConfirm();
|
|
67
|
+
}, loading: pending, disabled: !typedMatches, children: confirmLabel })
|
|
19
68
|
] }),
|
|
20
|
-
children:
|
|
69
|
+
children: /* @__PURE__ */ jsxs("div", { className: "uix-confirm", children: [
|
|
70
|
+
hasBody && /* @__PURE__ */ jsxs("div", { id: bodyId, className: "uix-confirm__body", children: [
|
|
71
|
+
description != null && /* @__PURE__ */ jsx("div", { children: description }),
|
|
72
|
+
children,
|
|
73
|
+
compensation != null && /* @__PURE__ */ jsxs("div", { className: "uix-confirm__compensation", children: [
|
|
74
|
+
/* @__PURE__ */ jsx("strong", { className: "uix-confirm__compensation-title", children: compensationLabel }),
|
|
75
|
+
/* @__PURE__ */ jsx("div", { children: compensation })
|
|
76
|
+
] })
|
|
77
|
+
] }),
|
|
78
|
+
typeToConfirm != null && /* @__PURE__ */ jsxs("div", { className: "uix-field", children: [
|
|
79
|
+
/* @__PURE__ */ jsx("label", { className: "uix-field__label", htmlFor: typedId, children: fillLabel(typeToConfirmLabel, { value: typeToConfirm }) }),
|
|
80
|
+
/* @__PURE__ */ jsx(Input, { ref: typedRef, id: typedId, value: typed, autoComplete: "off", spellCheck: false, onChange: (event) => setTyped(event.currentTarget.value) })
|
|
81
|
+
] }),
|
|
82
|
+
error != null && /* @__PURE__ */ jsx("div", { className: "uix-confirm__error", role: "alert", children: error })
|
|
83
|
+
] })
|
|
84
|
+
}
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
function Popconfirm({
|
|
88
|
+
open,
|
|
89
|
+
anchor,
|
|
90
|
+
title,
|
|
91
|
+
description,
|
|
92
|
+
confirmLabel,
|
|
93
|
+
cancelLabel,
|
|
94
|
+
onConfirm,
|
|
95
|
+
onCancel,
|
|
96
|
+
pending,
|
|
97
|
+
destructive,
|
|
98
|
+
compensation,
|
|
99
|
+
compensationLabel: compensationLabelProp,
|
|
100
|
+
placement = "bottom-start"
|
|
101
|
+
}) {
|
|
102
|
+
const uixLabels = useUixLabels();
|
|
103
|
+
const compensationLabel = compensationLabelProp ?? uixLabels.confirmDialog?.compensation ?? "How to undo this";
|
|
104
|
+
const id = useId();
|
|
105
|
+
const panelId = `${id}-popconfirm`;
|
|
106
|
+
const cancelRef = useRef(null);
|
|
107
|
+
const onCancelRef = useRef(onCancel);
|
|
108
|
+
onCancelRef.current = onCancel;
|
|
109
|
+
const hasBody = description != null || compensation != null;
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
const panel = document.getElementById(panelId);
|
|
112
|
+
if (!panel) return;
|
|
113
|
+
const shown = () => {
|
|
114
|
+
try {
|
|
115
|
+
return panel.matches(":popover-open");
|
|
116
|
+
} catch {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
if (!open) {
|
|
121
|
+
if (shown()) {
|
|
122
|
+
const focusInside = panel.contains(document.activeElement);
|
|
123
|
+
try {
|
|
124
|
+
panel.hidePopover();
|
|
125
|
+
} catch {
|
|
126
|
+
}
|
|
127
|
+
if (focusInside) resolveAnchor(anchor)?.focus();
|
|
128
|
+
}
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (!shown()) {
|
|
132
|
+
try {
|
|
133
|
+
panel.showPopover();
|
|
134
|
+
} catch {
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
cancelRef.current?.focus();
|
|
138
|
+
const onKey = (event) => {
|
|
139
|
+
if (event.key !== "Escape") return;
|
|
140
|
+
event.preventDefault();
|
|
141
|
+
event.stopPropagation();
|
|
142
|
+
onCancelRef.current();
|
|
143
|
+
};
|
|
144
|
+
const onPointer = (event) => {
|
|
145
|
+
const target = event.target;
|
|
146
|
+
if (panel.contains(target) || resolveAnchor(anchor)?.contains(target)) return;
|
|
147
|
+
onCancelRef.current();
|
|
148
|
+
};
|
|
149
|
+
document.addEventListener("keydown", onKey, true);
|
|
150
|
+
document.addEventListener("pointerdown", onPointer, true);
|
|
151
|
+
return () => {
|
|
152
|
+
document.removeEventListener("keydown", onKey, true);
|
|
153
|
+
document.removeEventListener("pointerdown", onPointer, true);
|
|
154
|
+
};
|
|
155
|
+
}, [open, anchor, panelId]);
|
|
156
|
+
return /* @__PURE__ */ jsxs(
|
|
157
|
+
Popover,
|
|
158
|
+
{
|
|
159
|
+
id: panelId,
|
|
160
|
+
popover: "manual",
|
|
161
|
+
anchor,
|
|
162
|
+
placement,
|
|
163
|
+
role: "dialog",
|
|
164
|
+
"aria-labelledby": `${id}-title`,
|
|
165
|
+
"aria-describedby": hasBody ? `${id}-body` : void 0,
|
|
166
|
+
className: "uix-popconfirm",
|
|
167
|
+
children: [
|
|
168
|
+
/* @__PURE__ */ jsx("div", { id: `${id}-title`, className: "uix-popconfirm__title", children: title }),
|
|
169
|
+
hasBody && /* @__PURE__ */ jsxs("div", { id: `${id}-body`, className: "uix-popconfirm__body", children: [
|
|
170
|
+
description != null && /* @__PURE__ */ jsx("div", { children: description }),
|
|
171
|
+
compensation != null && /* @__PURE__ */ jsxs("div", { className: "uix-confirm__compensation", children: [
|
|
172
|
+
/* @__PURE__ */ jsx("strong", { className: "uix-confirm__compensation-title", children: compensationLabel }),
|
|
173
|
+
/* @__PURE__ */ jsx("div", { children: compensation })
|
|
174
|
+
] })
|
|
175
|
+
] }),
|
|
176
|
+
/* @__PURE__ */ jsxs("div", { className: "uix-popconfirm__actions", children: [
|
|
177
|
+
/* @__PURE__ */ jsx(Button, { ref: cancelRef, type: "button", size: "sm", onClick: onCancel, disabled: pending, children: cancelLabel }),
|
|
178
|
+
/* @__PURE__ */ jsx(Button, { type: "button", size: "sm", variant: destructive ? "danger" : "primary", onClick: () => void onConfirm(), loading: pending, children: confirmLabel })
|
|
179
|
+
] })
|
|
180
|
+
]
|
|
21
181
|
}
|
|
22
182
|
);
|
|
23
183
|
}
|
|
@@ -40,7 +200,7 @@ function PromptDialog({ open, title, description, inputLabel, defaultValue = "",
|
|
|
40
200
|
};
|
|
41
201
|
return /* @__PURE__ */ jsx(Modal, { open, onClose: onCancel, title, closeLabel, children: /* @__PURE__ */ jsxs("form", { className: "uix-prompt", onSubmit: handleSubmit, children: [
|
|
42
202
|
description != null && /* @__PURE__ */ jsx("div", { children: description }),
|
|
43
|
-
/* @__PURE__ */ jsx("label", { className: "uix-
|
|
203
|
+
/* @__PURE__ */ jsx("label", { className: "uix-field__label", htmlFor: inputId, children: inputLabel }),
|
|
44
204
|
/* @__PURE__ */ jsx(Input, { id: inputId, value, placeholder, "aria-describedby": error ? errorId : void 0, invalid: error != null, onChange: (event) => setValue(event.currentTarget.value), autoFocus: true }),
|
|
45
205
|
error != null && /* @__PURE__ */ jsx("div", { id: errorId, className: "uix-field__error", role: "alert", children: error }),
|
|
46
206
|
/* @__PURE__ */ jsxs("div", { className: "uix-dialog__footer uix-prompt__actions", children: [
|
|
@@ -51,5 +211,6 @@ function PromptDialog({ open, title, description, inputLabel, defaultValue = "",
|
|
|
51
211
|
}
|
|
52
212
|
export {
|
|
53
213
|
ConfirmDialog,
|
|
214
|
+
Popconfirm,
|
|
54
215
|
PromptDialog
|
|
55
216
|
};
|
|
@@ -12,10 +12,15 @@ interface DrawerProps extends Omit<HTMLAttributes<HTMLDialogElement>, 'title'> {
|
|
|
12
12
|
* can't discard it; Escape and the close button still close. Default `true`.
|
|
13
13
|
*/
|
|
14
14
|
dismissOnBackdrop?: boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Edge the drawer slides from (HAR-1351). `end` (default) is the side sheet; `start` is
|
|
17
|
+
* the phone navigation drawer; `bottom` is a bottom sheet capped at 85% of the viewport.
|
|
18
|
+
*/
|
|
19
|
+
side?: 'end' | 'start' | 'bottom';
|
|
15
20
|
title?: ReactNode;
|
|
16
21
|
children?: ReactNode;
|
|
17
22
|
footer?: ReactNode;
|
|
18
23
|
}
|
|
19
|
-
declare function Drawer({ open, onClose, dismissOnBackdrop, title, children, footer, closeLabel, className, onClick, ...rest }: DrawerProps): react.JSX.Element;
|
|
24
|
+
declare function Drawer({ open, onClose, dismissOnBackdrop, side, title, children, footer, closeLabel: closeLabelProp, className, onClick, ...rest }: DrawerProps): react.JSX.Element;
|
|
20
25
|
|
|
21
26
|
export { Drawer, type DrawerProps };
|
|
@@ -2,13 +2,16 @@
|
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useId } from "react";
|
|
4
4
|
import { cx } from "../cx.js";
|
|
5
|
+
import { useUixLabels } from "../labels-context.js";
|
|
5
6
|
import { useDialog } from "../hooks/useDialog.js";
|
|
6
7
|
import { backdropDismiss } from "../hooks/backdropDismiss.js";
|
|
7
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" }) });
|
|
8
|
-
function Drawer({ open, onClose, dismissOnBackdrop = true, title, children, footer, closeLabel
|
|
9
|
+
function Drawer({ open, onClose, dismissOnBackdrop = true, side = "end", title, children, footer, closeLabel: closeLabelProp, className, onClick, ...rest }) {
|
|
10
|
+
const uixLabels = useUixLabels();
|
|
11
|
+
const closeLabel = closeLabelProp ?? uixLabels.drawer?.close ?? "Close drawer";
|
|
9
12
|
const ref = useDialog(open, onClose);
|
|
10
13
|
const titleId = useId();
|
|
11
|
-
return /* @__PURE__ */ jsxs("dialog", { ref, className: cx("uix-drawer", className), "aria-labelledby": title ? titleId : void 0, ...rest, onClick: backdropDismiss(onClose, onClick, dismissOnBackdrop), children: [
|
|
14
|
+
return /* @__PURE__ */ jsxs("dialog", { ref, className: cx("uix-drawer", side !== "end" && `uix-drawer--${side}`, className), "aria-labelledby": title ? titleId : void 0, ...rest, onClick: backdropDismiss(onClose, onClick, dismissOnBackdrop), children: [
|
|
12
15
|
(title != null || onClose) && /* @__PURE__ */ jsxs("div", { className: "uix-drawer__header", children: [
|
|
13
16
|
title && /* @__PURE__ */ jsx("h2", { id: titleId, style: { fontWeight: 600, fontSize: "var(--uix-text-h3)", fontFamily: "inherit", letterSpacing: "inherit", lineHeight: "inherit" }, children: title }),
|
|
14
17
|
onClose && /* @__PURE__ */ jsx(
|
|
@@ -54,6 +54,9 @@ declare const EMOJI_GRID_COLUMNS = 8;
|
|
|
54
54
|
* Emoji picker in a native popover: search, quick picks, recently used and the
|
|
55
55
|
* Unicode categories, with names in English or German. The emoji dataset loads on
|
|
56
56
|
* first open. The grid is one tab stop; arrow keys, Home and End move within it.
|
|
57
|
+
* The picker is one size in every state (loading, searching, failed), so it keeps the
|
|
58
|
+
* side it opened on; it stays attached to its trigger while the page scrolls and
|
|
59
|
+
* closes once the trigger is scrolled out of view.
|
|
57
60
|
*/
|
|
58
61
|
declare function EmojiPicker({ onSelect, trigger, labels: labelOverrides, locale, quickPicks, loadData, recentStorageKey, placement, onOpenChange, className, emojiImageBaseUrl, }: EmojiPickerProps): react.JSX.Element;
|
|
59
62
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { cloneElement, isValidElement, useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
|
|
3
|
+
import { cloneElement, isValidElement, useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
4
4
|
import { cx } from "../cx.js";
|
|
5
5
|
import { Popover } from "./Popover.js";
|
|
6
6
|
import {
|
|
@@ -23,6 +23,14 @@ const DEFAULT_EMOJI_PICKER_LABELS = {
|
|
|
23
23
|
loadFailed: "Emoji could not be loaded."
|
|
24
24
|
};
|
|
25
25
|
const EMOJI_GRID_COLUMNS = 8;
|
|
26
|
+
const useIsomorphicLayoutEffect = typeof document !== "undefined" ? useLayoutEffect : useEffect;
|
|
27
|
+
function revealInGrid(grid, cell) {
|
|
28
|
+
const g = grid.getBoundingClientRect();
|
|
29
|
+
const c = cell.getBoundingClientRect();
|
|
30
|
+
const top = g.top + (parseFloat(getComputedStyle(grid).scrollPaddingTop) || 0);
|
|
31
|
+
if (c.top < top) grid.scrollTop -= top - c.top;
|
|
32
|
+
else if (c.bottom > g.bottom) grid.scrollTop += c.bottom - g.bottom;
|
|
33
|
+
}
|
|
26
34
|
function EmojiPicker({
|
|
27
35
|
onSelect,
|
|
28
36
|
trigger,
|
|
@@ -44,6 +52,8 @@ function EmojiPicker({
|
|
|
44
52
|
const searchRef = useRef(null);
|
|
45
53
|
const gridRef = useRef(null);
|
|
46
54
|
const pointerWasOpen = useRef(null);
|
|
55
|
+
const closedByAnchor = useRef(false);
|
|
56
|
+
const pendingJump = useRef(null);
|
|
47
57
|
const [open, setOpen] = useState(false);
|
|
48
58
|
const [data, setData] = useState(null);
|
|
49
59
|
const [failed, setFailed] = useState(false);
|
|
@@ -67,10 +77,14 @@ function EmojiPicker({
|
|
|
67
77
|
setOpen(nowOpen);
|
|
68
78
|
onOpenChange?.(nowOpen);
|
|
69
79
|
if (nowOpen) {
|
|
80
|
+
closedByAnchor.current = false;
|
|
81
|
+
pendingJump.current = null;
|
|
70
82
|
setRecent(readRecentEmoji(recentStorageKey));
|
|
71
83
|
setQuery("");
|
|
72
84
|
setActive(0);
|
|
73
|
-
requestAnimationFrame(() => searchRef.current?.focus());
|
|
85
|
+
requestAnimationFrame(() => searchRef.current?.focus({ preventScroll: true }));
|
|
86
|
+
} else if (closedByAnchor.current) {
|
|
87
|
+
closedByAnchor.current = false;
|
|
74
88
|
} else if (el.contains(document.activeElement) || document.activeElement === document.body) {
|
|
75
89
|
focusTrigger();
|
|
76
90
|
}
|
|
@@ -102,10 +116,7 @@ function EmojiPicker({
|
|
|
102
116
|
const el = popoverEl();
|
|
103
117
|
if (el && isOpen(el)) el.hidePopover();
|
|
104
118
|
};
|
|
105
|
-
const
|
|
106
|
-
if (query.trim()) {
|
|
107
|
-
return data ? [{ key: "results", title: labels.search, emojis: searchEmoji(data, query).map((e) => e.emoji) }] : [];
|
|
108
|
-
}
|
|
119
|
+
const browseSections = useMemo(() => {
|
|
109
120
|
const list = [];
|
|
110
121
|
if (quickPicks?.length) list.push({ key: "quick", title: labels.quickPicks, emojis: [...new Set(quickPicks)] });
|
|
111
122
|
if (recent.length) list.push({ key: "recent", title: labels.recent, emojis: recent });
|
|
@@ -115,7 +126,11 @@ function EmojiPicker({
|
|
|
115
126
|
});
|
|
116
127
|
}
|
|
117
128
|
return list;
|
|
118
|
-
}, [
|
|
129
|
+
}, [data, quickPicks, recent, labels.quickPicks, labels.recent]);
|
|
130
|
+
const sections = useMemo(() => {
|
|
131
|
+
if (!query.trim()) return browseSections;
|
|
132
|
+
return data ? [{ key: "results", title: labels.search, emojis: searchEmoji(data, query).map((e) => e.emoji) }] : [];
|
|
133
|
+
}, [query, data, browseSections, labels.search]);
|
|
119
134
|
const flat = useMemo(() => sections.flatMap((s) => s.emojis), [sections]);
|
|
120
135
|
const sectionStart = useMemo(() => {
|
|
121
136
|
const starts = /* @__PURE__ */ new Map();
|
|
@@ -132,13 +147,43 @@ function EmojiPicker({
|
|
|
132
147
|
onSelect(emoji);
|
|
133
148
|
hide();
|
|
134
149
|
};
|
|
135
|
-
const focusCell = useCallback((index) => {
|
|
150
|
+
const focusCell = useCallback((index, reveal = true) => {
|
|
136
151
|
setActive(index);
|
|
137
152
|
const find = () => gridRef.current?.querySelector(`[data-emoji-index="${index}"]`);
|
|
153
|
+
const move = (cell2) => {
|
|
154
|
+
cell2.focus({ preventScroll: true });
|
|
155
|
+
if (reveal && gridRef.current) revealInGrid(gridRef.current, cell2);
|
|
156
|
+
};
|
|
138
157
|
const cell = find();
|
|
139
|
-
if (cell) cell
|
|
140
|
-
else requestAnimationFrame(() =>
|
|
158
|
+
if (cell) move(cell);
|
|
159
|
+
else requestAnimationFrame(() => {
|
|
160
|
+
const later = find();
|
|
161
|
+
if (later) move(later);
|
|
162
|
+
});
|
|
141
163
|
}, []);
|
|
164
|
+
const showSection = (key) => {
|
|
165
|
+
const grid = gridRef.current;
|
|
166
|
+
const section = grid?.querySelector(`[data-section="${key}"]`);
|
|
167
|
+
if (!grid || !section) return;
|
|
168
|
+
grid.scrollTop += section.getBoundingClientRect().top - grid.getBoundingClientRect().top;
|
|
169
|
+
const start = sectionStart.get(key);
|
|
170
|
+
if (start !== void 0) focusCell(start, false);
|
|
171
|
+
};
|
|
172
|
+
const onCategory = (key) => {
|
|
173
|
+
if (!query.trim()) {
|
|
174
|
+
showSection(key);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
pendingJump.current = key;
|
|
178
|
+
setQuery("");
|
|
179
|
+
setActive(0);
|
|
180
|
+
};
|
|
181
|
+
useIsomorphicLayoutEffect(() => {
|
|
182
|
+
const key = pendingJump.current;
|
|
183
|
+
if (key === null || query.trim()) return;
|
|
184
|
+
pendingJump.current = null;
|
|
185
|
+
showSection(key);
|
|
186
|
+
}, [query, sections]);
|
|
142
187
|
const onGridKeyDown = (event) => {
|
|
143
188
|
const target = event.target;
|
|
144
189
|
const index = Number(target.dataset.emojiIndex ?? -1);
|
|
@@ -146,7 +191,7 @@ function EmojiPicker({
|
|
|
146
191
|
const next = emojiGridMove(index, event.key, flat.length, EMOJI_GRID_COLUMNS);
|
|
147
192
|
if (event.key === "ArrowUp" && next === null) {
|
|
148
193
|
event.preventDefault();
|
|
149
|
-
searchRef.current?.focus();
|
|
194
|
+
searchRef.current?.focus({ preventScroll: true });
|
|
150
195
|
return;
|
|
151
196
|
}
|
|
152
197
|
if (next === null) return;
|
|
@@ -202,6 +247,10 @@ function EmojiPicker({
|
|
|
202
247
|
className: "uix-emoji-picker-popover",
|
|
203
248
|
role: "dialog",
|
|
204
249
|
"aria-label": labels.dialog,
|
|
250
|
+
closeWhenAnchorHidden: true,
|
|
251
|
+
onAnchorHidden: () => {
|
|
252
|
+
closedByAnchor.current = true;
|
|
253
|
+
},
|
|
205
254
|
children: open ? /* @__PURE__ */ jsxs("div", { className: "uix-emoji-picker uix-emoji-picker--full", lang: locale, children: [
|
|
206
255
|
/* @__PURE__ */ jsx(
|
|
207
256
|
"input",
|
|
@@ -221,50 +270,56 @@ function EmojiPicker({
|
|
|
221
270
|
spellCheck: false
|
|
222
271
|
}
|
|
223
272
|
),
|
|
224
|
-
|
|
225
|
-
"
|
|
273
|
+
/* @__PURE__ */ jsx(
|
|
274
|
+
"div",
|
|
226
275
|
{
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
"aria-label":
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
276
|
+
className: "uix-emoji-picker__nav",
|
|
277
|
+
role: data ? "group" : void 0,
|
|
278
|
+
"aria-label": data ? labels.categories : void 0,
|
|
279
|
+
children: data ? browseSections.map((section) => /* @__PURE__ */ jsx(
|
|
280
|
+
"button",
|
|
281
|
+
{
|
|
282
|
+
type: "button",
|
|
283
|
+
className: "uix-emoji-picker__nav-btn",
|
|
284
|
+
"aria-label": section.title,
|
|
285
|
+
title: section.title,
|
|
286
|
+
onClick: () => onCategory(section.key),
|
|
287
|
+
children: /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: section.emojis[0] ? renderEmoji(section.emojis[0]) : null })
|
|
288
|
+
},
|
|
289
|
+
section.key
|
|
290
|
+
)) : null
|
|
291
|
+
}
|
|
292
|
+
),
|
|
293
|
+
/* @__PURE__ */ jsxs("div", { className: "uix-emoji-picker__grid", ref: gridRef, onKeyDown: onGridKeyDown, children: [
|
|
294
|
+
sections.map((section) => {
|
|
295
|
+
const start = offset;
|
|
296
|
+
offset += section.emojis.length;
|
|
297
|
+
if (!section.emojis.length) return null;
|
|
298
|
+
const headingId = `${id}-${section.key}`;
|
|
299
|
+
return /* @__PURE__ */ jsxs("section", { className: "uix-emoji-picker__section", "data-section": section.key, "aria-labelledby": headingId, children: [
|
|
300
|
+
/* @__PURE__ */ jsx("h3", { id: headingId, className: "uix-emoji-picker__heading", children: section.title }),
|
|
301
|
+
/* @__PURE__ */ jsx("div", { className: "uix-emoji-picker__cells", role: "group", "aria-labelledby": headingId, children: section.emojis.map((emoji, i) => {
|
|
302
|
+
const index = start + i;
|
|
303
|
+
return /* @__PURE__ */ jsx(
|
|
304
|
+
"button",
|
|
305
|
+
{
|
|
306
|
+
type: "button",
|
|
307
|
+
className: "uix-emoji-picker__btn",
|
|
308
|
+
"data-emoji-index": index,
|
|
309
|
+
tabIndex: index === activeIndex ? 0 : -1,
|
|
310
|
+
"aria-label": names?.get(emoji) ?? emoji,
|
|
311
|
+
title: names?.get(emoji),
|
|
312
|
+
onClick: () => choose(emoji),
|
|
313
|
+
onFocus: () => setActive(index),
|
|
314
|
+
children: renderEmoji(emoji)
|
|
315
|
+
},
|
|
316
|
+
`${section.key}-${emoji}`
|
|
317
|
+
);
|
|
318
|
+
}) })
|
|
319
|
+
] }, section.key);
|
|
320
|
+
}),
|
|
321
|
+
/* @__PURE__ */ jsx("p", { className: "uix-emoji-picker__status", role: "status", children: status })
|
|
322
|
+
] })
|
|
268
323
|
] }) : null
|
|
269
324
|
}
|
|
270
325
|
)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { HTMLAttributes, ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
interface KbdProps extends HTMLAttributes<HTMLElement> {
|
|
5
|
+
children?: ReactNode;
|
|
6
|
+
}
|
|
7
|
+
/** One keyboard key over `.uix-kbd` (HAR-1361; TENSOR C14, MOTUS B-A16). */
|
|
8
|
+
declare function Kbd({ className, children, ...props }: KbdProps): react.JSX.Element;
|
|
9
|
+
type KbdPlatform = 'mac' | 'other';
|
|
10
|
+
interface KbdComboProps extends Omit<HTMLAttributes<HTMLSpanElement>, 'children'> {
|
|
11
|
+
/**
|
|
12
|
+
* Keys in order, e.g. `['Mod', 'K']`. `Mod` is ⌘ on Apple and Ctrl elsewhere; `Shift`,
|
|
13
|
+
* `Alt`, `Enter`, `Escape`, `Backspace`, `Tab` and the arrows get platform glyphs.
|
|
14
|
+
*/
|
|
15
|
+
keys: string[];
|
|
16
|
+
/** Fix the platform (tests, docs). Default: detected after mount; the server renders `other`. */
|
|
17
|
+
platform?: KbdPlatform;
|
|
18
|
+
/** Spoken names for keys, e.g. `{ Mod: 'Steuerung' }`; the glyphs stay. */
|
|
19
|
+
keyLabels?: Partial<Record<string, string>>;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* A shortcut such as ⌘K / Ctrl+K. Screen readers hear the key names ("Command+K"), not the
|
|
23
|
+
* glyphs. The platform is resolved at display time, after mount, so server and client agree.
|
|
24
|
+
*/
|
|
25
|
+
declare function KbdCombo({ keys, platform, keyLabels, className, ...props }: KbdComboProps): react.JSX.Element;
|
|
26
|
+
|
|
27
|
+
export { Kbd, KbdCombo, type KbdComboProps, type KbdPlatform, type KbdProps };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import { cx } from "../cx.js";
|
|
5
|
+
function Kbd({ className, children, ...props }) {
|
|
6
|
+
return /* @__PURE__ */ jsx("kbd", { className: cx("uix-kbd", className), ...props, children });
|
|
7
|
+
}
|
|
8
|
+
const KEYS = {
|
|
9
|
+
Mod: { mac: ["\u2318", "Command"], other: ["Ctrl", "Control"] },
|
|
10
|
+
Ctrl: { mac: ["\u2303", "Control"], other: ["Ctrl", "Control"] },
|
|
11
|
+
Shift: { mac: ["\u21E7", "Shift"], other: ["Shift", "Shift"] },
|
|
12
|
+
Alt: { mac: ["\u2325", "Option"], other: ["Alt", "Alt"] },
|
|
13
|
+
Enter: { mac: ["\u21B5", "Enter"], other: ["Enter", "Enter"] },
|
|
14
|
+
Escape: { mac: ["Esc", "Escape"], other: ["Esc", "Escape"] },
|
|
15
|
+
Backspace: { mac: ["\u232B", "Backspace"], other: ["Backspace", "Backspace"] },
|
|
16
|
+
Tab: { mac: ["\u21E5", "Tab"], other: ["Tab", "Tab"] },
|
|
17
|
+
ArrowUp: { mac: ["\u2191", "Up arrow"], other: ["\u2191", "Up arrow"] },
|
|
18
|
+
ArrowDown: { mac: ["\u2193", "Down arrow"], other: ["\u2193", "Down arrow"] },
|
|
19
|
+
ArrowLeft: { mac: ["\u2190", "Left arrow"], other: ["\u2190", "Left arrow"] },
|
|
20
|
+
ArrowRight: { mac: ["\u2192", "Right arrow"], other: ["\u2192", "Right arrow"] }
|
|
21
|
+
};
|
|
22
|
+
const detectPlatform = () => {
|
|
23
|
+
if (typeof navigator === "undefined") return "other";
|
|
24
|
+
const nav = navigator;
|
|
25
|
+
return /mac|iphone|ipad|ipod/i.test(nav.userAgentData?.platform ?? navigator.platform ?? "") ? "mac" : "other";
|
|
26
|
+
};
|
|
27
|
+
function KbdCombo({ keys, platform, keyLabels, className, ...props }) {
|
|
28
|
+
const [detected, setDetected] = useState("other");
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
if (!platform) setDetected(detectPlatform());
|
|
31
|
+
}, [platform]);
|
|
32
|
+
const active = platform ?? detected;
|
|
33
|
+
const parts = keys.map((key) => {
|
|
34
|
+
const known = KEYS[key]?.[active];
|
|
35
|
+
return { glyph: known?.[0] ?? key, spoken: keyLabels?.[key] ?? known?.[1] ?? key };
|
|
36
|
+
});
|
|
37
|
+
return /* @__PURE__ */ jsxs("span", { className: cx("uix-kbd-combo", className), ...props, children: [
|
|
38
|
+
/* @__PURE__ */ jsx("span", { className: "uix-visually-hidden", children: parts.map((p) => p.spoken).join("+") }),
|
|
39
|
+
/* @__PURE__ */ jsx("span", { className: "uix-kbd-combo__keys", "aria-hidden": "true", children: parts.map((p, i) => /* @__PURE__ */ jsx("kbd", { className: "uix-kbd", children: p.glyph }, `${keys[i]}-${i}`)) })
|
|
40
|
+
] });
|
|
41
|
+
}
|
|
42
|
+
export {
|
|
43
|
+
Kbd,
|
|
44
|
+
KbdCombo
|
|
45
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ReactElement, ReactNode } from 'react';
|
|
3
|
+
import { UixRenderLink } from '../link.js';
|
|
4
|
+
import { Placement } from '../overlay-position.js';
|
|
5
|
+
|
|
6
|
+
interface MenuProps {
|
|
7
|
+
/**
|
|
8
|
+
* The menu button, usually a `Button`. It receives `aria-haspopup="menu"`,
|
|
9
|
+
* `aria-expanded`, `aria-controls` and the open/keyboard handlers.
|
|
10
|
+
*/
|
|
11
|
+
trigger: ReactElement;
|
|
12
|
+
/** `MenuItem`, `MenuGroup` and `MenuSeparator`. */
|
|
13
|
+
children?: ReactNode;
|
|
14
|
+
/** Controlled open state; pair with `onOpenChange`. */
|
|
15
|
+
open?: boolean;
|
|
16
|
+
defaultOpen?: boolean;
|
|
17
|
+
onOpenChange?: (open: boolean) => void;
|
|
18
|
+
/** Accessible name of the menu. Default: labelled by the trigger. */
|
|
19
|
+
label?: string;
|
|
20
|
+
placement?: Placement;
|
|
21
|
+
className?: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* A menu button with an APG menu (HAR-1359; TENSOR C1, MOTUS B-A14): ↓/↑ move, Home/End,
|
|
25
|
+
* type-to-find, Enter/Space activate, Esc and Tab close, focus returns to the trigger.
|
|
26
|
+
* The surface is the kit `Popover`, so it escapes clipping containers.
|
|
27
|
+
*/
|
|
28
|
+
declare function Menu({ trigger, children, open: controlledOpen, defaultOpen, onOpenChange, label, placement, className }: MenuProps): react.JSX.Element;
|
|
29
|
+
interface MenuItemProps {
|
|
30
|
+
children?: ReactNode;
|
|
31
|
+
/** Runs on click, Enter or Space; the menu then closes and focus returns to the trigger. */
|
|
32
|
+
onSelect?: () => void;
|
|
33
|
+
/** A link item; it navigates instead of running `onSelect`. */
|
|
34
|
+
href?: string;
|
|
35
|
+
renderLink?: UixRenderLink;
|
|
36
|
+
icon?: ReactNode;
|
|
37
|
+
/** Shortcut hint at the end of the row, e.g. `<KbdCombo keys={['Mod', 'D']} />`. */
|
|
38
|
+
shortcut?: ReactNode;
|
|
39
|
+
tone?: 'default' | 'danger';
|
|
40
|
+
disabled?: boolean;
|
|
41
|
+
className?: string;
|
|
42
|
+
}
|
|
43
|
+
declare function MenuItem({ children, onSelect, href, renderLink, icon, shortcut, tone, disabled, className }: MenuItemProps): react.JSX.Element;
|
|
44
|
+
interface MenuGroupProps {
|
|
45
|
+
label: ReactNode;
|
|
46
|
+
children?: ReactNode;
|
|
47
|
+
}
|
|
48
|
+
declare function MenuGroup({ label, children }: MenuGroupProps): react.JSX.Element;
|
|
49
|
+
declare function MenuSeparator(): react.JSX.Element;
|
|
50
|
+
|
|
51
|
+
export { Menu, MenuGroup, type MenuGroupProps, MenuItem, type MenuItemProps, type MenuProps, MenuSeparator };
|