@squiz/formatted-text-editor 1.35.1-alpha.5 → 1.38.0-alpha.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/demo/App.tsx CHANGED
@@ -3,8 +3,20 @@ import { Editor, remirrorNodeToSquizNode, squizNodeToRemirrorNode } from '../src
3
3
  import { RemirrorEventListener, Extension } from '@remirror/core';
4
4
  import ReactDiffViewer from 'react-diff-viewer-continued';
5
5
  import { AppContext } from './AppContext';
6
+ import Button from '../src/ui/Button/Button';
7
+ import TextFieldsOutlinedIcon from '@mui/icons-material/TextFieldsOutlined';
8
+ import DeleteOutlineOutlinedIcon from '@mui/icons-material/DeleteOutlineOutlined';
9
+ import { VerticalDivider } from '@remirror/react-components';
10
+ const ComponentHandlers = () => (
11
+ <div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: '5px' }}>
12
+ <Button icon={<TextFieldsOutlinedIcon />} onClick={(x) => x} />
13
+ <VerticalDivider />
14
+ <Button icon={<DeleteOutlineOutlinedIcon />} onClick={(x) => x} />
15
+ </div>
16
+ );
6
17
 
7
18
  function App() {
19
+ const [showChildren] = useState(false);
8
20
  const [doc, setDoc] = useState('');
9
21
  const [squizDoc, setSquizDoc] = useState('');
10
22
  const [reconvertedDoc, setReconvertedDoc] = useState('');
@@ -41,7 +53,9 @@ function App() {
41
53
  editable={editable}
42
54
  content={`<p>Hello <a href="https://www.google.com"><strong>Mr Bean</strong></a>, nice to <a href="https://www.google.com">meet you</a>.<img src="https://media2.giphy.com/media/3o6ozsIxg5legYvggo/giphy.gif" height="150" width="200"/></p>`}
43
55
  onChange={handleEditorChange}
44
- />
56
+ >
57
+ {showChildren && <ComponentHandlers />}
58
+ </Editor>
45
59
  </AppContext>
46
60
  </div>
47
61
  <h1>Document</h1>
@@ -1,8 +1,11 @@
1
+ import { ReactNode } from 'react';
1
2
  import { RemirrorContentType, RemirrorEventListener, Extension } from '@remirror/core';
2
3
  type EditorProps = {
3
4
  content?: RemirrorContentType;
4
5
  onChange?: RemirrorEventListener<Extension>;
5
6
  editable?: boolean;
7
+ children?: ReactNode;
8
+ isFocused?: boolean;
6
9
  };
7
- declare const Editor: ({ content, editable, onChange }: EditorProps) => JSX.Element;
10
+ declare const Editor: ({ content, editable, onChange, children, isFocused }: EditorProps) => JSX.Element;
8
11
  export default Editor;
@@ -28,10 +28,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
28
28
  Object.defineProperty(exports, "__esModule", { value: true });
29
29
  const react_1 = __importStar(require("react"));
30
30
  const react_2 = require("@remirror/react");
31
+ const clsx_1 = __importDefault(require("clsx"));
31
32
  const EditorToolbar_1 = require("../EditorToolbar");
32
33
  const EditorContext_1 = require("./EditorContext");
33
34
  const Extensions_1 = require("../Extensions/Extensions");
34
- const clsx_1 = __importDefault(require("clsx"));
35
+ const useFocus_1 = __importDefault(require("../hooks/useFocus"));
35
36
  const WrappedEditor = () => {
36
37
  const preventImagePaste = (0, react_1.useCallback)((event) => {
37
38
  const { clipboardData } = event;
@@ -45,7 +46,7 @@ const WrappedEditor = () => {
45
46
  (0, react_2.useEditorEvent)('paste', preventImagePaste);
46
47
  return react_1.default.createElement(react_2.EditorComponent, null);
47
48
  };
48
- const Editor = ({ content, editable = true, onChange }) => {
49
+ const Editor = ({ content, editable = true, onChange, children, isFocused }) => {
49
50
  const { manager, state, setState } = (0, react_2.useRemirror)({
50
51
  extensions: (0, Extensions_1.createExtensions)((0, react_1.useContext)(EditorContext_1.EditorContext)),
51
52
  content,
@@ -56,10 +57,18 @@ const Editor = ({ content, editable = true, onChange }) => {
56
57
  setState(parameter.state);
57
58
  onChange?.(parameter);
58
59
  };
60
+ const { isVisible, handleFocus, handleBlur, wrapperRef } = (0, useFocus_1.default)(isFocused || false);
61
+ // On initial load, check if we need to focus the actual text content
62
+ (0, react_1.useEffect)(() => {
63
+ if (isFocused) {
64
+ manager.view.dom.focus();
65
+ }
66
+ }, []);
59
67
  return (react_1.default.createElement("div", { className: "squiz-fte-scope" },
60
- react_1.default.createElement("div", { className: (0, clsx_1.default)('remirror-theme formatted-text-editor', !editable && 'formatted-text-editor--is-disabled') },
68
+ react_1.default.createElement("div", { ref: wrapperRef, onBlur: handleBlur, onFocusCapture: handleFocus, className: (0, clsx_1.default)('remirror-theme formatted-text-editor', !editable && 'formatted-text-editor--is-disabled') },
61
69
  react_1.default.createElement(react_2.Remirror, { manager: manager, state: state, editable: editable, onChange: handleChange, placeholder: "Write something", label: "Text editor" },
62
- editable && react_1.default.createElement(EditorToolbar_1.Toolbar, null),
70
+ editable && react_1.default.createElement(EditorToolbar_1.Toolbar, { isVisible: isVisible }),
71
+ children,
63
72
  react_1.default.createElement(WrappedEditor, null),
64
73
  editable && react_1.default.createElement(EditorToolbar_1.FloatingToolbar, null)))));
65
74
  };
@@ -71,6 +71,6 @@ const FloatingToolbar = () => {
71
71
  if (extensionNames.clearFormatting && clearFormatting.enabled()) {
72
72
  buttons.push(react_1.default.createElement(ClearFormattingButton_1.default, { key: "clearFormatting" }));
73
73
  }
74
- return (react_1.default.createElement(react_2.FloatingToolbar, { className: "squiz-fte-scope squiz-fte-scope__floating-popover", positioner: positioner }, buttons));
74
+ return (react_1.default.createElement(react_2.FloatingToolbar, { className: "squiz-fte-scope squiz-fte-scope__floating-popover", positioner: positioner, tabIndex: 0, role: 'toolbar' }, buttons));
75
75
  };
76
76
  exports.FloatingToolbar = FloatingToolbar;
@@ -1 +1,5 @@
1
- export declare const Toolbar: () => JSX.Element;
1
+ type ToolbarProps = {
2
+ isVisible: boolean;
3
+ };
4
+ export declare const Toolbar: ({ isVisible }: ToolbarProps) => JSX.Element;
5
+ export {};
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.Toolbar = void 0;
7
7
  const react_1 = __importDefault(require("react"));
8
8
  const react_components_1 = require("@remirror/react-components");
9
+ const clsx_1 = __importDefault(require("clsx"));
9
10
  const ItalicButton_1 = __importDefault(require("./Tools/Italic/ItalicButton"));
10
11
  const UnderlineButton_1 = __importDefault(require("./Tools/Underline/UnderlineButton"));
11
12
  const BoldButton_1 = __importDefault(require("./Tools/Bold/BoldButton"));
@@ -13,14 +14,14 @@ const TextAlignButtons_1 = __importDefault(require("./Tools/TextAlign/TextAlignB
13
14
  const UndoButton_1 = __importDefault(require("./Tools/Undo/UndoButton"));
14
15
  const RedoButton_1 = __importDefault(require("./Tools/Redo/RedoButton"));
15
16
  const TextTypeDropdown_1 = __importDefault(require("./Tools/TextType/TextTypeDropdown"));
16
- const hooks_1 = require("../hooks");
17
17
  const LinkButton_1 = __importDefault(require("./Tools/Link/LinkButton"));
18
18
  const ImageButton_1 = __importDefault(require("./Tools/Image/ImageButton"));
19
19
  const RemoveLinkButton_1 = __importDefault(require("./Tools/Link/RemoveLinkButton"));
20
20
  const ClearFormattingButton_1 = __importDefault(require("./Tools/ClearFormatting/ClearFormattingButton"));
21
- const Toolbar = () => {
21
+ const hooks_1 = require("../hooks");
22
+ const Toolbar = ({ isVisible }) => {
22
23
  const extensionNames = (0, hooks_1.useExtensionNames)();
23
- return (react_1.default.createElement(react_components_1.Toolbar, { className: "remirror-toolbar editor-toolbar" },
24
+ return (react_1.default.createElement(react_components_1.Toolbar, { className: (0, clsx_1.default)('remirror-toolbar editor-toolbar header-toolbar', isVisible && 'show-toolbar'), role: "toolbar", tabIndex: 0 },
24
25
  extensionNames.history && (react_1.default.createElement(react_1.default.Fragment, null,
25
26
  react_1.default.createElement(UndoButton_1.default, null),
26
27
  react_1.default.createElement(RedoButton_1.default, null),
@@ -24,7 +24,7 @@ let UnsupportedNodeExtension = class UnsupportedNodeExtension extends core_1.Nod
24
24
  createNodeSpec(extra, override) {
25
25
  return {
26
26
  selectable: false,
27
- draggable: true,
27
+ draggable: false,
28
28
  atom: true,
29
29
  inline: true,
30
30
  ...override,
@@ -0,0 +1,8 @@
1
+ import { FocusEventHandler, RefObject } from 'react';
2
+ declare const useFocus: (initialState: boolean) => {
3
+ handleFocus: () => void;
4
+ handleBlur: FocusEventHandler<HTMLDivElement>;
5
+ isVisible: boolean;
6
+ wrapperRef: RefObject<HTMLDivElement>;
7
+ };
8
+ export default useFocus;
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const react_1 = require("react");
4
+ const useFocus = (initialState) => {
5
+ const wrapperRef = (0, react_1.createRef)();
6
+ const [isVisible, setIsVisible] = (0, react_1.useState)(initialState);
7
+ const handleFocus = (0, react_1.useCallback)(() => {
8
+ setIsVisible(true);
9
+ }, []);
10
+ const handleBlur = (event) => {
11
+ const isOutside = wrapperRef.current !== null && !wrapperRef.current.contains(event.relatedTarget);
12
+ if (isOutside) {
13
+ setIsVisible(false);
14
+ }
15
+ };
16
+ return { handleFocus, handleBlur, isVisible, wrapperRef };
17
+ };
18
+ exports.default = useFocus;
package/lib/index.css CHANGED
@@ -638,71 +638,71 @@
638
638
  .squiz-fte-scope .filter {
639
639
  filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow) !important;
640
640
  }
641
- .squiz-fte-scope a {
641
+ .squiz-fte-scope .remirror-editor a {
642
642
  --tw-text-opacity: 1;
643
643
  color: rgb(7 116 210 / var(--tw-text-opacity));
644
644
  text-decoration: underline;
645
645
  }
646
- .squiz-fte-scope h1 {
646
+ .squiz-fte-scope .remirror-editor h1 {
647
647
  font-size: 1.625rem;
648
648
  font-weight: 600;
649
649
  letter-spacing: -0.2px;
650
650
  line-height: 2rem;
651
651
  }
652
- .squiz-fte-scope h2 {
652
+ .squiz-fte-scope .remirror-editor h2 {
653
653
  font-size: 1.25rem;
654
654
  font-weight: 600;
655
655
  letter-spacing: -0.5px;
656
656
  line-height: 1.5rem;
657
657
  }
658
- .squiz-fte-scope h3 {
658
+ .squiz-fte-scope .remirror-editor h3 {
659
659
  font-size: 1.125rem;
660
660
  font-weight: 600;
661
661
  letter-spacing: -0.2px;
662
662
  line-height: 1.375rem;
663
663
  }
664
- .squiz-fte-scope h4 {
664
+ .squiz-fte-scope .remirror-editor h4 {
665
665
  font-size: 1rem;
666
666
  font-weight: 700;
667
667
  letter-spacing: -0.2px;
668
668
  line-height: 1.25rem;
669
669
  }
670
- .squiz-fte-scope h5 {
670
+ .squiz-fte-scope .remirror-editor h5 {
671
671
  font-size: 1rem;
672
672
  font-weight: 600;
673
673
  letter-spacing: -0.2px;
674
674
  line-height: 1.25rem;
675
675
  }
676
- .squiz-fte-scope h6 {
676
+ .squiz-fte-scope .remirror-editor h6 {
677
677
  font-size: 0.875rem;
678
678
  font-weight: 600;
679
679
  letter-spacing: -0.2px;
680
680
  line-height: 1.25rem;
681
681
  }
682
- .squiz-fte-scope .code-block,
683
- .squiz-fte-scope .preformatted {
682
+ .squiz-fte-scope .remirror-editor .code-block,
683
+ .squiz-fte-scope .remirror-editor .preformatted {
684
684
  display: flex;
685
685
  }
686
- .squiz-fte-scope .code-block .material-symbols-rounded,
687
- .squiz-fte-scope .preformatted .material-symbols-rounded {
686
+ .squiz-fte-scope .remirror-editor .code-block .material-symbols-rounded,
687
+ .squiz-fte-scope .remirror-editor .preformatted .material-symbols-rounded {
688
688
  font-size: 1.125rem;
689
689
  --tw-text-opacity: 1;
690
690
  color: rgb(79 79 79 / var(--tw-text-opacity));
691
691
  pointer-events: none;
692
692
  margin-right: 0.375rem;
693
693
  }
694
- .squiz-fte-scope .code-block .block-divider,
695
- .squiz-fte-scope .preformatted .block-divider {
694
+ .squiz-fte-scope .remirror-editor .code-block .block-divider,
695
+ .squiz-fte-scope .remirror-editor .preformatted .block-divider {
696
696
  --tw-bg-opacity: 1;
697
697
  background-color: rgb(237 237 237 / var(--tw-bg-opacity));
698
698
  width: 4px;
699
699
  border-radius: 0.75rem;
700
700
  margin-right: 0.625rem;
701
701
  }
702
- .squiz-fte-scope .code-block code,
703
- .squiz-fte-scope .code-block pre,
704
- .squiz-fte-scope .preformatted code,
705
- .squiz-fte-scope .preformatted pre {
702
+ .squiz-fte-scope .remirror-editor .code-block code,
703
+ .squiz-fte-scope .remirror-editor .code-block pre,
704
+ .squiz-fte-scope .remirror-editor .preformatted code,
705
+ .squiz-fte-scope .remirror-editor .preformatted pre {
706
706
  --tw-text-opacity: 1;
707
707
  color: rgb(79 79 79 / var(--tw-text-opacity));
708
708
  font-size: 0.75rem;
@@ -835,8 +835,19 @@
835
835
  .squiz-fte-scope .formatted-text-editor img {
836
836
  display: inline;
837
837
  }
838
+ .squiz-fte-scope .formatted-text-editor:has(.show-toolbar) {
839
+ border-width: 0px;
840
+ --tw-shadow: 0 0 0 1px rgba(0,0,0,0.04), 0 1px 12px 4px rgba(0,0,0,0.12);
841
+ --tw-shadow-colored: 0 0 0 1px var(--tw-shadow-color), 0 1px 12px 4px var(--tw-shadow-color);
842
+ box-shadow:
843
+ var(--tw-ring-offset-shadow, 0 0 #0000),
844
+ var(--tw-ring-shadow, 0 0 #0000),
845
+ var(--tw-shadow);
846
+ }
838
847
  .squiz-fte-scope .editor-toolbar,
839
848
  .squiz-fte-scope__floating-popover {
849
+ border-top-left-radius: 4px;
850
+ border-top-right-radius: 4px;
840
851
  border-bottom-width: 2px;
841
852
  border-style: solid;
842
853
  --tw-border-opacity: 1;
@@ -846,6 +857,7 @@
846
857
  padding: 0.25rem;
847
858
  display: flex;
848
859
  justify-items: center;
860
+ flex-wrap: wrap;
849
861
  }
850
862
  .squiz-fte-scope .editor-toolbar > *:not(:first-child, .MuiDivider-root),
851
863
  .squiz-fte-scope__floating-popover > *:not(:first-child, .MuiDivider-root) {
@@ -853,8 +865,8 @@
853
865
  }
854
866
  .squiz-fte-scope .editor-toolbar .MuiDivider-root,
855
867
  .squiz-fte-scope__floating-popover .MuiDivider-root {
856
- margin-top: -0.25rem;
857
- margin-bottom: -0.25rem;
868
+ margin-top: 0px;
869
+ margin-bottom: 0px;
858
870
  margin-left: 0.25rem;
859
871
  margin-right: 0.25rem;
860
872
  border-width: 1px;
@@ -870,6 +882,15 @@
870
882
  .squiz-fte-scope__floating-popover .squiz-fte-btn ~ .squiz-fte-btn {
871
883
  margin-left: 2px;
872
884
  }
885
+ .squiz-fte-scope .header-toolbar {
886
+ transition: opacity 0.2s linear, max-height 0.2s linear;
887
+ opacity: 0;
888
+ max-height: 0;
889
+ }
890
+ .squiz-fte-scope .header-toolbar.show-toolbar {
891
+ opacity: 1;
892
+ max-height: 15vh;
893
+ }
873
894
  .squiz-fte-scope__floating-popover {
874
895
  display: flex;
875
896
  border-radius: 6px;
@@ -890,7 +911,7 @@
890
911
  margin-top: 0px;
891
912
  margin-bottom: 0px;
892
913
  }
893
- .squiz-fte-scope .squiz-fte-btn {
914
+ .squiz-fte-scope button.squiz-fte-btn {
894
915
  border-radius: 4px;
895
916
  --tw-bg-opacity: 1;
896
917
  background-color: rgb(255 255 255 / var(--tw-bg-opacity));
@@ -912,23 +933,23 @@
912
933
  border: 1px solid transparent;
913
934
  padding: 6px 12px;
914
935
  }
915
- .squiz-fte-scope .squiz-fte-btn--is-icon {
936
+ .squiz-fte-scope button.squiz-fte-btn--is-icon {
916
937
  padding: 6px;
917
938
  }
918
- .squiz-fte-scope .squiz-fte-btn ~ .squiz-fte-btn {
939
+ .squiz-fte-scope button.squiz-fte-btn ~ .squiz-fte-btn {
919
940
  margin-left: 2px;
920
941
  }
921
- .squiz-fte-scope .squiz-fte-btn.disabled,
922
- .squiz-fte-scope .squiz-fte-btn[disabled] {
942
+ .squiz-fte-scope button.squiz-fte-btn.disabled,
943
+ .squiz-fte-scope button.squiz-fte-btn[disabled] {
923
944
  cursor: not-allowed;
924
945
  opacity: 0.5;
925
946
  }
926
- .squiz-fte-scope .squiz-fte-btn:hover,
927
- .squiz-fte-scope .squiz-fte-btn:focus {
947
+ .squiz-fte-scope button.squiz-fte-btn:hover,
948
+ .squiz-fte-scope button.squiz-fte-btn:focus {
928
949
  background-color: rgba(0, 0, 0, 0.04);
929
950
  }
930
- .squiz-fte-scope .squiz-fte-btn--is-active,
931
- .squiz-fte-scope .squiz-fte-btn:active {
951
+ .squiz-fte-scope button.squiz-fte-btn--is-active,
952
+ .squiz-fte-scope button.squiz-fte-btn:active {
932
953
  --tw-bg-opacity: 1;
933
954
  background-color: rgb(230 241 250 / var(--tw-bg-opacity));
934
955
  --tw-text-opacity: 1;
@@ -1041,6 +1062,7 @@
1041
1062
  .squiz-fte-scope .collapse-box__label {
1042
1063
  margin-left: 0.5rem;
1043
1064
  margin-right: auto;
1065
+ text-align: left;
1044
1066
  }
1045
1067
  .squiz-fte-scope .collapse-box__icon {
1046
1068
  --tw-text-opacity: 1;
@@ -1126,7 +1148,7 @@
1126
1148
  .squiz-fte-scope__floating-popover .squiz-fte-modal-footer__button ~ .squiz-fte-btn {
1127
1149
  margin-left: 2px;
1128
1150
  }
1129
- .squiz-fte-scope .squiz-fte-modal-footer__button {
1151
+ .squiz-fte-scope button.squiz-fte-modal-footer__button {
1130
1152
  border-radius: 4px;
1131
1153
  --tw-bg-opacity: 1;
1132
1154
  background-color: rgb(255 255 255 / var(--tw-bg-opacity));
@@ -1148,19 +1170,19 @@
1148
1170
  border: 1px solid transparent;
1149
1171
  padding: 6px 12px;
1150
1172
  }
1151
- .squiz-fte-scope .squiz-fte-modal-footer__button ~ .squiz-fte-btn {
1173
+ .squiz-fte-scope button.squiz-fte-modal-footer__button ~ .squiz-fte-btn {
1152
1174
  margin-left: 2px;
1153
1175
  }
1154
- .squiz-fte-scope .squiz-fte-modal-footer__button.disabled,
1155
- .squiz-fte-scope .squiz-fte-modal-footer__button[disabled] {
1176
+ .squiz-fte-scope button.squiz-fte-modal-footer__button.disabled,
1177
+ .squiz-fte-scope button.squiz-fte-modal-footer__button[disabled] {
1156
1178
  cursor: not-allowed;
1157
1179
  opacity: 0.5;
1158
1180
  }
1159
- .squiz-fte-scope .squiz-fte-modal-footer__button:hover,
1160
- .squiz-fte-scope .squiz-fte-modal-footer__button:focus {
1181
+ .squiz-fte-scope button.squiz-fte-modal-footer__button:hover,
1182
+ .squiz-fte-scope button.squiz-fte-modal-footer__button:focus {
1161
1183
  background-color: rgba(0, 0, 0, 0.04);
1162
1184
  }
1163
- .squiz-fte-scope .squiz-fte-modal-footer__button:active {
1185
+ .squiz-fte-scope button.squiz-fte-modal-footer__button:active {
1164
1186
  --tw-bg-opacity: 1;
1165
1187
  background-color: rgb(230 241 250 / var(--tw-bg-opacity));
1166
1188
  --tw-text-opacity: 1;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@squiz/formatted-text-editor",
3
- "version": "1.35.1-alpha.5",
3
+ "version": "1.38.0-alpha.2",
4
4
  "main": "lib/index.js",
5
5
  "types": "lib/index.d.ts",
6
6
  "scripts": {
@@ -20,8 +20,8 @@
20
20
  "@headlessui/react": "1.7.11",
21
21
  "@mui/icons-material": "5.11.16",
22
22
  "@remirror/react": "2.0.25",
23
- "@squiz/dx-json-schema-lib": "1.35.1-alpha.5",
24
- "@squiz/resource-browser": "1.35.1-alpha.5",
23
+ "@squiz/dx-json-schema-lib": "1.38.0-alpha.2",
24
+ "@squiz/resource-browser": "1.38.0-alpha.2",
25
25
  "clsx": "1.2.1",
26
26
  "react-hook-form": "7.43.2",
27
27
  "react-image-size": "2.0.0",
@@ -75,5 +75,5 @@
75
75
  "volta": {
76
76
  "node": "18.15.0"
77
77
  },
78
- "gitHead": "6c976023e77f7cceab962b5563fcaf669aedaa6d"
78
+ "gitHead": "9c2f775a7d6607a73e5b25577d82be939d74ffec"
79
79
  }
@@ -5,7 +5,23 @@ import '@testing-library/jest-dom';
5
5
  import { renderWithEditor } from '../../tests';
6
6
  import ImageButton from '../EditorToolbar/Tools/Image/ImageButton';
7
7
 
8
+ const isVisible = jest.fn().mockReturnValue(false);
9
+ const handleFocusMock = jest.fn();
10
+ const handleBlurMock = jest.fn();
11
+ jest.mock('../hooks/useFocus', () => ({
12
+ __esModule: true,
13
+ default: () => ({
14
+ isVisible: isVisible(),
15
+ handleFocus: handleFocusMock,
16
+ handleBlur: handleBlurMock,
17
+ }),
18
+ }));
19
+
8
20
  describe('Formatted text editor', () => {
21
+ afterEach(() => {
22
+ jest.clearAllMocks();
23
+ });
24
+
9
25
  it('Renders the text editor', () => {
10
26
  render(<Editor />);
11
27
  expect(screen.getByRole('textbox')).toBeInTheDocument();
@@ -311,4 +327,31 @@ describe('Formatted text editor', () => {
311
327
 
312
328
  expect(buttonLabels).toEqual([]);
313
329
  });
330
+
331
+ it('triggers handleFocus when editor is focused', async () => {
332
+ const { getByLabelText } = render(<Editor />);
333
+ const editorInput = getByLabelText('Text editor');
334
+
335
+ fireEvent.focus(editorInput);
336
+
337
+ expect(handleBlurMock).toHaveBeenCalledTimes(0);
338
+ expect(handleFocusMock).toHaveBeenCalledTimes(1);
339
+ });
340
+
341
+ it('triggers handleBlur when editor is blurred', () => {
342
+ const { getByLabelText } = render(<Editor />);
343
+ const editorInput = getByLabelText('Text editor');
344
+
345
+ fireEvent.blur(editorInput);
346
+
347
+ expect(handleBlurMock).toHaveBeenCalledTimes(1);
348
+ expect(handleFocusMock).toHaveBeenCalledTimes(0);
349
+ });
350
+
351
+ it('should apply hide class when focus hook returns false', () => {
352
+ isVisible.mockReturnValue(true);
353
+ const { container } = render(<Editor />);
354
+
355
+ expect(container.querySelector('.show-toolbar')).toBeInTheDocument();
356
+ });
314
357
  });
@@ -1,16 +1,19 @@
1
- import React, { useContext, useCallback } from 'react';
1
+ import React, { useContext, useCallback, ReactNode, useEffect } from 'react';
2
2
  import { EditorComponent, Remirror, useRemirror, useEditorEvent } from '@remirror/react';
3
3
  import { RemirrorContentType, RemirrorEventListener, Extension } from '@remirror/core';
4
+ import { ClipboardEventHandler } from '@remirror/extension-events/dist-types/events-extension';
5
+ import clsx from 'clsx';
4
6
  import { Toolbar, FloatingToolbar } from '../EditorToolbar';
5
7
  import { EditorContext } from './EditorContext';
6
8
  import { createExtensions } from '../Extensions/Extensions';
7
- import clsx from 'clsx';
8
- import { ClipboardEventHandler } from '@remirror/extension-events/dist-types/events-extension';
9
+ import useFocus from '../hooks/useFocus';
9
10
 
10
11
  type EditorProps = {
11
12
  content?: RemirrorContentType;
12
13
  onChange?: RemirrorEventListener<Extension>;
13
14
  editable?: boolean;
15
+ children?: ReactNode;
16
+ isFocused?: boolean;
14
17
  };
15
18
 
16
19
  const WrappedEditor = () => {
@@ -29,7 +32,7 @@ const WrappedEditor = () => {
29
32
  return <EditorComponent />;
30
33
  };
31
34
 
32
- const Editor = ({ content, editable = true, onChange }: EditorProps) => {
35
+ const Editor = ({ content, editable = true, onChange, children, isFocused }: EditorProps) => {
33
36
  const { manager, state, setState } = useRemirror({
34
37
  extensions: createExtensions(useContext(EditorContext)),
35
38
  content,
@@ -42,9 +45,23 @@ const Editor = ({ content, editable = true, onChange }: EditorProps) => {
42
45
  onChange?.(parameter);
43
46
  };
44
47
 
48
+ const { isVisible, handleFocus, handleBlur, wrapperRef } = useFocus(isFocused || false);
49
+
50
+ // On initial load, check if we need to focus the actual text content
51
+ useEffect(() => {
52
+ if (isFocused) {
53
+ manager.view.dom.focus();
54
+ }
55
+ }, []);
56
+
45
57
  return (
46
58
  <div className="squiz-fte-scope">
47
- <div className={clsx('remirror-theme formatted-text-editor', !editable && 'formatted-text-editor--is-disabled')}>
59
+ <div
60
+ ref={wrapperRef}
61
+ onBlur={handleBlur}
62
+ onFocusCapture={handleFocus}
63
+ className={clsx('remirror-theme formatted-text-editor', !editable && 'formatted-text-editor--is-disabled')}
64
+ >
48
65
  <Remirror
49
66
  manager={manager}
50
67
  state={state}
@@ -53,7 +70,8 @@ const Editor = ({ content, editable = true, onChange }: EditorProps) => {
53
70
  placeholder="Write something"
54
71
  label="Text editor"
55
72
  >
56
- {editable && <Toolbar />}
73
+ {editable && <Toolbar isVisible={isVisible} />}
74
+ {children}
57
75
  <WrappedEditor />
58
76
  {editable && <FloatingToolbar />}
59
77
  </Remirror>
@@ -48,4 +48,8 @@
48
48
  img {
49
49
  display: inline;
50
50
  }
51
+
52
+ &:has(.show-toolbar) {
53
+ @apply shadow-md border-0;
54
+ }
51
55
  }
@@ -50,7 +50,12 @@ export const FloatingToolbar = () => {
50
50
  }
51
51
 
52
52
  return (
53
- <RemirrorFloatingToolbar className="squiz-fte-scope squiz-fte-scope__floating-popover" positioner={positioner}>
53
+ <RemirrorFloatingToolbar
54
+ className="squiz-fte-scope squiz-fte-scope__floating-popover"
55
+ positioner={positioner}
56
+ tabIndex={0}
57
+ role={'toolbar'}
58
+ >
54
59
  {buttons}
55
60
  </RemirrorFloatingToolbar>
56
61
  );
@@ -1,5 +1,6 @@
1
1
  import React from 'react';
2
2
  import { Toolbar as RemirrorToolbar, VerticalDivider } from '@remirror/react-components';
3
+ import clsx from 'clsx';
3
4
  import ItalicButton from './Tools/Italic/ItalicButton';
4
5
  import UnderlineButton from './Tools/Underline/UnderlineButton';
5
6
  import BoldButton from './Tools/Bold/BoldButton';
@@ -7,17 +8,24 @@ import TextAlignButtons from './Tools/TextAlign/TextAlignButtons';
7
8
  import UndoButton from './Tools/Undo/UndoButton';
8
9
  import RedoButton from './Tools/Redo/RedoButton';
9
10
  import TextTypeDropdown from './Tools/TextType/TextTypeDropdown';
10
- import { useExtensionNames } from '../hooks';
11
11
  import LinkButton from './Tools/Link/LinkButton';
12
12
  import ImageButton from './Tools/Image/ImageButton';
13
13
  import RemoveLinkButton from './Tools/Link/RemoveLinkButton';
14
14
  import ClearFormattingButton from './Tools/ClearFormatting/ClearFormattingButton';
15
+ import { useExtensionNames } from '../hooks';
15
16
 
16
- export const Toolbar = () => {
17
+ type ToolbarProps = {
18
+ isVisible: boolean;
19
+ };
20
+ export const Toolbar = ({ isVisible }: ToolbarProps) => {
17
21
  const extensionNames = useExtensionNames();
18
22
 
19
23
  return (
20
- <RemirrorToolbar className="remirror-toolbar editor-toolbar">
24
+ <RemirrorToolbar
25
+ className={clsx('remirror-toolbar editor-toolbar header-toolbar', isVisible && 'show-toolbar')}
26
+ role="toolbar"
27
+ tabIndex={0}
28
+ >
21
29
  {extensionNames.history && (
22
30
  <>
23
31
  <UndoButton />
@@ -1,18 +1,20 @@
1
1
  .editor-toolbar {
2
- @apply bg-white border-gray-200 border-b-2 border-solid p-1;
2
+ @apply bg-white border-gray-200 border-b-2 border-solid p-1 rounded-t;
3
3
 
4
4
  display: flex;
5
5
  justify-items: center;
6
+ flex-wrap: wrap;
6
7
 
7
8
  > *:not(:first-child, .MuiDivider-root) {
8
9
  margin: 0 0 0 2px;
9
10
  }
10
11
 
11
12
  .MuiDivider-root {
12
- @apply -my-1 mx-1 border;
13
+ @apply my-0 mx-1 border;
13
14
  margin-right: 2px;
14
15
  height: auto;
15
16
  }
17
+
16
18
  .squiz-fte-btn {
17
19
  @apply p-1 font-bold;
18
20
  ~ .squiz-fte-btn {
@@ -20,3 +22,14 @@
20
22
  }
21
23
  }
22
24
  }
25
+
26
+ .header-toolbar {
27
+ transition: opacity 0.2s linear, max-height 0.2s linear;
28
+ opacity: 0;
29
+ max-height: 0;
30
+
31
+ &.show-toolbar {
32
+ opacity: 1;
33
+ max-height: 15vh;
34
+ }
35
+ }
@@ -35,7 +35,7 @@ describe('UnsupportedNodeExtension', () => {
35
35
  const nodeSpec = extension.createNodeSpec(extra, override);
36
36
 
37
37
  expect(nodeSpec.selectable).toBe(true);
38
- expect(nodeSpec.draggable).toBe(true);
38
+ expect(nodeSpec.draggable).toBe(false);
39
39
  expect(nodeSpec.atom).toBe(true);
40
40
  expect(nodeSpec.inline).toBe(true);
41
41
  expect(nodeSpec.attrs).toEqual({ originalNode: {}, errorMessage: {} });
@@ -131,7 +131,7 @@ describe('UnsupportedNodeExtension', () => {
131
131
  });
132
132
 
133
133
  expect(getHtmlContent()).toBe(
134
- '<span class="unsupported-node-node-view-wrapper" originalnode="null" errormessage="null" data-unsupported-node="{&quot;originalNode&quot;:null,&quot;errorMessage&quot;:null}" draggable="true"><div class="collapse-box" contenteditable="false"><button class="collapse-box__header" type="button"><svg class="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium collapse-box__icon--warning css-i4bv87-MuiSvgIcon-root" focusable="false" aria-hidden="true" viewBox="0 0 24 24" data-testid="MotionPhotosOffOutlinedIcon"><path d="M2.81 2.81 1.39 4.22l2.27 2.27C2.61 8.07 2 9.96 2 12c0 5.52 4.48 10 10 10 2.04 0 3.93-.61 5.51-1.66l2.27 2.27 1.41-1.42L2.81 2.81zM12 20c-4.41 0-8-3.59-8-8 0-1.48.41-2.86 1.12-4.06l10.93 10.94C14.86 19.59 13.48 20 12 20zm0-16c4.41 0 8 3.59 8 8 0 1.48-.41 2.86-1.12 4.05l1.45 1.45C21.39 15.93 22 14.04 22 12c0-5.52-4.48-10-10-10-2.04 0-3.93.61-5.51 1.66l1.45 1.45C9.14 4.41 10.52 4 12 4z"></path></svg><div class="collapse-box__label">This section cannot be displayed here due to unsupported HTML elements. The front-end view of your page won’t be affected.</div><svg class="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium css-i4bv87-MuiSvgIcon-root" focusable="false" aria-hidden="true" viewBox="0 0 24 24" data-testid="UnfoldLessOutlinedIcon"><path d="M7.41 18.59 8.83 20 12 16.83 15.17 20l1.41-1.41L12 14l-4.59 4.59zm9.18-13.18L15.17 4 12 7.17 8.83 4 7.41 5.41 12 10l4.59-4.59z"></path></svg></button><div class="collapse-box__content" hidden="" data-testid="content"><br>null</div></div></span>',
134
+ '<span class="unsupported-node-node-view-wrapper" originalnode="null" errormessage="null" data-unsupported-node="{&quot;originalNode&quot;:null,&quot;errorMessage&quot;:null}"><div class="collapse-box" contenteditable="false"><button class="collapse-box__header" type="button"><svg class="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium collapse-box__icon--warning css-i4bv87-MuiSvgIcon-root" focusable="false" aria-hidden="true" viewBox="0 0 24 24" data-testid="MotionPhotosOffOutlinedIcon"><path d="M2.81 2.81 1.39 4.22l2.27 2.27C2.61 8.07 2 9.96 2 12c0 5.52 4.48 10 10 10 2.04 0 3.93-.61 5.51-1.66l2.27 2.27 1.41-1.42L2.81 2.81zM12 20c-4.41 0-8-3.59-8-8 0-1.48.41-2.86 1.12-4.06l10.93 10.94C14.86 19.59 13.48 20 12 20zm0-16c4.41 0 8 3.59 8 8 0 1.48-.41 2.86-1.12 4.05l1.45 1.45C21.39 15.93 22 14.04 22 12c0-5.52-4.48-10-10-10-2.04 0-3.93.61-5.51 1.66l1.45 1.45C9.14 4.41 10.52 4 12 4z"></path></svg><div class="collapse-box__label">This section cannot be displayed here due to unsupported HTML elements. The front-end view of your page won’t be affected.</div><svg class="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium css-i4bv87-MuiSvgIcon-root" focusable="false" aria-hidden="true" viewBox="0 0 24 24" data-testid="UnfoldLessOutlinedIcon"><path d="M7.41 18.59 8.83 20 12 16.83 15.17 20l1.41-1.41L12 14l-4.59 4.59zm9.18-13.18L15.17 4 12 7.17 8.83 4 7.41 5.41 12 10l4.59-4.59z"></path></svg></button><div class="collapse-box__content" hidden="" data-testid="content"><br>null</div></div></span>',
135
135
  );
136
136
  });
137
137
  });
@@ -30,7 +30,7 @@ export class UnsupportedNodeExtension extends NodeExtension {
30
30
  createNodeSpec(extra: ApplySchemaAttributes, override: NodeSpecOverride): NodeExtensionSpec {
31
31
  return {
32
32
  selectable: false,
33
- draggable: true,
33
+ draggable: false,
34
34
  atom: true,
35
35
  inline: true,
36
36
  ...override,
@@ -0,0 +1,28 @@
1
+ import { useState, useCallback, FocusEvent, FocusEventHandler, RefObject, createRef } from 'react';
2
+
3
+ const useFocus = (
4
+ initialState: boolean,
5
+ ): {
6
+ handleFocus: () => void;
7
+ handleBlur: FocusEventHandler<HTMLDivElement>;
8
+ isVisible: boolean;
9
+ wrapperRef: RefObject<HTMLDivElement>;
10
+ } => {
11
+ const wrapperRef = createRef<HTMLDivElement>();
12
+ const [isVisible, setIsVisible] = useState(initialState);
13
+
14
+ const handleFocus = useCallback(() => {
15
+ setIsVisible(true);
16
+ }, []);
17
+
18
+ const handleBlur: FocusEventHandler<HTMLDivElement> = (event: FocusEvent<HTMLDivElement>) => {
19
+ const isOutside = wrapperRef.current !== null && !wrapperRef.current.contains(event.relatedTarget as Node);
20
+ if (isOutside) {
21
+ setIsVisible(false);
22
+ }
23
+ };
24
+
25
+ return { handleFocus, handleBlur, isVisible, wrapperRef };
26
+ };
27
+
28
+ export default useFocus;
@@ -1,4 +1,4 @@
1
- .squiz-fte-btn {
1
+ button.squiz-fte-btn {
2
2
  @apply font-bold rounded ease-linear transition-all bg-white text-gray-600 duration-150;
3
3
  display: flex;
4
4
  align-items: center;
@@ -6,7 +6,7 @@
6
6
  }
7
7
 
8
8
  &__label {
9
- @apply ml-2 mr-auto;
9
+ @apply ml-2 mr-auto text-left;
10
10
  }
11
11
 
12
12
  &__icon {
@@ -1,72 +1,74 @@
1
- a {
2
- @apply text-blue-300;
3
- text-decoration: underline;
4
- }
1
+ .remirror-editor {
2
+ a {
3
+ @apply text-blue-300;
4
+ text-decoration: underline;
5
+ }
5
6
 
6
- h1 {
7
- font-size: 1.625rem;
8
- font-weight: 600;
9
- letter-spacing: -0.2px;
10
- line-height: 2rem;
11
- }
7
+ h1 {
8
+ font-size: 1.625rem;
9
+ font-weight: 600;
10
+ letter-spacing: -0.2px;
11
+ line-height: 2rem;
12
+ }
12
13
 
13
- h2 {
14
- font-size: 1.25rem;
15
- font-weight: 600;
16
- letter-spacing: -0.5px;
17
- line-height: 1.5rem;
18
- }
14
+ h2 {
15
+ font-size: 1.25rem;
16
+ font-weight: 600;
17
+ letter-spacing: -0.5px;
18
+ line-height: 1.5rem;
19
+ }
19
20
 
20
- h3 {
21
- font-size: 1.125rem;
22
- font-weight: 600;
23
- letter-spacing: -0.2px;
24
- line-height: 1.375rem;
25
- }
21
+ h3 {
22
+ font-size: 1.125rem;
23
+ font-weight: 600;
24
+ letter-spacing: -0.2px;
25
+ line-height: 1.375rem;
26
+ }
26
27
 
27
- h4 {
28
- font-size: 1rem;
29
- font-weight: 700;
30
- letter-spacing: -0.2px;
31
- line-height: 1.25rem;
32
- }
28
+ h4 {
29
+ font-size: 1rem;
30
+ font-weight: 700;
31
+ letter-spacing: -0.2px;
32
+ line-height: 1.25rem;
33
+ }
33
34
 
34
- h5 {
35
- font-size: 1rem;
36
- font-weight: 600;
37
- letter-spacing: -0.2px;
38
- line-height: 1.25rem;
39
- }
35
+ h5 {
36
+ font-size: 1rem;
37
+ font-weight: 600;
38
+ letter-spacing: -0.2px;
39
+ line-height: 1.25rem;
40
+ }
40
41
 
41
- h6 {
42
- font-size: 0.875rem;
43
- font-weight: 600;
44
- letter-spacing: -0.2px;
45
- line-height: 1.25rem;
46
- }
42
+ h6 {
43
+ font-size: 0.875rem;
44
+ font-weight: 600;
45
+ letter-spacing: -0.2px;
46
+ line-height: 1.25rem;
47
+ }
47
48
 
48
- .code-block,
49
- .preformatted {
50
- display: flex;
49
+ .code-block,
50
+ .preformatted {
51
+ display: flex;
51
52
 
52
- .material-symbols-rounded {
53
- @apply text-gray-700 text-xlg;
54
- pointer-events: none;
55
- margin-right: 0.375rem;
56
- }
53
+ .material-symbols-rounded {
54
+ @apply text-gray-700 text-xlg;
55
+ pointer-events: none;
56
+ margin-right: 0.375rem;
57
+ }
57
58
 
58
- .block-divider {
59
- @apply bg-gray-200;
60
- width: 4px;
61
- border-radius: 0.75rem;
62
- margin-right: 0.625rem;
63
- }
59
+ .block-divider {
60
+ @apply bg-gray-200;
61
+ width: 4px;
62
+ border-radius: 0.75rem;
63
+ margin-right: 0.625rem;
64
+ }
64
65
 
65
- code,
66
- pre {
67
- @apply text-gray-700;
68
- font-size: 0.75rem;
69
- padding-top: 0.75rem;
70
- padding-bottom: 0.75rem;
66
+ code,
67
+ pre {
68
+ @apply text-gray-700;
69
+ font-size: 0.75rem;
70
+ padding-top: 0.75rem;
71
+ padding-bottom: 0.75rem;
72
+ }
71
73
  }
72
74
  }