@keboola/design 13.1.0 → 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 +25 -0
- package/dist/markdown-editor.d.ts +1 -1
- package/dist/markdown-editor.js +173 -51
- 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,30 @@
|
|
|
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
|
+
|
|
15
|
+
## 13.1.1
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- `MarkdownEditor`'s toolbar names its shortcuts for the platform it runs on, and two open link panels on one page each label their own fields.
|
|
20
|
+
- Bold, Italic and Underline are named by the action alone: `Bold`, `Italic`, `Underline`. The shortcut is in `aria-keyshortcuts`, `Meta+B` on Apple platforms, where Lexical binds Cmd, and `Control+B` elsewhere. The tooltip shows the action with its keys, `⌘+B` or `Ctrl+B`.
|
|
21
|
+
- The link panel's Label and URL fields take their ids from `useId`, so a second editor's panel no longer points its labels at the first editor's fields.
|
|
22
|
+
|
|
23
|
+
- `MarkdownEditor`'s formatting toolbar names its buttons, and its link panel takes clicks and Escape, in both `toolbarMode`s.
|
|
24
|
+
- Each icon-only button has an accessible name (`Bold`, `Insert Link`, or `Remove Link` inside a link, `Text alignment`, ...), so a screen reader announces it and a test finds it with `getByRole('button', { name })`. Bold, Italic and Underline give their shortcut in `aria-keyshortcuts`, not in the name: `Meta+B` on Apple platforms, `Control+B` elsewhere. The block-type dropdown already had a name from its visible text.
|
|
25
|
+
- A click in the link panel's Label or URL field focuses it. The toolbar still keeps focus in the editor when a button is pressed, which is what keeps the selection a format applies to; the panel saves that selection when it opens.
|
|
26
|
+
- Escape while the link panel is open closes the panel and nothing around it. The panel registers in Radix's dismissable-layer stack and claims the key, so a surrounding `Modal`, `Sheet`, `Popover` or `FullScreenModal` stays open. Focus goes back to the editor when it was in the panel and stays put when it was elsewhere. Escape with the panel closed reaches the surface as before, and an outside click or focus still leaves the panel open.
|
|
27
|
+
|
|
3
28
|
## 13.1.0
|
|
4
29
|
|
|
5
30
|
### Minor 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,13 +6,15 @@ 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
|
+
import { Kbd } from "./Kbd-J4n1J6i4.js";
|
|
12
14
|
import { toast } from "./toast-CyXEdaCZ.js";
|
|
13
|
-
import { Suspense, createContext, lazy, use, useCallback, useEffect, useRef, useState } from "react";
|
|
15
|
+
import { Suspense, createContext, lazy, use, useCallback, useEffect, useId, useRef, useState } from "react";
|
|
14
16
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
15
|
-
import {
|
|
17
|
+
import { DismissableLayer } from "@radix-ui/react-dismissable-layer";
|
|
16
18
|
import { $createCodeNode, $isCodeNode, CodeNode } from "@lexical/code";
|
|
17
19
|
import { AutoFocusPlugin } from "@lexical/react/LexicalAutoFocusPlugin";
|
|
18
20
|
import { AutoLinkPlugin } from "@lexical/react/LexicalAutoLinkPlugin";
|
|
@@ -29,8 +31,8 @@ import { MarkdownShortcutPlugin } from "@lexical/react/LexicalMarkdownShortcutPl
|
|
|
29
31
|
import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin";
|
|
30
32
|
import { TabIndentationPlugin } from "@lexical/react/LexicalTabIndentationPlugin";
|
|
31
33
|
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
|
|
32
|
-
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";
|
|
33
|
-
import { $findMatchingParent, $getNearestBlockElementAncestorOrThrow } from "@lexical/utils";
|
|
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
|
+
import { $findMatchingParent, $getNearestBlockElementAncestorOrThrow, IS_APPLE } from "@lexical/utils";
|
|
34
36
|
import { $createLinkNode, $isAutoLinkNode, $isLinkNode, AutoLinkNode, LinkNode, TOGGLE_LINK_COMMAND } from "@lexical/link";
|
|
35
37
|
import { $isListNode, INSERT_ORDERED_LIST_COMMAND, INSERT_UNORDERED_LIST_COMMAND, ListItemNode, ListNode, REMOVE_LIST_COMMAND } from "@lexical/list";
|
|
36
38
|
import { $createHorizontalRuleNode, $isHorizontalRuleNode, HorizontalRuleNode } from "@lexical/react/LexicalHorizontalRuleNode";
|
|
@@ -754,6 +756,7 @@ const ALIGNMENT_OPTIONS = [
|
|
|
754
756
|
icon: AlignJustify
|
|
755
757
|
}
|
|
756
758
|
];
|
|
759
|
+
const TRIGGER_LABEL = "Text alignment";
|
|
757
760
|
const ALIGNMENT_ICONS = Object.fromEntries(ALIGNMENT_OPTIONS.map(({ value, icon }) => [value, icon]));
|
|
758
761
|
const AlignmentDropdown = ({ editor, alignment }) => {
|
|
759
762
|
const savedSelectionRef = useRef(null);
|
|
@@ -773,13 +776,14 @@ const AlignmentDropdown = ({ editor, alignment }) => {
|
|
|
773
776
|
return /* @__PURE__ */ jsxs(DropdownMenu, {
|
|
774
777
|
onOpenChange: handleOpenChange,
|
|
775
778
|
children: [/* @__PURE__ */ jsx(Tooltip, {
|
|
776
|
-
tooltip:
|
|
779
|
+
tooltip: TRIGGER_LABEL,
|
|
777
780
|
placement: "top",
|
|
778
781
|
triggerAsChild: true,
|
|
779
782
|
children: /* @__PURE__ */ jsx(DropdownMenu.Trigger, {
|
|
780
783
|
asChild: true,
|
|
781
784
|
children: /* @__PURE__ */ jsxs("button", {
|
|
782
785
|
type: "button",
|
|
786
|
+
"aria-label": TRIGGER_LABEL,
|
|
783
787
|
className: cn("tw:flex tw:h-7 tw:cursor-pointer tw:items-center tw:gap-1 tw:rounded tw:border-0 tw:bg-transparent tw:px-1.5 tw:text-xs tw:text-muted-foreground", "tw:hover:bg-accent"),
|
|
784
788
|
children: [/* @__PURE__ */ jsx(CurrentAlignmentIcon, { className: "tw:size-4" }), /* @__PURE__ */ jsx(ChevronDown, { className: "tw:size-3" })]
|
|
785
789
|
})
|
|
@@ -906,12 +910,39 @@ const BlockTypeDropdown = ({ editor, blockType }) => {
|
|
|
906
910
|
};
|
|
907
911
|
//#endregion
|
|
908
912
|
//#region src/blocks/MarkdownEditor/plugins/toolbar/ToolbarButton.tsx
|
|
909
|
-
|
|
910
|
-
|
|
913
|
+
/**
|
|
914
|
+
* The modifier Lexical binds its format keys to: Cmd on Apple platforms, Ctrl elsewhere.
|
|
915
|
+
*
|
|
916
|
+
* - Read from Lexical's own `IS_APPLE`, which counts iPhone and iPad where `isMac` does not.
|
|
917
|
+
*/
|
|
918
|
+
const MOD_KEY = IS_APPLE ? {
|
|
919
|
+
aria: "Meta",
|
|
920
|
+
symbol: "⌘"
|
|
921
|
+
} : {
|
|
922
|
+
aria: "Control",
|
|
923
|
+
symbol: "Ctrl"
|
|
924
|
+
};
|
|
925
|
+
const ToolbarButton = ({ active, disabled, onClick, tooltip, shortcutKey, children, className: extraClassName }) => /* @__PURE__ */ jsx(Tooltip, {
|
|
926
|
+
tooltip: shortcutKey ? /* @__PURE__ */ jsxs("span", {
|
|
927
|
+
className: "tw:inline-flex tw:items-center tw:gap-2",
|
|
928
|
+
children: [tooltip, /* @__PURE__ */ jsxs(Kbd.Group, {
|
|
929
|
+
"aria-hidden": true,
|
|
930
|
+
separator: "+",
|
|
931
|
+
children: [/* @__PURE__ */ jsx(Kbd, {
|
|
932
|
+
variant: "primary",
|
|
933
|
+
children: MOD_KEY.symbol
|
|
934
|
+
}), /* @__PURE__ */ jsx(Kbd, {
|
|
935
|
+
variant: "primary",
|
|
936
|
+
children: shortcutKey
|
|
937
|
+
})]
|
|
938
|
+
})]
|
|
939
|
+
}) : tooltip,
|
|
911
940
|
placement: "top",
|
|
912
941
|
triggerAsChild: true,
|
|
913
942
|
children: /* @__PURE__ */ jsx("button", {
|
|
914
943
|
type: "button",
|
|
944
|
+
"aria-label": tooltip,
|
|
945
|
+
"aria-keyshortcuts": shortcutKey ? `${MOD_KEY.aria}+${shortcutKey}` : void 0,
|
|
915
946
|
disabled,
|
|
916
947
|
onClick,
|
|
917
948
|
className: cn("tw:flex tw:h-7 tw:min-w-7 tw:cursor-pointer tw:items-center tw:justify-center tw:rounded tw:border-0 tw:bg-transparent tw:px-1 tw:text-xs tw:text-muted-foreground", "tw:hover:bg-accent tw:disabled:cursor-not-allowed tw:disabled:opacity-30", active && "tw:bg-secondary-100 tw:text-secondary-500", extraClassName),
|
|
@@ -943,6 +974,8 @@ const LinkPanel = ({ editor, isLink }) => {
|
|
|
943
974
|
const [label, setLabel] = useState("");
|
|
944
975
|
const savedSelectionRef = useRef(null);
|
|
945
976
|
const panelRef = useRef(null);
|
|
977
|
+
const labelId = useId();
|
|
978
|
+
const urlId = useId();
|
|
946
979
|
const handleOpen = () => {
|
|
947
980
|
editor.getEditorState().read(() => {
|
|
948
981
|
const selection = $getSelection();
|
|
@@ -954,13 +987,27 @@ const LinkPanel = ({ editor, isLink }) => {
|
|
|
954
987
|
});
|
|
955
988
|
setOpen(true);
|
|
956
989
|
};
|
|
957
|
-
const
|
|
990
|
+
const resetPanel = () => {
|
|
958
991
|
setOpen(false);
|
|
959
992
|
setUrl("");
|
|
960
993
|
setLabel("");
|
|
961
994
|
savedSelectionRef.current = null;
|
|
995
|
+
};
|
|
996
|
+
const handleClose = () => {
|
|
997
|
+
resetPanel();
|
|
962
998
|
editor.focus();
|
|
963
999
|
};
|
|
1000
|
+
/**
|
|
1001
|
+
* Closes the panel on Escape pressed anywhere while it is open.
|
|
1002
|
+
*
|
|
1003
|
+
* - `preventDefault` claims the key from a surface outside Radix's stack (`FullScreenModal`).
|
|
1004
|
+
* - Focus goes back to the editor only from inside the panel; anywhere else it stays put.
|
|
1005
|
+
*/
|
|
1006
|
+
const handleEscape = (event) => {
|
|
1007
|
+
event.preventDefault();
|
|
1008
|
+
if (panelRef.current?.contains(document.activeElement)) handleClose();
|
|
1009
|
+
else resetPanel();
|
|
1010
|
+
};
|
|
964
1011
|
const handleSubmit = () => {
|
|
965
1012
|
const trimmedUrl = url.trim();
|
|
966
1013
|
if (!trimmedUrl) return;
|
|
@@ -972,10 +1019,6 @@ const LinkPanel = ({ editor, isLink }) => {
|
|
|
972
1019
|
e.preventDefault();
|
|
973
1020
|
handleSubmit();
|
|
974
1021
|
}
|
|
975
|
-
if (e.key === "Escape") {
|
|
976
|
-
e.preventDefault();
|
|
977
|
-
handleClose();
|
|
978
|
-
}
|
|
979
1022
|
};
|
|
980
1023
|
if (isLink) return /* @__PURE__ */ jsx(ToolbarButton, {
|
|
981
1024
|
active: true,
|
|
@@ -990,18 +1033,19 @@ const LinkPanel = ({ editor, isLink }) => {
|
|
|
990
1033
|
onClick: handleOpen,
|
|
991
1034
|
tooltip: "Insert Link",
|
|
992
1035
|
children: /* @__PURE__ */ jsx(Link, { className: "tw:size-4" })
|
|
993
|
-
}), open && /* @__PURE__ */ jsx(
|
|
1036
|
+
}), open && /* @__PURE__ */ jsx(DismissableLayer, {
|
|
994
1037
|
ref: panelRef,
|
|
1038
|
+
onEscapeKeyDown: handleEscape,
|
|
995
1039
|
className: "tw:absolute tw:left-0 tw:top-full tw:z-50 tw:mt-1 tw:w-80 tw:rounded-md tw:border tw:border-border tw:bg-popover tw:p-3 tw:text-popover-foreground tw:shadow-lg",
|
|
996
1040
|
children: /* @__PURE__ */ jsxs("div", {
|
|
997
1041
|
className: "tw:flex tw:flex-col tw:gap-2",
|
|
998
1042
|
children: [
|
|
999
1043
|
/* @__PURE__ */ jsxs(FormGroup, { children: [/* @__PURE__ */ jsx(Label, {
|
|
1000
|
-
htmlFor:
|
|
1044
|
+
htmlFor: labelId,
|
|
1001
1045
|
className: "tw:mb-1 tw:text-xs",
|
|
1002
1046
|
children: "Label"
|
|
1003
1047
|
}), /* @__PURE__ */ jsx(TextInput, {
|
|
1004
|
-
id:
|
|
1048
|
+
id: labelId,
|
|
1005
1049
|
autoFocus: true,
|
|
1006
1050
|
placeholder: "Link text",
|
|
1007
1051
|
value: label,
|
|
@@ -1010,11 +1054,11 @@ const LinkPanel = ({ editor, isLink }) => {
|
|
|
1010
1054
|
variant: "secondary"
|
|
1011
1055
|
})] }),
|
|
1012
1056
|
/* @__PURE__ */ jsxs(FormGroup, { children: [/* @__PURE__ */ jsx(Label, {
|
|
1013
|
-
htmlFor:
|
|
1057
|
+
htmlFor: urlId,
|
|
1014
1058
|
className: "tw:mb-1 tw:text-xs",
|
|
1015
1059
|
children: "URL"
|
|
1016
1060
|
}), /* @__PURE__ */ jsx(TextInput, {
|
|
1017
|
-
id:
|
|
1061
|
+
id: urlId,
|
|
1018
1062
|
placeholder: "https://…",
|
|
1019
1063
|
value: url,
|
|
1020
1064
|
onValueChange: setUrl,
|
|
@@ -1054,6 +1098,16 @@ const ELEMENT_FORMAT_TYPES = [
|
|
|
1054
1098
|
const isElementFormatType = (value) => ELEMENT_FORMAT_TYPES.includes(value);
|
|
1055
1099
|
const toElementFormatType = (value) => isElementFormatType(value) ? value : "left";
|
|
1056
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);
|
|
1102
|
+
/**
|
|
1103
|
+
* Keeps focus, and the editor selection with it, where it was when a toolbar control is pressed.
|
|
1104
|
+
*
|
|
1105
|
+
* - A link panel field takes focus as usual, so a click there lands the caret in it.
|
|
1106
|
+
*/
|
|
1107
|
+
const keepEditorFocus = (event) => {
|
|
1108
|
+
if (event.target instanceof HTMLInputElement) return;
|
|
1109
|
+
event.preventDefault();
|
|
1110
|
+
};
|
|
1057
1111
|
const ToolbarPlugin = ({ mode = "floating" }) => {
|
|
1058
1112
|
const [editor] = useLexicalComposerContext();
|
|
1059
1113
|
const [isBold, setIsBold] = useState(false);
|
|
@@ -1065,23 +1119,68 @@ const ToolbarPlugin = ({ mode = "floating" }) => {
|
|
|
1065
1119
|
const [blockType, setBlockType] = useState("paragraph");
|
|
1066
1120
|
const [alignment, setAlignment] = useState("left");
|
|
1067
1121
|
const [isVisible, setIsVisible] = useState(false);
|
|
1068
|
-
const [
|
|
1069
|
-
const toolbarRef = useRef(null);
|
|
1122
|
+
const [anchor, setAnchor] = useState(null);
|
|
1070
1123
|
const isMouseDownRef = useRef(false);
|
|
1071
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);
|
|
1072
1128
|
const isFloating = mode === "floating";
|
|
1073
|
-
|
|
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(() => {
|
|
1074
1138
|
const nativeSelection = window.getSelection();
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
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;
|
|
1083
1179
|
}
|
|
1084
|
-
|
|
1180
|
+
setIsVisible(false);
|
|
1181
|
+
dismissedRef.current = editor.getEditorState().read(() => $getSelection()?.clone() ?? null);
|
|
1182
|
+
if (hasFocus(editor.getRootElement())) editor.dispatchCommand(KEY_DOWN_COMMAND, event);
|
|
1183
|
+
};
|
|
1085
1184
|
useEffect(() => {
|
|
1086
1185
|
if (!isFloating) return;
|
|
1087
1186
|
const handleMouseDown = () => {
|
|
@@ -1089,15 +1188,24 @@ const ToolbarPlugin = ({ mode = "floating" }) => {
|
|
|
1089
1188
|
};
|
|
1090
1189
|
const handleMouseUp = () => {
|
|
1091
1190
|
isMouseDownRef.current = false;
|
|
1092
|
-
if (hasSelectionRef.current) setIsVisible(true);
|
|
1191
|
+
if (hasSelectionRef.current && !dismissedRef.current && isFocusWithin()) setIsVisible(true);
|
|
1093
1192
|
};
|
|
1094
1193
|
document.addEventListener("mousedown", handleMouseDown);
|
|
1095
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);
|
|
1096
1199
|
return () => {
|
|
1097
1200
|
document.removeEventListener("mousedown", handleMouseDown);
|
|
1098
1201
|
document.removeEventListener("mouseup", handleMouseUp);
|
|
1202
|
+
unregisterFocus();
|
|
1099
1203
|
};
|
|
1100
|
-
}, [
|
|
1204
|
+
}, [
|
|
1205
|
+
editor,
|
|
1206
|
+
isFloating,
|
|
1207
|
+
isFocusWithin
|
|
1208
|
+
]);
|
|
1101
1209
|
const updateToolbar = useCallback(() => {
|
|
1102
1210
|
const selection = $getSelection();
|
|
1103
1211
|
if (!$isRangeSelection(selection)) {
|
|
@@ -1107,9 +1215,10 @@ const ToolbarPlugin = ({ mode = "floating" }) => {
|
|
|
1107
1215
|
}
|
|
1108
1216
|
const hasSelection = !selection.isCollapsed() && selection.getTextContent().length > 0;
|
|
1109
1217
|
hasSelectionRef.current = hasSelection;
|
|
1218
|
+
if (dismissedRef.current && !selection.is(dismissedRef.current)) dismissedRef.current = null;
|
|
1110
1219
|
if (!hasSelection) setIsVisible(false);
|
|
1111
|
-
else if (!isMouseDownRef.current) setIsVisible(true);
|
|
1112
|
-
if (hasSelection)
|
|
1220
|
+
else if (!isMouseDownRef.current && !dismissedRef.current && isFocusWithin()) setIsVisible(true);
|
|
1221
|
+
if (hasSelection && isFloating) updateAnchor();
|
|
1113
1222
|
setIsBold(selection.hasFormat("bold"));
|
|
1114
1223
|
setIsItalic(selection.hasFormat("italic"));
|
|
1115
1224
|
setIsUnderline(selection.hasFormat("underline"));
|
|
@@ -1127,7 +1236,11 @@ const ToolbarPlugin = ({ mode = "floating" }) => {
|
|
|
1127
1236
|
else if ($isQuoteNode(blockElement)) setBlockType("quote");
|
|
1128
1237
|
else setBlockType("paragraph");
|
|
1129
1238
|
setAlignment(toElementFormatType(blockElement.getFormatType()));
|
|
1130
|
-
}, [
|
|
1239
|
+
}, [
|
|
1240
|
+
isFloating,
|
|
1241
|
+
isFocusWithin,
|
|
1242
|
+
updateAnchor
|
|
1243
|
+
]);
|
|
1131
1244
|
useEffect(() => {
|
|
1132
1245
|
const unregisterUpdate = editor.registerUpdateListener(({ editorState }) => {
|
|
1133
1246
|
editorState.read(() => {
|
|
@@ -1138,7 +1251,6 @@ const ToolbarPlugin = ({ mode = "floating" }) => {
|
|
|
1138
1251
|
unregisterUpdate();
|
|
1139
1252
|
};
|
|
1140
1253
|
}, [editor, updateToolbar]);
|
|
1141
|
-
if (isFloating && (!isVisible || !position)) return null;
|
|
1142
1254
|
const buttons = /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
1143
1255
|
/* @__PURE__ */ jsx(BlockTypeDropdown, {
|
|
1144
1256
|
editor,
|
|
@@ -1148,19 +1260,22 @@ const ToolbarPlugin = ({ mode = "floating" }) => {
|
|
|
1148
1260
|
/* @__PURE__ */ jsx(ToolbarButton, {
|
|
1149
1261
|
active: isBold,
|
|
1150
1262
|
onClick: () => editor.dispatchCommand(FORMAT_TEXT_COMMAND, "bold"),
|
|
1151
|
-
tooltip: "Bold
|
|
1263
|
+
tooltip: "Bold",
|
|
1264
|
+
shortcutKey: "B",
|
|
1152
1265
|
children: /* @__PURE__ */ jsx(Bold, { className: "tw:size-4" })
|
|
1153
1266
|
}),
|
|
1154
1267
|
/* @__PURE__ */ jsx(ToolbarButton, {
|
|
1155
1268
|
active: isItalic,
|
|
1156
1269
|
onClick: () => editor.dispatchCommand(FORMAT_TEXT_COMMAND, "italic"),
|
|
1157
|
-
tooltip: "Italic
|
|
1270
|
+
tooltip: "Italic",
|
|
1271
|
+
shortcutKey: "I",
|
|
1158
1272
|
children: /* @__PURE__ */ jsx(Italic, { className: "tw:size-4" })
|
|
1159
1273
|
}),
|
|
1160
1274
|
/* @__PURE__ */ jsx(ToolbarButton, {
|
|
1161
1275
|
active: isUnderline,
|
|
1162
1276
|
onClick: () => editor.dispatchCommand(FORMAT_TEXT_COMMAND, "underline"),
|
|
1163
|
-
tooltip: "Underline
|
|
1277
|
+
tooltip: "Underline",
|
|
1278
|
+
shortcutKey: "U",
|
|
1164
1279
|
children: /* @__PURE__ */ jsx(Underline, { className: "tw:size-4" })
|
|
1165
1280
|
}),
|
|
1166
1281
|
/* @__PURE__ */ jsx(ToolbarButton, {
|
|
@@ -1241,22 +1356,29 @@ const ToolbarPlugin = ({ mode = "floating" }) => {
|
|
|
1241
1356
|
if (!isFloating) return /* @__PURE__ */ jsx("div", {
|
|
1242
1357
|
role: "toolbar",
|
|
1243
1358
|
"aria-label": "Formatting options",
|
|
1244
|
-
onMouseDown:
|
|
1359
|
+
onMouseDown: keepEditorFocus,
|
|
1245
1360
|
className: cn(TOOLBAR_BASE_CLASS, "tw:mb-1 tw:flex-wrap tw:bg-background"),
|
|
1246
1361
|
children: buttons
|
|
1247
1362
|
});
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
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
|
+
});
|
|
1260
1382
|
};
|
|
1261
1383
|
//#endregion
|
|
1262
1384
|
//#region src/blocks/MarkdownEditor/theme.ts
|