@tensor_1/react 2.25.0 → 2.26.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 +15 -1
- package/dist/components/Drawer.d.ts +7 -1
- package/dist/components/Drawer.js +3 -2
- package/dist/components/Peek.js +2 -9
- package/dist/components/RichTextEditor.d.ts +9 -1
- package/dist/components/RichTextEditor.js +23 -10
- package/dist/hooks/backdropDismiss.d.ts +7 -0
- package/dist/hooks/backdropDismiss.js +16 -0
- package/dist/index.cjs +19 -11
- package/dist/index.d.cts +7 -1
- package/dist/rich-text.cjs +23 -10
- package/dist/rich-text.d.cts +9 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -92,7 +92,9 @@ Markdown cannot represent everything a user can type. When the editor re-seriali
|
|
|
92
92
|
- Reference definitions (`[id]: url`) are kept. If their surrounding block is deleted, they move to the end.
|
|
93
93
|
|
|
94
94
|
Editor props: `features` (`full` / `comment` / `template`), `headingLevels` (limits the toolbar and input rules;
|
|
95
|
-
existing headings at other levels keep their level), `onUploadImage` (enables the image button and image paste/drop
|
|
95
|
+
existing headings at other levels keep their level), `onUploadImage` (enables the image button and image paste/drop in
|
|
96
|
+
every preset), `imagesUnavailableReason` (status-line text when an image is pasted or dropped while images are off;
|
|
97
|
+
default label `imagesUnsupported`, "Images can't be added here."; a paste that also carries text stays a text paste),
|
|
96
98
|
`isSafeUrl`, `resolveImageSrc` (its return value is used verbatim), `onSubmitShortcut` (Ctrl/Cmd+Enter), `emoji`,
|
|
97
99
|
`emojiImageBaseUrl`, `maxLength` (counter; over the limit sets `aria-invalid`), `placeholder`, `disabled`, `readOnly`,
|
|
98
100
|
`labels` (every string, placeholders `{count}` `{max}`), `emojiPickerLabels`, `emojiLocale`, `id`, `name` (adds a
|
|
@@ -227,6 +229,18 @@ Radial mode still shortens long labels to fit its ring, but every node carries i
|
|
|
227
229
|
|
|
228
230
|
Ships **ESM + CJS + types**, with per-file `"use client"` so it's safe under React Server Components. Part of the **[UIx v2 styleguide](https://github.com/COx-ORG-H/UIx)**.
|
|
229
231
|
|
|
232
|
+
### Drawer closes on a backdrop click
|
|
233
|
+
|
|
234
|
+
`Drawer` closes on a click on the dimmed backdrop, as well as on Escape and its close button, and calls the same
|
|
235
|
+
`onClose` (TENSOR HAR-547). `Peek` already did; both now share one handler. On a drawer that holds unsaved form input,
|
|
236
|
+
pass `dismissOnBackdrop={false}` so a stray click can't discard it; Escape and the close button still close. On a
|
|
237
|
+
phone the panel stops 2em + 6px short of the left edge (the browser's modal-dialog `max-width`), and a tap on that
|
|
238
|
+
strip closes it too.
|
|
239
|
+
|
|
240
|
+
```tsx
|
|
241
|
+
<Drawer open={open} onClose={close} title="Edit user" dismissOnBackdrop={!dirty}>…</Drawer>
|
|
242
|
+
```
|
|
243
|
+
|
|
230
244
|
### InfoTip and `help` slots
|
|
231
245
|
|
|
232
246
|
A small ? next to a title or label opens a plain-text explanation. Use it for what a page, section or field *means*. Keep instructions the user needs while typing ("8+ characters", "ISO date") visible in `hint`.
|
|
@@ -6,10 +6,16 @@ interface DrawerProps extends Omit<HTMLAttributes<HTMLDialogElement>, 'title'> {
|
|
|
6
6
|
closeLabel?: string;
|
|
7
7
|
open: boolean;
|
|
8
8
|
onClose?: () => void;
|
|
9
|
+
/**
|
|
10
|
+
* A click on the dimmed backdrop calls `onClose`, like Escape and the close button
|
|
11
|
+
* (TENSOR HAR-547). Pass `false` on a drawer that holds unsaved form input, so a stray click
|
|
12
|
+
* can't discard it; Escape and the close button still close. Default `true`.
|
|
13
|
+
*/
|
|
14
|
+
dismissOnBackdrop?: boolean;
|
|
9
15
|
title?: ReactNode;
|
|
10
16
|
children?: ReactNode;
|
|
11
17
|
footer?: ReactNode;
|
|
12
18
|
}
|
|
13
|
-
declare function Drawer({ open, onClose, title, children, footer, closeLabel, className, ...rest }: DrawerProps): react.JSX.Element;
|
|
19
|
+
declare function Drawer({ open, onClose, dismissOnBackdrop, title, children, footer, closeLabel, className, onClick, ...rest }: DrawerProps): react.JSX.Element;
|
|
14
20
|
|
|
15
21
|
export { Drawer, type DrawerProps };
|
|
@@ -3,11 +3,12 @@ import { jsx, jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { useId } from "react";
|
|
4
4
|
import { cx } from "../cx.js";
|
|
5
5
|
import { useDialog } from "../hooks/useDialog.js";
|
|
6
|
+
import { backdropDismiss } from "../hooks/backdropDismiss.js";
|
|
6
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" }) });
|
|
7
|
-
function Drawer({ open, onClose, title, children, footer, closeLabel = "Close drawer", className, ...rest }) {
|
|
8
|
+
function Drawer({ open, onClose, dismissOnBackdrop = true, title, children, footer, closeLabel = "Close drawer", className, onClick, ...rest }) {
|
|
8
9
|
const ref = useDialog(open, onClose);
|
|
9
10
|
const titleId = useId();
|
|
10
|
-
return /* @__PURE__ */ jsxs("dialog", { ref, className: cx("uix-drawer", className), "aria-labelledby": title ? titleId : void 0, ...rest, children: [
|
|
11
|
+
return /* @__PURE__ */ jsxs("dialog", { ref, className: cx("uix-drawer", className), "aria-labelledby": title ? titleId : void 0, ...rest, onClick: backdropDismiss(onClose, onClick, dismissOnBackdrop), children: [
|
|
11
12
|
(title != null || onClose) && /* @__PURE__ */ jsxs("div", { className: "uix-drawer__header", children: [
|
|
12
13
|
title && /* @__PURE__ */ jsx("h2", { id: titleId, style: { fontWeight: 600, fontSize: "var(--uix-text-h3)", fontFamily: "inherit", letterSpacing: "inherit", lineHeight: "inherit" }, children: title }),
|
|
13
14
|
onClose && /* @__PURE__ */ jsx(
|
package/dist/components/Peek.js
CHANGED
|
@@ -3,6 +3,7 @@ import { jsx, jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { useId } from "react";
|
|
4
4
|
import { cx } from "../cx.js";
|
|
5
5
|
import { useDialog } from "../hooks/useDialog.js";
|
|
6
|
+
import { backdropDismiss } from "../hooks/backdropDismiss.js";
|
|
6
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" }) });
|
|
7
8
|
const ChevronUp = () => /* @__PURE__ */ jsx("svg", { width: "12", height: "12", viewBox: "0 0 12 12", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: "M2 8l4-4 4 4" }) });
|
|
8
9
|
const ChevronDown = () => /* @__PURE__ */ jsx("svg", { width: "12", height: "12", viewBox: "0 0 12 12", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: "M2 4l4 4 4-4" }) });
|
|
@@ -10,15 +11,7 @@ function Peek({ open, onClose, title, children, footer, onNavPrev, onNavNext, hi
|
|
|
10
11
|
const ref = useDialog(open, onClose);
|
|
11
12
|
const hasNav = onNavPrev != null || onNavNext != null;
|
|
12
13
|
const titleId = useId();
|
|
13
|
-
|
|
14
|
-
onClick?.(e);
|
|
15
|
-
if (!onClose || e.target !== e.currentTarget) return;
|
|
16
|
-
const r = e.currentTarget.getBoundingClientRect();
|
|
17
|
-
if (r.width === 0) return;
|
|
18
|
-
const outside = e.clientX < r.left || e.clientX > r.right || e.clientY < r.top || e.clientY > r.bottom;
|
|
19
|
-
if (outside) onClose();
|
|
20
|
-
};
|
|
21
|
-
return /* @__PURE__ */ jsxs("dialog", { ref, className: cx("uix-peek", className), "aria-labelledby": title ? titleId : void 0, ...rest, onClick: onBackdropClick, children: [
|
|
14
|
+
return /* @__PURE__ */ jsxs("dialog", { ref, className: cx("uix-peek", className), "aria-labelledby": title ? titleId : void 0, ...rest, onClick: backdropDismiss(onClose, onClick), children: [
|
|
22
15
|
/* @__PURE__ */ jsxs("div", { className: "uix-peek__header", children: [
|
|
23
16
|
hasNav && /* @__PURE__ */ jsxs("div", { className: "uix-peek__nav", children: [
|
|
24
17
|
/* @__PURE__ */ jsx("button", { className: "uix-peek__navbtn", onClick: () => onNavPrev?.(), "aria-disabled": onNavPrev ? void 0 : true, "aria-label": previousLabel, children: /* @__PURE__ */ jsx(ChevronUp, {}) }),
|
|
@@ -39,6 +39,8 @@ interface RichTextLabels {
|
|
|
39
39
|
image: string;
|
|
40
40
|
imageUploading: string;
|
|
41
41
|
imageFailed: string;
|
|
42
|
+
/** Shown when an image is pasted or dropped where images are off (default for `imagesUnavailableReason`). */
|
|
43
|
+
imagesUnsupported: string;
|
|
42
44
|
emoji: string;
|
|
43
45
|
/** Accessible name of the `:shortcode` suggestion list. */
|
|
44
46
|
emojiSuggestions: string;
|
|
@@ -65,11 +67,17 @@ interface RichTextEditorProps {
|
|
|
65
67
|
features?: RichTextFeatures;
|
|
66
68
|
/** Heading levels the toolbar and input rules offer. Default `[1, 2, 3]`. */
|
|
67
69
|
headingLevels?: ReadonlyArray<RichTextHeadingLevel>;
|
|
68
|
-
/** Enables the image button
|
|
70
|
+
/** Enables image paste/drop and the toolbar image button, in every preset. */
|
|
69
71
|
onUploadImage?: (file: File) => Promise<{
|
|
70
72
|
src: string;
|
|
71
73
|
alt: string;
|
|
72
74
|
}>;
|
|
75
|
+
/**
|
|
76
|
+
* Shown in the status line when an image is pasted or dropped while images are off (no
|
|
77
|
+
* `onUploadImage`), e.g. "Attach files to the incident instead.". Nothing is inserted.
|
|
78
|
+
* Default: the `imagesUnsupported` label.
|
|
79
|
+
*/
|
|
80
|
+
imagesUnavailableReason?: string;
|
|
73
81
|
/** Link policy. Default: http(s), mailto and same-app paths. */
|
|
74
82
|
isSafeUrl?: (url: string) => boolean;
|
|
75
83
|
/** Image policy: the URL to display, or `null` to show the image as a link chip. */
|
|
@@ -59,6 +59,7 @@ const DEFAULT_RICH_TEXT_LABELS = {
|
|
|
59
59
|
image: "Image",
|
|
60
60
|
imageUploading: "Uploading image\u2026",
|
|
61
61
|
imageFailed: "The image could not be added.",
|
|
62
|
+
imagesUnsupported: "Images can't be added here.",
|
|
62
63
|
emoji: "Emoji",
|
|
63
64
|
emojiSuggestions: "Emoji suggestions",
|
|
64
65
|
undo: "Undo",
|
|
@@ -70,8 +71,9 @@ const DEFAULT_RICH_TEXT_LABELS = {
|
|
|
70
71
|
const COMMON = ["bold", "italic", "strike", "code", "bulletList", "orderedList", "taskList", "blockquote", "codeBlock", "link", "emoji", "history", "source"];
|
|
71
72
|
const PRESETS = {
|
|
72
73
|
full: /* @__PURE__ */ new Set([...COMMON, "heading", "horizontalRule", "table", "image"]),
|
|
73
|
-
|
|
74
|
-
|
|
74
|
+
// Images in every preset once the consumer supplies onUploadImage (TENSOR HAR-749).
|
|
75
|
+
comment: /* @__PURE__ */ new Set([...COMMON, "image"]),
|
|
76
|
+
template: /* @__PURE__ */ new Set([...COMMON, "heading", "horizontalRule", "table", "image"])
|
|
75
77
|
};
|
|
76
78
|
const emojiSuggestionKey = new PluginKey("uixEmojiSuggestion");
|
|
77
79
|
const SHORTCODE_RE = /:([a-zA-Z0-9_+-]+):$/;
|
|
@@ -187,7 +189,8 @@ function RichTextEditor(props) {
|
|
|
187
189
|
minRows,
|
|
188
190
|
className,
|
|
189
191
|
onBlur,
|
|
190
|
-
emojiImageBaseUrl
|
|
192
|
+
emojiImageBaseUrl,
|
|
193
|
+
imagesUnavailableReason
|
|
191
194
|
} = props;
|
|
192
195
|
const emojiBase = useMemo(() => normalizeEmojiImageBaseUrl(emojiImageBaseUrl), [emojiImageBaseUrl]);
|
|
193
196
|
const renderEmoji = useEmojiRenderer(emojiImageBaseUrl);
|
|
@@ -199,7 +202,7 @@ function RichTextEditor(props) {
|
|
|
199
202
|
const linkPopoverId = `${uid}-link`;
|
|
200
203
|
const controls = PRESETS[features];
|
|
201
204
|
const has = (control) => controls.has(control) && (control !== "emoji" || emoji) && (control !== "image" || !!onUploadImage);
|
|
202
|
-
const latest = useRef({ onChange, onSubmitShortcut, onBlur, isSafeUrl, resolveImageSrc, onUploadImage, placeholder, imagesOn: false });
|
|
205
|
+
const latest = useRef({ onChange, onSubmitShortcut, onBlur, isSafeUrl, resolveImageSrc, onUploadImage, placeholder, imagesOn: false, imagesOffText: "" });
|
|
203
206
|
latest.current = {
|
|
204
207
|
onChange,
|
|
205
208
|
onSubmitShortcut,
|
|
@@ -208,7 +211,8 @@ function RichTextEditor(props) {
|
|
|
208
211
|
resolveImageSrc,
|
|
209
212
|
onUploadImage,
|
|
210
213
|
placeholder,
|
|
211
|
-
imagesOn: controls.has("image") && !!onUploadImage
|
|
214
|
+
imagesOn: controls.has("image") && !!onUploadImage,
|
|
215
|
+
imagesOffText: imagesUnavailableReason?.trim() || labels.imagesUnsupported
|
|
212
216
|
};
|
|
213
217
|
const lastValue = useRef(value);
|
|
214
218
|
const origin = useRef(null);
|
|
@@ -431,22 +435,31 @@ function RichTextEditor(props) {
|
|
|
431
435
|
shouldRerenderOnTransaction: false,
|
|
432
436
|
editable: !disabled && !readOnly,
|
|
433
437
|
editorProps: {
|
|
438
|
+
// An image file pasted or dropped where images are off used to vanish silently; now the
|
|
439
|
+
// status line says so (TENSOR HAR-749). A paste that also carries text is a text paste:
|
|
440
|
+
// Excel, Word and Docs add a PNG rendering of the copied cells or text, which must not
|
|
441
|
+
// replace (or block) the text itself.
|
|
434
442
|
handlePaste: (_view, event) => {
|
|
435
|
-
const
|
|
436
|
-
|
|
437
|
-
files.
|
|
443
|
+
const data = event.clipboardData;
|
|
444
|
+
const files = Array.from(data?.files ?? []).filter((f) => f.type.startsWith("image/"));
|
|
445
|
+
if (!files.length || !editorRef.current || data?.getData("text/plain").trim()) return false;
|
|
446
|
+
event.preventDefault();
|
|
447
|
+
if (!latest.current.imagesOn) setStatus({ kind: "notice", text: latest.current.imagesOffText });
|
|
448
|
+
else files.forEach((file) => void uploadImage(editorRef.current, file));
|
|
438
449
|
return true;
|
|
439
450
|
},
|
|
440
451
|
handleDrop: (_view, event) => {
|
|
441
452
|
const files = Array.from(event.dataTransfer?.files ?? []).filter((f) => f.type.startsWith("image/"));
|
|
442
|
-
if (!files.length || !
|
|
453
|
+
if (!files.length || !editorRef.current) return false;
|
|
443
454
|
event.preventDefault();
|
|
444
|
-
|
|
455
|
+
if (!latest.current.imagesOn) setStatus({ kind: "notice", text: latest.current.imagesOffText });
|
|
456
|
+
else files.forEach((file) => void uploadImage(editorRef.current, file));
|
|
445
457
|
return true;
|
|
446
458
|
}
|
|
447
459
|
},
|
|
448
460
|
onCreate: ({ editor: ed }) => load(ed, lastValue.current),
|
|
449
461
|
onUpdate: ({ editor: ed }) => {
|
|
462
|
+
setStatus((s) => s?.kind === "notice" ? null : s);
|
|
450
463
|
if (!origin.current) return;
|
|
451
464
|
emit(writeMarkdown(ed.getJSON(), origin.current, managerOf(ed)));
|
|
452
465
|
},
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { MouseEventHandler, MouseEvent } from 'react';
|
|
2
|
+
|
|
3
|
+
declare function isBackdropClick(e: MouseEvent<HTMLDialogElement>): boolean;
|
|
4
|
+
/** The dialog's onClick: runs the consumer's own onClick first, then closes on a backdrop click. */
|
|
5
|
+
declare function backdropDismiss(onClose: (() => void) | undefined, onClick?: MouseEventHandler<HTMLDialogElement>, enabled?: boolean): MouseEventHandler<HTMLDialogElement>;
|
|
6
|
+
|
|
7
|
+
export { backdropDismiss, isBackdropClick };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
function isBackdropClick(e) {
|
|
2
|
+
if (e.target !== e.currentTarget) return false;
|
|
3
|
+
const r = e.currentTarget.getBoundingClientRect();
|
|
4
|
+
if (r.width === 0) return false;
|
|
5
|
+
return e.clientX < r.left || e.clientX > r.right || e.clientY < r.top || e.clientY > r.bottom;
|
|
6
|
+
}
|
|
7
|
+
function backdropDismiss(onClose, onClick, enabled = true) {
|
|
8
|
+
return (e) => {
|
|
9
|
+
onClick?.(e);
|
|
10
|
+
if (enabled && onClose && isBackdropClick(e)) onClose();
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export {
|
|
14
|
+
backdropDismiss,
|
|
15
|
+
isBackdropClick
|
|
16
|
+
};
|
package/dist/index.cjs
CHANGED
|
@@ -1791,12 +1791,28 @@ function PromptDialog({ open, title, description, inputLabel, defaultValue = "",
|
|
|
1791
1791
|
|
|
1792
1792
|
// src/components/Drawer.tsx
|
|
1793
1793
|
var import_react19 = require("react");
|
|
1794
|
+
|
|
1795
|
+
// src/hooks/backdropDismiss.ts
|
|
1796
|
+
function isBackdropClick(e) {
|
|
1797
|
+
if (e.target !== e.currentTarget) return false;
|
|
1798
|
+
const r = e.currentTarget.getBoundingClientRect();
|
|
1799
|
+
if (r.width === 0) return false;
|
|
1800
|
+
return e.clientX < r.left || e.clientX > r.right || e.clientY < r.top || e.clientY > r.bottom;
|
|
1801
|
+
}
|
|
1802
|
+
function backdropDismiss(onClose, onClick, enabled = true) {
|
|
1803
|
+
return (e) => {
|
|
1804
|
+
onClick?.(e);
|
|
1805
|
+
if (enabled && onClose && isBackdropClick(e)) onClose();
|
|
1806
|
+
};
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
// src/components/Drawer.tsx
|
|
1794
1810
|
var import_jsx_runtime16 = require("react/jsx-runtime");
|
|
1795
1811
|
var CloseIcon2 = () => /* @__PURE__ */ (0, import_jsx_runtime16.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__ */ (0, import_jsx_runtime16.jsx)("path", { d: "M4 4l8 8M12 4l-8 8" }) });
|
|
1796
|
-
function Drawer({ open, onClose, title, children, footer, closeLabel = "Close drawer", className, ...rest }) {
|
|
1812
|
+
function Drawer({ open, onClose, dismissOnBackdrop = true, title, children, footer, closeLabel = "Close drawer", className, onClick, ...rest }) {
|
|
1797
1813
|
const ref = useDialog(open, onClose);
|
|
1798
1814
|
const titleId = (0, import_react19.useId)();
|
|
1799
|
-
return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("dialog", { ref, className: cx("uix-drawer", className), "aria-labelledby": title ? titleId : void 0, ...rest, children: [
|
|
1815
|
+
return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("dialog", { ref, className: cx("uix-drawer", className), "aria-labelledby": title ? titleId : void 0, ...rest, onClick: backdropDismiss(onClose, onClick, dismissOnBackdrop), children: [
|
|
1800
1816
|
(title != null || onClose) && /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "uix-drawer__header", children: [
|
|
1801
1817
|
title && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("h2", { id: titleId, style: { fontWeight: 600, fontSize: "var(--uix-text-h3)", fontFamily: "inherit", letterSpacing: "inherit", lineHeight: "inherit" }, children: title }),
|
|
1802
1818
|
onClose && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
|
|
@@ -1824,15 +1840,7 @@ function Peek({ open, onClose, title, children, footer, onNavPrev, onNavNext, hi
|
|
|
1824
1840
|
const ref = useDialog(open, onClose);
|
|
1825
1841
|
const hasNav = onNavPrev != null || onNavNext != null;
|
|
1826
1842
|
const titleId = (0, import_react20.useId)();
|
|
1827
|
-
|
|
1828
|
-
onClick?.(e);
|
|
1829
|
-
if (!onClose || e.target !== e.currentTarget) return;
|
|
1830
|
-
const r = e.currentTarget.getBoundingClientRect();
|
|
1831
|
-
if (r.width === 0) return;
|
|
1832
|
-
const outside = e.clientX < r.left || e.clientX > r.right || e.clientY < r.top || e.clientY > r.bottom;
|
|
1833
|
-
if (outside) onClose();
|
|
1834
|
-
};
|
|
1835
|
-
return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("dialog", { ref, className: cx("uix-peek", className), "aria-labelledby": title ? titleId : void 0, ...rest, onClick: onBackdropClick, children: [
|
|
1843
|
+
return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("dialog", { ref, className: cx("uix-peek", className), "aria-labelledby": title ? titleId : void 0, ...rest, onClick: backdropDismiss(onClose, onClick), children: [
|
|
1836
1844
|
/* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "uix-peek__header", children: [
|
|
1837
1845
|
hasNav && /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "uix-peek__nav", children: [
|
|
1838
1846
|
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)("button", { className: "uix-peek__navbtn", onClick: () => onNavPrev?.(), "aria-disabled": onNavPrev ? void 0 : true, "aria-label": previousLabel, children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(ChevronUp, {}) }),
|
package/dist/index.d.cts
CHANGED
|
@@ -453,11 +453,17 @@ interface DrawerProps extends Omit<HTMLAttributes<HTMLDialogElement>, 'title'> {
|
|
|
453
453
|
closeLabel?: string;
|
|
454
454
|
open: boolean;
|
|
455
455
|
onClose?: () => void;
|
|
456
|
+
/**
|
|
457
|
+
* A click on the dimmed backdrop calls `onClose`, like Escape and the close button
|
|
458
|
+
* (TENSOR HAR-547). Pass `false` on a drawer that holds unsaved form input, so a stray click
|
|
459
|
+
* can't discard it; Escape and the close button still close. Default `true`.
|
|
460
|
+
*/
|
|
461
|
+
dismissOnBackdrop?: boolean;
|
|
456
462
|
title?: ReactNode;
|
|
457
463
|
children?: ReactNode;
|
|
458
464
|
footer?: ReactNode;
|
|
459
465
|
}
|
|
460
|
-
declare function Drawer({ open, onClose, title, children, footer, closeLabel, className, ...rest }: DrawerProps): react.JSX.Element;
|
|
466
|
+
declare function Drawer({ open, onClose, dismissOnBackdrop, title, children, footer, closeLabel, className, onClick, ...rest }: DrawerProps): react.JSX.Element;
|
|
461
467
|
|
|
462
468
|
interface PeekProps extends Omit<HTMLAttributes<HTMLDialogElement>, 'title'> {
|
|
463
469
|
/** TENSOR RX-125 (UIX-04): translatable; English default. */
|
package/dist/rich-text.cjs
CHANGED
|
@@ -1674,6 +1674,7 @@ var DEFAULT_RICH_TEXT_LABELS = {
|
|
|
1674
1674
|
image: "Image",
|
|
1675
1675
|
imageUploading: "Uploading image\u2026",
|
|
1676
1676
|
imageFailed: "The image could not be added.",
|
|
1677
|
+
imagesUnsupported: "Images can't be added here.",
|
|
1677
1678
|
emoji: "Emoji",
|
|
1678
1679
|
emojiSuggestions: "Emoji suggestions",
|
|
1679
1680
|
undo: "Undo",
|
|
@@ -1685,8 +1686,9 @@ var DEFAULT_RICH_TEXT_LABELS = {
|
|
|
1685
1686
|
var COMMON = ["bold", "italic", "strike", "code", "bulletList", "orderedList", "taskList", "blockquote", "codeBlock", "link", "emoji", "history", "source"];
|
|
1686
1687
|
var PRESETS = {
|
|
1687
1688
|
full: /* @__PURE__ */ new Set([...COMMON, "heading", "horizontalRule", "table", "image"]),
|
|
1688
|
-
|
|
1689
|
-
|
|
1689
|
+
// Images in every preset once the consumer supplies onUploadImage (TENSOR HAR-749).
|
|
1690
|
+
comment: /* @__PURE__ */ new Set([...COMMON, "image"]),
|
|
1691
|
+
template: /* @__PURE__ */ new Set([...COMMON, "heading", "horizontalRule", "table", "image"])
|
|
1690
1692
|
};
|
|
1691
1693
|
var emojiSuggestionKey = new import_state.PluginKey("uixEmojiSuggestion");
|
|
1692
1694
|
var SHORTCODE_RE = /:([a-zA-Z0-9_+-]+):$/;
|
|
@@ -1802,7 +1804,8 @@ function RichTextEditor(props) {
|
|
|
1802
1804
|
minRows,
|
|
1803
1805
|
className,
|
|
1804
1806
|
onBlur,
|
|
1805
|
-
emojiImageBaseUrl
|
|
1807
|
+
emojiImageBaseUrl,
|
|
1808
|
+
imagesUnavailableReason
|
|
1806
1809
|
} = props;
|
|
1807
1810
|
const emojiBase = (0, import_react7.useMemo)(() => normalizeEmojiImageBaseUrl(emojiImageBaseUrl), [emojiImageBaseUrl]);
|
|
1808
1811
|
const renderEmoji = useEmojiRenderer(emojiImageBaseUrl);
|
|
@@ -1814,7 +1817,7 @@ function RichTextEditor(props) {
|
|
|
1814
1817
|
const linkPopoverId = `${uid}-link`;
|
|
1815
1818
|
const controls = PRESETS[features];
|
|
1816
1819
|
const has = (control) => controls.has(control) && (control !== "emoji" || emoji) && (control !== "image" || !!onUploadImage);
|
|
1817
|
-
const latest = (0, import_react7.useRef)({ onChange, onSubmitShortcut, onBlur, isSafeUrl, resolveImageSrc, onUploadImage, placeholder, imagesOn: false });
|
|
1820
|
+
const latest = (0, import_react7.useRef)({ onChange, onSubmitShortcut, onBlur, isSafeUrl, resolveImageSrc, onUploadImage, placeholder, imagesOn: false, imagesOffText: "" });
|
|
1818
1821
|
latest.current = {
|
|
1819
1822
|
onChange,
|
|
1820
1823
|
onSubmitShortcut,
|
|
@@ -1823,7 +1826,8 @@ function RichTextEditor(props) {
|
|
|
1823
1826
|
resolveImageSrc,
|
|
1824
1827
|
onUploadImage,
|
|
1825
1828
|
placeholder,
|
|
1826
|
-
imagesOn: controls.has("image") && !!onUploadImage
|
|
1829
|
+
imagesOn: controls.has("image") && !!onUploadImage,
|
|
1830
|
+
imagesOffText: imagesUnavailableReason?.trim() || labels.imagesUnsupported
|
|
1827
1831
|
};
|
|
1828
1832
|
const lastValue = (0, import_react7.useRef)(value);
|
|
1829
1833
|
const origin = (0, import_react7.useRef)(null);
|
|
@@ -2046,22 +2050,31 @@ function RichTextEditor(props) {
|
|
|
2046
2050
|
shouldRerenderOnTransaction: false,
|
|
2047
2051
|
editable: !disabled && !readOnly,
|
|
2048
2052
|
editorProps: {
|
|
2053
|
+
// An image file pasted or dropped where images are off used to vanish silently; now the
|
|
2054
|
+
// status line says so (TENSOR HAR-749). A paste that also carries text is a text paste:
|
|
2055
|
+
// Excel, Word and Docs add a PNG rendering of the copied cells or text, which must not
|
|
2056
|
+
// replace (or block) the text itself.
|
|
2049
2057
|
handlePaste: (_view, event) => {
|
|
2050
|
-
const
|
|
2051
|
-
|
|
2052
|
-
files.
|
|
2058
|
+
const data = event.clipboardData;
|
|
2059
|
+
const files = Array.from(data?.files ?? []).filter((f) => f.type.startsWith("image/"));
|
|
2060
|
+
if (!files.length || !editorRef.current || data?.getData("text/plain").trim()) return false;
|
|
2061
|
+
event.preventDefault();
|
|
2062
|
+
if (!latest.current.imagesOn) setStatus({ kind: "notice", text: latest.current.imagesOffText });
|
|
2063
|
+
else files.forEach((file) => void uploadImage(editorRef.current, file));
|
|
2053
2064
|
return true;
|
|
2054
2065
|
},
|
|
2055
2066
|
handleDrop: (_view, event) => {
|
|
2056
2067
|
const files = Array.from(event.dataTransfer?.files ?? []).filter((f) => f.type.startsWith("image/"));
|
|
2057
|
-
if (!files.length || !
|
|
2068
|
+
if (!files.length || !editorRef.current) return false;
|
|
2058
2069
|
event.preventDefault();
|
|
2059
|
-
|
|
2070
|
+
if (!latest.current.imagesOn) setStatus({ kind: "notice", text: latest.current.imagesOffText });
|
|
2071
|
+
else files.forEach((file) => void uploadImage(editorRef.current, file));
|
|
2060
2072
|
return true;
|
|
2061
2073
|
}
|
|
2062
2074
|
},
|
|
2063
2075
|
onCreate: ({ editor: ed }) => load(ed, lastValue.current),
|
|
2064
2076
|
onUpdate: ({ editor: ed }) => {
|
|
2077
|
+
setStatus((s) => s?.kind === "notice" ? null : s);
|
|
2065
2078
|
if (!origin.current) return;
|
|
2066
2079
|
emit(writeMarkdown(ed.getJSON(), origin.current, managerOf(ed)));
|
|
2067
2080
|
},
|
package/dist/rich-text.d.cts
CHANGED
|
@@ -38,6 +38,8 @@ interface RichTextLabels {
|
|
|
38
38
|
image: string;
|
|
39
39
|
imageUploading: string;
|
|
40
40
|
imageFailed: string;
|
|
41
|
+
/** Shown when an image is pasted or dropped where images are off (default for `imagesUnavailableReason`). */
|
|
42
|
+
imagesUnsupported: string;
|
|
41
43
|
emoji: string;
|
|
42
44
|
/** Accessible name of the `:shortcode` suggestion list. */
|
|
43
45
|
emojiSuggestions: string;
|
|
@@ -64,11 +66,17 @@ interface RichTextEditorProps {
|
|
|
64
66
|
features?: RichTextFeatures;
|
|
65
67
|
/** Heading levels the toolbar and input rules offer. Default `[1, 2, 3]`. */
|
|
66
68
|
headingLevels?: ReadonlyArray<RichTextHeadingLevel>;
|
|
67
|
-
/** Enables the image button
|
|
69
|
+
/** Enables image paste/drop and the toolbar image button, in every preset. */
|
|
68
70
|
onUploadImage?: (file: File) => Promise<{
|
|
69
71
|
src: string;
|
|
70
72
|
alt: string;
|
|
71
73
|
}>;
|
|
74
|
+
/**
|
|
75
|
+
* Shown in the status line when an image is pasted or dropped while images are off (no
|
|
76
|
+
* `onUploadImage`), e.g. "Attach files to the incident instead.". Nothing is inserted.
|
|
77
|
+
* Default: the `imagesUnsupported` label.
|
|
78
|
+
*/
|
|
79
|
+
imagesUnavailableReason?: string;
|
|
72
80
|
/** Link policy. Default: http(s), mailto and same-app paths. */
|
|
73
81
|
isSafeUrl?: (url: string) => boolean;
|
|
74
82
|
/** Image policy: the URL to display, or `null` to show the image as a link chip. */
|