@keboola/design 13.1.1 → 13.1.2
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/CHANGELOG.md +12 -0
- package/dist/markdown-editor.d.ts +1 -1
- package/dist/markdown-editor.js +98 -32
- package/dist/markdown-editor.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @keboola/design
|
|
2
2
|
|
|
3
|
+
## 13.1.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- `MarkdownEditor`'s floating toolbar works inside a modal `Modal` or `Sheet`.
|
|
8
|
+
- The toolbar is a `Popover` anchored to the selection, so it stacks over a surrounding Radix dialog like any popover. Its buttons take clicks, and the link panel's fields take clicks and typing. It used to be portalled to `document.body`, under the dialog's `pointer-events: none` and outside its focus trap.
|
|
9
|
+
- Escape goes to the top layer: the link panel, then the toolbar, then the dialog. Escape with the toolbar showing hides it until the selection changes, and still reaches the editor's own Escape handling (`onCancel` with `saveOnBlur` or `singleLine`). Escape with focus on a toolbar button sends focus back to the editor.
|
|
10
|
+
- Inside a dialog, the dialog's focus trap is paused while the toolbar shows, as with any non-modal popover. Tab cycles inside the toolbar.
|
|
11
|
+
- A click or focus outside the editor and the toolbar hides it, so Escape in another field reaches that field; focus back in the editor shows it again. It used to stay up, pointing at the editor's selection, with an open link panel inside it.
|
|
12
|
+
- The toolbar follows its text when the text moves (an alignment change, a container scroll, a window resize), hides while the text is scrolled out of view, and moves below the selection when there is no room above it. It used to move only on an editor update, not on a scroll or resize.
|
|
13
|
+
- Nothing else changes: it shows for a non-empty selection, a button press keeps the editor's focus and selection, and it still renders in `document.body`. `toolbarMode="fixed"` is unchanged.
|
|
14
|
+
|
|
3
15
|
## 13.1.1
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
|
@@ -3,7 +3,7 @@ import { ReactNode } from "react";
|
|
|
3
3
|
/**
|
|
4
4
|
* How the toolbar is presented.
|
|
5
5
|
*
|
|
6
|
-
* - `floating`: a
|
|
6
|
+
* - `floating`: a `Popover` over the selection, so it stacks over a surrounding dialog.
|
|
7
7
|
* - `fixed`: a static bar at the top of the editor, visible with no selection.
|
|
8
8
|
*/
|
|
9
9
|
type ToolbarMode = 'floating' | 'fixed';
|
package/dist/markdown-editor.js
CHANGED
|
@@ -6,6 +6,7 @@ import { ButtonGroup } from "./internal/components/ButtonGroup.js";
|
|
|
6
6
|
import { DropdownMenu } from "./DropdownMenu-BJGLjpVc.js";
|
|
7
7
|
import { Label } from "./internal/components/Label.js";
|
|
8
8
|
import { useStableCallback } from "./internal/hooks/useStableCallback.js";
|
|
9
|
+
import { Popover } from "./Popover-CEk915I1.js";
|
|
9
10
|
import { TextInput } from "./TextInput-BWYXbfs4.js";
|
|
10
11
|
import { FormGroup } from "./FormGroup-CmCppxOn.js";
|
|
11
12
|
import { Skeleton } from "./internal/components/Skeleton/Skeleton.js";
|
|
@@ -13,7 +14,6 @@ import { Kbd } from "./Kbd-J4n1J6i4.js";
|
|
|
13
14
|
import { toast } from "./toast-CyXEdaCZ.js";
|
|
14
15
|
import { Suspense, createContext, lazy, use, useCallback, useEffect, useId, useRef, useState } from "react";
|
|
15
16
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
16
|
-
import { createPortal } from "react-dom";
|
|
17
17
|
import { DismissableLayer } from "@radix-ui/react-dismissable-layer";
|
|
18
18
|
import { $createCodeNode, $isCodeNode, CodeNode } from "@lexical/code";
|
|
19
19
|
import { AutoFocusPlugin } from "@lexical/react/LexicalAutoFocusPlugin";
|
|
@@ -31,7 +31,7 @@ import { MarkdownShortcutPlugin } from "@lexical/react/LexicalMarkdownShortcutPl
|
|
|
31
31
|
import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin";
|
|
32
32
|
import { TabIndentationPlugin } from "@lexical/react/LexicalTabIndentationPlugin";
|
|
33
33
|
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
|
|
34
|
-
import { $addUpdateTag, $createParagraphNode, $createTextNode, $getNodeByKey, $getSelection, $insertNodes, $isParagraphNode, $isRangeSelection, $isTextNode, $parseSerializedNode, $setSelection, COMMAND_PRIORITY_CRITICAL, COMMAND_PRIORITY_HIGH, COMMAND_PRIORITY_LOW, DROP_COMMAND, DecoratorNode, FORMAT_ELEMENT_COMMAND, FORMAT_TEXT_COMMAND, HISTORY_PUSH_TAG, KEY_DOWN_COMMAND, PASTE_COMMAND, PASTE_TAG, createEditor } from "lexical";
|
|
34
|
+
import { $addUpdateTag, $createParagraphNode, $createTextNode, $getNodeByKey, $getSelection, $insertNodes, $isParagraphNode, $isRangeSelection, $isTextNode, $parseSerializedNode, $setSelection, COMMAND_PRIORITY_CRITICAL, COMMAND_PRIORITY_HIGH, COMMAND_PRIORITY_LOW, DROP_COMMAND, DecoratorNode, FOCUS_COMMAND, FORMAT_ELEMENT_COMMAND, FORMAT_TEXT_COMMAND, HISTORY_PUSH_TAG, KEY_DOWN_COMMAND, PASTE_COMMAND, PASTE_TAG, createEditor } from "lexical";
|
|
35
35
|
import { $findMatchingParent, $getNearestBlockElementAncestorOrThrow, IS_APPLE } from "@lexical/utils";
|
|
36
36
|
import { $createLinkNode, $isAutoLinkNode, $isLinkNode, AutoLinkNode, LinkNode, TOGGLE_LINK_COMMAND } from "@lexical/link";
|
|
37
37
|
import { $isListNode, INSERT_ORDERED_LIST_COMMAND, INSERT_UNORDERED_LIST_COMMAND, ListItemNode, ListNode, REMOVE_LIST_COMMAND } from "@lexical/list";
|
|
@@ -1098,6 +1098,7 @@ const ELEMENT_FORMAT_TYPES = [
|
|
|
1098
1098
|
const isElementFormatType = (value) => ELEMENT_FORMAT_TYPES.includes(value);
|
|
1099
1099
|
const toElementFormatType = (value) => isElementFormatType(value) ? value : "left";
|
|
1100
1100
|
const TOOLBAR_BASE_CLASS = "tw:flex tw:items-center tw:gap-0.5 tw:rounded-lg tw:border tw:border-border tw:px-1.5 tw:py-1";
|
|
1101
|
+
const hasFocus = (element) => !!element?.contains(document.activeElement);
|
|
1101
1102
|
/**
|
|
1102
1103
|
* Keeps focus, and the editor selection with it, where it was when a toolbar control is pressed.
|
|
1103
1104
|
*
|
|
@@ -1118,23 +1119,68 @@ const ToolbarPlugin = ({ mode = "floating" }) => {
|
|
|
1118
1119
|
const [blockType, setBlockType] = useState("paragraph");
|
|
1119
1120
|
const [alignment, setAlignment] = useState("left");
|
|
1120
1121
|
const [isVisible, setIsVisible] = useState(false);
|
|
1121
|
-
const [
|
|
1122
|
-
const toolbarRef = useRef(null);
|
|
1122
|
+
const [anchor, setAnchor] = useState(null);
|
|
1123
1123
|
const isMouseDownRef = useRef(false);
|
|
1124
1124
|
const hasSelectionRef = useRef(false);
|
|
1125
|
+
const toolbarRef = useRef(null);
|
|
1126
|
+
/** The selection an Escape hid the toolbar on; it stays hidden until the selection differs. */
|
|
1127
|
+
const dismissedRef = useRef(null);
|
|
1125
1128
|
const isFloating = mode === "floating";
|
|
1126
|
-
|
|
1129
|
+
/**
|
|
1130
|
+
* Anchors the floating toolbar to the DOM selection inside the editor.
|
|
1131
|
+
*
|
|
1132
|
+
* - The range is a copy, so it keeps measuring these nodes when the page selection moves.
|
|
1133
|
+
* - Measured live, so the toolbar follows the text on scroll and resize.
|
|
1134
|
+
* - A range whose nodes Lexical replaced measures zero; the last real rect stands in for it.
|
|
1135
|
+
* - A new anchor on every update, so floating-ui places the toolbar again when the text moves.
|
|
1136
|
+
*/
|
|
1137
|
+
const updateAnchor = useCallback(() => {
|
|
1127
1138
|
const nativeSelection = window.getSelection();
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1139
|
+
const root = editor.getRootElement();
|
|
1140
|
+
if (!root || !nativeSelection || nativeSelection.rangeCount === 0) return;
|
|
1141
|
+
const range = nativeSelection.getRangeAt(0).cloneRange();
|
|
1142
|
+
if (!root.contains(range.commonAncestorContainer)) return;
|
|
1143
|
+
let rect = range.getBoundingClientRect();
|
|
1144
|
+
if (rect.width === 0) return;
|
|
1145
|
+
const getBoundingClientRect = () => {
|
|
1146
|
+
const next = range.getBoundingClientRect();
|
|
1147
|
+
if (next.width > 0) rect = next;
|
|
1148
|
+
return rect;
|
|
1149
|
+
};
|
|
1150
|
+
setAnchor({
|
|
1151
|
+
getBoundingClientRect,
|
|
1152
|
+
contextElement: root
|
|
1153
|
+
});
|
|
1154
|
+
}, [editor]);
|
|
1155
|
+
/** Whether focus is in the editor or the toolbar, the places a selection shows the toolbar from. */
|
|
1156
|
+
const isFocusWithin = useCallback(() => hasFocus(editor.getRootElement()) || hasFocus(toolbarRef.current), [editor]);
|
|
1157
|
+
/**
|
|
1158
|
+
* Hides the floating toolbar on a click or focus outside it and the editor.
|
|
1159
|
+
*
|
|
1160
|
+
* - So Escape in another field reaches that field, not this layer.
|
|
1161
|
+
* - Radix counts the toolbar's own dropdown menus as inside.
|
|
1162
|
+
*/
|
|
1163
|
+
const handleInteractOutside = (event) => {
|
|
1164
|
+
const target = event.target;
|
|
1165
|
+
if (target instanceof Node && editor.getRootElement()?.contains(target)) return;
|
|
1166
|
+
setIsVisible(false);
|
|
1167
|
+
};
|
|
1168
|
+
/**
|
|
1169
|
+
* Escape on the floating toolbar, which `PopoverContent` stops at `document`.
|
|
1170
|
+
*
|
|
1171
|
+
* 1. Focus in the toolbar goes back to the editor, and the toolbar stays.
|
|
1172
|
+
* 2. Otherwise the toolbar hides until the selection changes; with focus in the editor, its own
|
|
1173
|
+
* Escape handling gets the key as a command.
|
|
1174
|
+
*/
|
|
1175
|
+
const handleEscape = (event) => {
|
|
1176
|
+
if (hasFocus(toolbarRef.current)) {
|
|
1177
|
+
editor.focus();
|
|
1178
|
+
return;
|
|
1136
1179
|
}
|
|
1137
|
-
|
|
1180
|
+
setIsVisible(false);
|
|
1181
|
+
dismissedRef.current = editor.getEditorState().read(() => $getSelection()?.clone() ?? null);
|
|
1182
|
+
if (hasFocus(editor.getRootElement())) editor.dispatchCommand(KEY_DOWN_COMMAND, event);
|
|
1183
|
+
};
|
|
1138
1184
|
useEffect(() => {
|
|
1139
1185
|
if (!isFloating) return;
|
|
1140
1186
|
const handleMouseDown = () => {
|
|
@@ -1142,15 +1188,24 @@ const ToolbarPlugin = ({ mode = "floating" }) => {
|
|
|
1142
1188
|
};
|
|
1143
1189
|
const handleMouseUp = () => {
|
|
1144
1190
|
isMouseDownRef.current = false;
|
|
1145
|
-
if (hasSelectionRef.current) setIsVisible(true);
|
|
1191
|
+
if (hasSelectionRef.current && !dismissedRef.current && isFocusWithin()) setIsVisible(true);
|
|
1146
1192
|
};
|
|
1147
1193
|
document.addEventListener("mousedown", handleMouseDown);
|
|
1148
1194
|
document.addEventListener("mouseup", handleMouseUp);
|
|
1195
|
+
const unregisterFocus = editor.registerCommand(FOCUS_COMMAND, () => {
|
|
1196
|
+
if (hasSelectionRef.current && !dismissedRef.current) setIsVisible(true);
|
|
1197
|
+
return false;
|
|
1198
|
+
}, COMMAND_PRIORITY_LOW);
|
|
1149
1199
|
return () => {
|
|
1150
1200
|
document.removeEventListener("mousedown", handleMouseDown);
|
|
1151
1201
|
document.removeEventListener("mouseup", handleMouseUp);
|
|
1202
|
+
unregisterFocus();
|
|
1152
1203
|
};
|
|
1153
|
-
}, [
|
|
1204
|
+
}, [
|
|
1205
|
+
editor,
|
|
1206
|
+
isFloating,
|
|
1207
|
+
isFocusWithin
|
|
1208
|
+
]);
|
|
1154
1209
|
const updateToolbar = useCallback(() => {
|
|
1155
1210
|
const selection = $getSelection();
|
|
1156
1211
|
if (!$isRangeSelection(selection)) {
|
|
@@ -1160,9 +1215,10 @@ const ToolbarPlugin = ({ mode = "floating" }) => {
|
|
|
1160
1215
|
}
|
|
1161
1216
|
const hasSelection = !selection.isCollapsed() && selection.getTextContent().length > 0;
|
|
1162
1217
|
hasSelectionRef.current = hasSelection;
|
|
1218
|
+
if (dismissedRef.current && !selection.is(dismissedRef.current)) dismissedRef.current = null;
|
|
1163
1219
|
if (!hasSelection) setIsVisible(false);
|
|
1164
|
-
else if (!isMouseDownRef.current) setIsVisible(true);
|
|
1165
|
-
if (hasSelection)
|
|
1220
|
+
else if (!isMouseDownRef.current && !dismissedRef.current && isFocusWithin()) setIsVisible(true);
|
|
1221
|
+
if (hasSelection && isFloating) updateAnchor();
|
|
1166
1222
|
setIsBold(selection.hasFormat("bold"));
|
|
1167
1223
|
setIsItalic(selection.hasFormat("italic"));
|
|
1168
1224
|
setIsUnderline(selection.hasFormat("underline"));
|
|
@@ -1180,7 +1236,11 @@ const ToolbarPlugin = ({ mode = "floating" }) => {
|
|
|
1180
1236
|
else if ($isQuoteNode(blockElement)) setBlockType("quote");
|
|
1181
1237
|
else setBlockType("paragraph");
|
|
1182
1238
|
setAlignment(toElementFormatType(blockElement.getFormatType()));
|
|
1183
|
-
}, [
|
|
1239
|
+
}, [
|
|
1240
|
+
isFloating,
|
|
1241
|
+
isFocusWithin,
|
|
1242
|
+
updateAnchor
|
|
1243
|
+
]);
|
|
1184
1244
|
useEffect(() => {
|
|
1185
1245
|
const unregisterUpdate = editor.registerUpdateListener(({ editorState }) => {
|
|
1186
1246
|
editorState.read(() => {
|
|
@@ -1191,7 +1251,6 @@ const ToolbarPlugin = ({ mode = "floating" }) => {
|
|
|
1191
1251
|
unregisterUpdate();
|
|
1192
1252
|
};
|
|
1193
1253
|
}, [editor, updateToolbar]);
|
|
1194
|
-
if (isFloating && (!isVisible || !position)) return null;
|
|
1195
1254
|
const buttons = /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
1196
1255
|
/* @__PURE__ */ jsx(BlockTypeDropdown, {
|
|
1197
1256
|
editor,
|
|
@@ -1301,18 +1360,25 @@ const ToolbarPlugin = ({ mode = "floating" }) => {
|
|
|
1301
1360
|
className: cn(TOOLBAR_BASE_CLASS, "tw:mb-1 tw:flex-wrap tw:bg-background"),
|
|
1302
1361
|
children: buttons
|
|
1303
1362
|
});
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1363
|
+
if (!isVisible || !anchor) return null;
|
|
1364
|
+
return /* @__PURE__ */ jsxs(Popover, {
|
|
1365
|
+
open: true,
|
|
1366
|
+
children: [/* @__PURE__ */ jsx(Popover.Anchor, { virtualRef: { current: anchor } }), /* @__PURE__ */ jsx(Popover.Content, {
|
|
1367
|
+
ref: toolbarRef,
|
|
1368
|
+
role: "toolbar",
|
|
1369
|
+
"aria-label": "Formatting options",
|
|
1370
|
+
side: "top",
|
|
1371
|
+
sideOffset: 8,
|
|
1372
|
+
collisionPadding: 8,
|
|
1373
|
+
hideWhenDetached: true,
|
|
1374
|
+
onOpenAutoFocus: (event) => event.preventDefault(),
|
|
1375
|
+
onEscapeKeyDown: handleEscape,
|
|
1376
|
+
onInteractOutside: handleInteractOutside,
|
|
1377
|
+
onMouseDown: keepEditorFocus,
|
|
1378
|
+
className: cn(TOOLBAR_BASE_CLASS, "tw:flex-row tw:overflow-visible"),
|
|
1379
|
+
children: buttons
|
|
1380
|
+
})]
|
|
1381
|
+
});
|
|
1316
1382
|
};
|
|
1317
1383
|
//#endregion
|
|
1318
1384
|
//#region src/blocks/MarkdownEditor/theme.ts
|