@squiz/formatted-text-editor 1.12.0-alpha.8 → 1.12.1-alpha.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.
Files changed (163) hide show
  1. package/.eslintrc.json +34 -0
  2. package/CHANGELOG.md +48 -0
  3. package/README.md +2 -3
  4. package/build.js +21 -0
  5. package/cypress/e2e/bold.spec.cy.ts +18 -0
  6. package/cypress/global.d.ts +9 -0
  7. package/cypress/support/commands.ts +130 -0
  8. package/cypress/support/e2e.ts +20 -0
  9. package/cypress/tsconfig.json +8 -0
  10. package/cypress.config.ts +7 -0
  11. package/demo/App.tsx +39 -0
  12. package/demo/index.html +13 -0
  13. package/demo/index.scss +40 -0
  14. package/demo/main.tsx +10 -0
  15. package/demo/public/favicon-dxp.svg +3 -0
  16. package/demo/vite-env.d.ts +1 -0
  17. package/file-transformer.js +1 -0
  18. package/jest.bootstrap.ts +3 -0
  19. package/jest.config.ts +30 -0
  20. package/lib/Editor/Editor.d.ts +4 -2
  21. package/lib/Editor/Editor.js +11 -14
  22. package/lib/EditorToolbar/FloatingToolbar.d.ts +1 -0
  23. package/lib/EditorToolbar/FloatingToolbar.js +31 -0
  24. package/lib/EditorToolbar/Toolbar.d.ts +1 -0
  25. package/lib/EditorToolbar/Toolbar.js +25 -0
  26. package/lib/EditorToolbar/Tools/Link/Form/LinkForm.d.ts +10 -0
  27. package/lib/EditorToolbar/Tools/Link/Form/LinkForm.js +23 -0
  28. package/lib/EditorToolbar/Tools/Link/LinkButton.d.ts +5 -0
  29. package/lib/EditorToolbar/Tools/Link/LinkButton.js +34 -0
  30. package/lib/EditorToolbar/Tools/Link/LinkModal.d.ts +8 -0
  31. package/lib/EditorToolbar/Tools/Link/LinkModal.js +14 -0
  32. package/lib/EditorToolbar/Tools/Link/RemoveLinkButton.d.ts +2 -0
  33. package/lib/EditorToolbar/Tools/Link/RemoveLinkButton.js +16 -0
  34. package/lib/EditorToolbar/Tools/Redo/RedoButton.d.ts +2 -0
  35. package/lib/EditorToolbar/Tools/Redo/RedoButton.js +16 -0
  36. package/lib/EditorToolbar/Tools/TextAlign/TextAlignButtons.js +4 -1
  37. package/lib/EditorToolbar/Tools/TextType/Heading/HeadingButton.d.ts +5 -0
  38. package/lib/EditorToolbar/Tools/TextType/Heading/HeadingButton.js +32 -0
  39. package/lib/EditorToolbar/Tools/TextType/Paragraph/ParagraphButton.d.ts +2 -0
  40. package/lib/EditorToolbar/Tools/TextType/Paragraph/ParagraphButton.js +16 -0
  41. package/lib/EditorToolbar/Tools/TextType/Preformatted/PreformattedButton.d.ts +2 -0
  42. package/lib/EditorToolbar/Tools/TextType/Preformatted/PreformattedButton.js +16 -0
  43. package/lib/EditorToolbar/Tools/TextType/TextTypeDropdown.d.ts +2 -0
  44. package/lib/EditorToolbar/Tools/TextType/TextTypeDropdown.js +35 -0
  45. package/lib/EditorToolbar/Tools/Undo/UndoButton.d.ts +2 -0
  46. package/lib/EditorToolbar/Tools/Undo/UndoButton.js +16 -0
  47. package/lib/EditorToolbar/index.d.ts +2 -0
  48. package/lib/EditorToolbar/index.js +2 -0
  49. package/lib/Extensions/Extensions.d.ts +4 -0
  50. package/lib/Extensions/Extensions.js +20 -0
  51. package/lib/Extensions/LinkExtension/LinkExtension.d.ts +16 -0
  52. package/lib/Extensions/LinkExtension/LinkExtension.js +91 -0
  53. package/lib/Extensions/PreformattedExtension/PreformattedExtension.d.ts +10 -0
  54. package/lib/Extensions/PreformattedExtension/PreformattedExtension.js +46 -0
  55. package/lib/FormattedTextEditor.d.ts +2 -2
  56. package/lib/FormattedTextEditor.js +1 -6
  57. package/lib/hooks/index.d.ts +1 -0
  58. package/lib/hooks/index.js +1 -0
  59. package/lib/hooks/useExtensionNames.d.ts +1 -0
  60. package/lib/hooks/useExtensionNames.js +12 -0
  61. package/lib/index.css +787 -3686
  62. package/lib/ui/Inputs/Select/Select.d.ts +12 -0
  63. package/lib/ui/Inputs/Select/Select.js +23 -0
  64. package/lib/ui/Inputs/Text/TextInput.d.ts +4 -0
  65. package/lib/ui/Inputs/Text/TextInput.js +7 -0
  66. package/lib/ui/Modal/FormModal.d.ts +5 -0
  67. package/lib/ui/Modal/FormModal.js +11 -0
  68. package/lib/ui/Modal/Modal.d.ts +10 -0
  69. package/lib/ui/Modal/Modal.js +48 -0
  70. package/lib/ui/ToolbarButton/ToolbarButton.d.ts +1 -1
  71. package/lib/ui/ToolbarButton/ToolbarButton.js +1 -1
  72. package/lib/ui/ToolbarDropdown/ToolbarDropdown.d.ts +6 -0
  73. package/lib/ui/ToolbarDropdown/ToolbarDropdown.js +20 -0
  74. package/lib/ui/ToolbarDropdownButton/ToolbarDropdownButton.d.ts +9 -0
  75. package/lib/ui/ToolbarDropdownButton/ToolbarDropdownButton.js +8 -0
  76. package/lib/utils/createToolbarPositioner.d.ts +18 -0
  77. package/lib/utils/createToolbarPositioner.js +81 -0
  78. package/lib/utils/getCursorRect.d.ts +2 -0
  79. package/lib/utils/getCursorRect.js +3 -0
  80. package/package.json +22 -13
  81. package/postcss.config.js +12 -0
  82. package/src/Editor/Editor.mock.tsx +43 -0
  83. package/src/Editor/Editor.spec.tsx +254 -0
  84. package/src/Editor/Editor.tsx +46 -0
  85. package/src/Editor/_editor.scss +82 -0
  86. package/src/EditorToolbar/FloatingToolbar.spec.tsx +30 -0
  87. package/src/EditorToolbar/FloatingToolbar.tsx +40 -0
  88. package/src/EditorToolbar/Toolbar.tsx +33 -0
  89. package/src/EditorToolbar/Tools/Bold/BoldButton.spec.tsx +19 -0
  90. package/src/EditorToolbar/Tools/Bold/BoldButton.tsx +30 -0
  91. package/src/EditorToolbar/Tools/Italic/ItalicButton.spec.tsx +19 -0
  92. package/src/EditorToolbar/Tools/Italic/ItalicButton.tsx +30 -0
  93. package/src/EditorToolbar/Tools/Link/Form/LinkForm.spec.tsx +30 -0
  94. package/src/EditorToolbar/Tools/Link/Form/LinkForm.tsx +48 -0
  95. package/src/EditorToolbar/Tools/Link/LinkButton.spec.tsx +277 -0
  96. package/src/EditorToolbar/Tools/Link/LinkButton.tsx +56 -0
  97. package/src/EditorToolbar/Tools/Link/LinkModal.tsx +29 -0
  98. package/src/EditorToolbar/Tools/Link/RemoveLinkButton.spec.tsx +46 -0
  99. package/src/EditorToolbar/Tools/Link/RemoveLinkButton.tsx +27 -0
  100. package/src/EditorToolbar/Tools/Redo/RedoButton.spec.tsx +59 -0
  101. package/src/EditorToolbar/Tools/Redo/RedoButton.tsx +30 -0
  102. package/src/EditorToolbar/Tools/TextAlign/CenterAlign/CenterAlignButton.spec.tsx +39 -0
  103. package/src/EditorToolbar/Tools/TextAlign/CenterAlign/CenterAlignButton.tsx +31 -0
  104. package/src/EditorToolbar/Tools/TextAlign/JustifyAlign/JustifyAlignButton.spec.tsx +39 -0
  105. package/src/EditorToolbar/Tools/TextAlign/JustifyAlign/JustifyAlignButton.tsx +31 -0
  106. package/src/EditorToolbar/Tools/TextAlign/LeftAlign/LeftAlignButton.spec.tsx +39 -0
  107. package/src/EditorToolbar/Tools/TextAlign/LeftAlign/LeftAlignButton.tsx +31 -0
  108. package/src/EditorToolbar/Tools/TextAlign/RightAlign/RightAlignButton.spec.tsx +39 -0
  109. package/src/EditorToolbar/Tools/TextAlign/RightAlign/RightAlignButton.tsx +31 -0
  110. package/src/EditorToolbar/Tools/TextAlign/TextAlignButtons.tsx +21 -0
  111. package/src/EditorToolbar/Tools/TextType/Heading/HeadingButton.spec.tsx +56 -0
  112. package/src/EditorToolbar/Tools/TextType/Heading/HeadingButton.tsx +52 -0
  113. package/src/EditorToolbar/Tools/TextType/Paragraph/ParagraphButton.spec.tsx +30 -0
  114. package/src/EditorToolbar/Tools/TextType/Paragraph/ParagraphButton.tsx +25 -0
  115. package/src/EditorToolbar/Tools/TextType/Preformatted/PreformattedButton.spec.tsx +47 -0
  116. package/src/EditorToolbar/Tools/TextType/Preformatted/PreformattedButton.tsx +30 -0
  117. package/src/EditorToolbar/Tools/TextType/TextTypeDropdown.spec.tsx +51 -0
  118. package/src/EditorToolbar/Tools/TextType/TextTypeDropdown.tsx +44 -0
  119. package/src/EditorToolbar/Tools/Underline/Underline.spec.tsx +19 -0
  120. package/src/EditorToolbar/Tools/Underline/UnderlineButton.tsx +30 -0
  121. package/src/EditorToolbar/Tools/Undo/UndoButton.spec.tsx +49 -0
  122. package/src/EditorToolbar/Tools/Undo/UndoButton.tsx +30 -0
  123. package/src/EditorToolbar/_floating-toolbar.scss +4 -0
  124. package/src/EditorToolbar/_toolbar.scss +16 -0
  125. package/src/EditorToolbar/index.ts +2 -0
  126. package/src/Extensions/Extensions.ts +29 -0
  127. package/src/Extensions/LinkExtension/LinkExtension.ts +116 -0
  128. package/src/Extensions/PreformattedExtension/PreformattedExtension.ts +50 -0
  129. package/src/FormattedTextEditor.spec.tsx +10 -0
  130. package/src/FormattedTextEditor.tsx +3 -0
  131. package/src/hooks/index.ts +1 -0
  132. package/src/hooks/useExtensionNames.ts +15 -0
  133. package/src/index.scss +19 -0
  134. package/src/index.ts +3 -0
  135. package/src/ui/Inputs/Select/Select.spec.tsx +30 -0
  136. package/src/ui/Inputs/Select/Select.tsx +66 -0
  137. package/src/ui/Inputs/Text/TextInput.spec.tsx +43 -0
  138. package/src/ui/Inputs/Text/TextInput.tsx +20 -0
  139. package/src/ui/Modal/FormModal.spec.tsx +20 -0
  140. package/src/ui/Modal/FormModal.tsx +17 -0
  141. package/src/ui/Modal/Modal.spec.tsx +113 -0
  142. package/src/ui/Modal/Modal.tsx +97 -0
  143. package/src/ui/Modal/_modal.scss +24 -0
  144. package/src/ui/ToolbarButton/ToolbarButton.tsx +26 -0
  145. package/src/ui/ToolbarButton/_toolbar-button.scss +17 -0
  146. package/src/ui/ToolbarDropdown/ToolbarDropdown.spec.tsx +78 -0
  147. package/src/ui/ToolbarDropdown/ToolbarDropdown.tsx +42 -0
  148. package/src/ui/ToolbarDropdown/_toolbar-dropdown.scss +32 -0
  149. package/src/ui/ToolbarDropdownButton/ToolbarDropdownButton.spec.tsx +48 -0
  150. package/src/ui/ToolbarDropdownButton/ToolbarDropdownButton.tsx +29 -0
  151. package/src/ui/ToolbarDropdownButton/_toolbar-dropdown-button.scss +14 -0
  152. package/src/ui/_buttons.scss +19 -0
  153. package/src/ui/_forms.scss +16 -0
  154. package/src/utils/createToolbarPositioner.ts +115 -0
  155. package/src/utils/getCursorRect.ts +5 -0
  156. package/tailwind.config.cjs +83 -0
  157. package/tests/index.ts +2 -0
  158. package/tests/renderWithEditor.tsx +110 -0
  159. package/tests/select.tsx +16 -0
  160. package/tsconfig.json +22 -0
  161. package/vite.config.ts +19 -0
  162. package/lib/EditorToolbar/EditorToolbar.d.ts +0 -7
  163. package/lib/EditorToolbar/EditorToolbar.js +0 -22
@@ -0,0 +1,254 @@
1
+ import { jest } from '@jest/globals';
2
+ import React from 'react';
3
+ import { act, fireEvent, render, screen } from '@testing-library/react';
4
+ import { MockEditor } from './Editor.mock';
5
+ import Editor from './Editor';
6
+ import '@testing-library/jest-dom';
7
+
8
+ const setContent: any = jest.fn();
9
+
10
+ describe('Formatted text editor', () => {
11
+ it('Renders the text editor', () => {
12
+ render(<Editor />);
13
+ expect(screen.getByRole('textbox')).toBeInTheDocument();
14
+ });
15
+
16
+ it('Renders the placeholder if there is no content', () => {
17
+ render(<Editor />);
18
+ expect(document.querySelector(`[data-placeholder='Write something']`)).toBeInTheDocument();
19
+ });
20
+
21
+ it('Renders the bold button', () => {
22
+ render(<Editor />);
23
+ expect(screen.getByRole('button', { name: 'Bold (cmd+B)' })).toBeInTheDocument();
24
+ });
25
+
26
+ it('Renders the italic button', () => {
27
+ render(<Editor />);
28
+ expect(screen.getByRole('button', { name: 'Italic (cmd+I)' })).toBeInTheDocument();
29
+ });
30
+
31
+ it('Renders the underline button', () => {
32
+ render(<Editor />);
33
+ expect(screen.getByRole('button', { name: 'Underline (cmd+U)' })).toBeInTheDocument();
34
+ });
35
+
36
+ it('Renders the align left button', () => {
37
+ render(<Editor />);
38
+ expect(screen.getByRole('button', { name: 'Align left' })).toBeInTheDocument();
39
+ });
40
+
41
+ it('Applies left alignment styling to text when clicked', () => {
42
+ const { baseElement } = render(<Editor />);
43
+ expect(baseElement).toBeTruthy();
44
+
45
+ expect(baseElement.querySelector('p[data-node-text-align="left"]')).toBeFalsy();
46
+
47
+ const leftAlignButton = baseElement.querySelector('button[title="Align left"]') as HTMLButtonElement;
48
+ expect(leftAlignButton).toBeTruthy();
49
+
50
+ fireEvent.click(leftAlignButton);
51
+ expect(baseElement.querySelector('p[data-node-text-align="left"]')).toBeTruthy();
52
+ });
53
+
54
+ it('Applies center alignment styling to text when clicked', () => {
55
+ const { baseElement } = render(<Editor />);
56
+ expect(baseElement).toBeTruthy();
57
+
58
+ expect(baseElement.querySelector('p[data-node-text-align="center"]')).toBeFalsy();
59
+
60
+ const centerAlignButton = baseElement.querySelector('button[title="Align center"]') as HTMLButtonElement;
61
+ expect(centerAlignButton).toBeTruthy();
62
+
63
+ fireEvent.click(centerAlignButton);
64
+ expect(baseElement.querySelector('p[data-node-text-align="center"]')).toBeTruthy();
65
+ });
66
+
67
+ it('Applies right alignment styling to text when clicked', () => {
68
+ const { baseElement } = render(<Editor />);
69
+ expect(baseElement).toBeTruthy();
70
+
71
+ expect(baseElement.querySelector('p[data-node-text-align="right"]')).toBeFalsy();
72
+
73
+ const rightAlignButton = baseElement.querySelector('button[title="Align right"]') as HTMLButtonElement;
74
+ expect(rightAlignButton).toBeTruthy();
75
+
76
+ fireEvent.click(rightAlignButton);
77
+ expect(baseElement.querySelector('p[data-node-text-align="right"]')).toBeTruthy();
78
+ });
79
+
80
+ it('Applies justify alignment styling to text when clicked', () => {
81
+ const { baseElement } = render(<Editor />);
82
+ expect(baseElement).toBeTruthy();
83
+
84
+ expect(baseElement.querySelector('p[data-node-text-align="justify"]')).toBeFalsy();
85
+
86
+ const justifyAlignButton = baseElement.querySelector('button[title="Justify"]') as HTMLButtonElement;
87
+ expect(justifyAlignButton).toBeTruthy();
88
+
89
+ fireEvent.click(justifyAlignButton);
90
+ expect(baseElement.querySelector('p[data-node-text-align="justify"]')).toBeTruthy();
91
+ });
92
+
93
+ it('Applies Heading 1 styling to text when clicked', () => {
94
+ const { baseElement } = render(<Editor />);
95
+ expect(baseElement).toBeTruthy();
96
+
97
+ expect(baseElement.querySelector('div.remirror-editor h1')).toBeFalsy();
98
+
99
+ const headingDropdown = baseElement.querySelector('.toolbar-dropdown__button') as HTMLButtonElement;
100
+ expect(headingDropdown).toBeTruthy();
101
+ fireEvent.click(headingDropdown);
102
+
103
+ const h1Button = baseElement.querySelector('button[title="Heading 1"]') as HTMLButtonElement;
104
+ expect(h1Button).toBeTruthy();
105
+ fireEvent.click(h1Button);
106
+
107
+ expect(baseElement.querySelector('div.remirror-editor h1')).toBeTruthy();
108
+ });
109
+
110
+ it('Applies Heading 2 styling to text when clicked', () => {
111
+ const { baseElement } = render(<Editor />);
112
+ expect(baseElement).toBeTruthy();
113
+
114
+ expect(baseElement.querySelector('div.remirror-editor h2')).toBeFalsy();
115
+
116
+ const headingDropdown = baseElement.querySelector('.toolbar-dropdown__button') as HTMLButtonElement;
117
+ expect(headingDropdown).toBeTruthy();
118
+ fireEvent.click(headingDropdown);
119
+
120
+ const h2Button = baseElement.querySelector('button[title="Heading 2"]') as HTMLButtonElement;
121
+ expect(h2Button).toBeTruthy();
122
+ fireEvent.click(h2Button);
123
+
124
+ expect(baseElement.querySelector('div.remirror-editor h2')).toBeTruthy();
125
+ });
126
+
127
+ it('Applies Heading 3 styling to text when clicked', () => {
128
+ const { baseElement } = render(<Editor />);
129
+ expect(baseElement).toBeTruthy();
130
+
131
+ expect(baseElement.querySelector('div.remirror-editor h3')).toBeFalsy();
132
+
133
+ const headingDropdown = baseElement.querySelector('.toolbar-dropdown__button') as HTMLButtonElement;
134
+ expect(headingDropdown).toBeTruthy();
135
+ fireEvent.click(headingDropdown);
136
+
137
+ const h3Button = baseElement.querySelector('button[title="Heading 3"]') as HTMLButtonElement;
138
+ expect(h3Button).toBeTruthy();
139
+ fireEvent.click(h3Button);
140
+
141
+ expect(baseElement.querySelector('div.remirror-editor h3')).toBeTruthy();
142
+ });
143
+
144
+ it('Applies Heading 4 styling to text when clicked', () => {
145
+ const { baseElement } = render(<Editor />);
146
+ expect(baseElement).toBeTruthy();
147
+
148
+ expect(baseElement.querySelector('div.remirror-editor h4')).toBeFalsy();
149
+
150
+ const headingDropdown = baseElement.querySelector('.toolbar-dropdown__button') as HTMLButtonElement;
151
+ expect(headingDropdown).toBeTruthy();
152
+ fireEvent.click(headingDropdown);
153
+
154
+ const h4Button = baseElement.querySelector('button[title="Heading 4"]') as HTMLButtonElement;
155
+ expect(h4Button).toBeTruthy();
156
+ fireEvent.click(h4Button);
157
+
158
+ expect(baseElement.querySelector('div.remirror-editor h4')).toBeTruthy();
159
+ });
160
+
161
+ it('Applies Heading 5 styling to text when clicked', () => {
162
+ const { baseElement } = render(<Editor />);
163
+ expect(baseElement).toBeTruthy();
164
+
165
+ expect(baseElement.querySelector('div.remirror-editor h5')).toBeFalsy();
166
+
167
+ const headingDropdown = baseElement.querySelector('.toolbar-dropdown__button') as HTMLButtonElement;
168
+ expect(headingDropdown).toBeTruthy();
169
+ fireEvent.click(headingDropdown);
170
+
171
+ const h5Button = baseElement.querySelector('button[title="Heading 5"]') as HTMLButtonElement;
172
+ expect(h5Button).toBeTruthy();
173
+ fireEvent.click(h5Button);
174
+
175
+ expect(baseElement.querySelector('div.remirror-editor h5')).toBeTruthy();
176
+ });
177
+
178
+ it('Applies Heading 6 styling to text when clicked', () => {
179
+ const { baseElement } = render(<Editor />);
180
+ expect(baseElement).toBeTruthy();
181
+
182
+ expect(baseElement.querySelector('div.remirror-editor h6')).toBeFalsy();
183
+
184
+ const headingDropdown = baseElement.querySelector('.toolbar-dropdown__button') as HTMLButtonElement;
185
+ expect(headingDropdown).toBeTruthy();
186
+ fireEvent.click(headingDropdown);
187
+
188
+ const h6Button = baseElement.querySelector('button[title="Heading 6"]') as HTMLButtonElement;
189
+ expect(h6Button).toBeTruthy();
190
+ fireEvent.click(h6Button);
191
+
192
+ expect(baseElement.querySelector('div.remirror-editor h6')).toBeTruthy();
193
+ });
194
+
195
+ it('Applies Preformatted styling to text when clicked', () => {
196
+ const { baseElement } = render(<Editor />);
197
+ expect(baseElement).toBeTruthy();
198
+
199
+ expect(baseElement.querySelector('div.remirror-editor pre')).toBeFalsy();
200
+
201
+ const headingDropdown = baseElement.querySelector('.toolbar-dropdown__button') as HTMLButtonElement;
202
+ expect(headingDropdown).toBeTruthy();
203
+ fireEvent.click(headingDropdown);
204
+
205
+ const preButton = baseElement.querySelector('button[title="Preformatted"]') as HTMLButtonElement;
206
+ expect(preButton).toBeTruthy();
207
+ fireEvent.click(preButton);
208
+
209
+ expect(baseElement.querySelector('div.remirror-editor pre')).toBeTruthy();
210
+ });
211
+
212
+ it('Applies Paragraph styling to text when clicked', () => {
213
+ const { baseElement } = render(<Editor />);
214
+ expect(baseElement).toBeTruthy();
215
+
216
+ expect(baseElement.querySelectorAll('div.remirror-editor p')).toHaveLength(1);
217
+
218
+ const headingDropdown = baseElement.querySelector('.toolbar-dropdown__button') as HTMLButtonElement;
219
+ expect(headingDropdown).toBeTruthy();
220
+ fireEvent.click(headingDropdown);
221
+
222
+ const preButton = baseElement.querySelector('button[title="Preformatted"]') as HTMLButtonElement;
223
+ expect(preButton).toBeTruthy();
224
+ fireEvent.click(preButton);
225
+
226
+ const paragraphButton = baseElement.querySelector('button[title="Paragraph"]') as HTMLButtonElement;
227
+ expect(paragraphButton).toBeTruthy();
228
+ fireEvent.click(paragraphButton);
229
+
230
+ expect(baseElement.querySelectorAll('div.remirror-editor p')).toHaveLength(1);
231
+ });
232
+
233
+ it('Should allow text input & undo input upon clicking undo button', () => {
234
+ const { baseElement, getByLabelText } = render(<MockEditor setContent={setContent} />);
235
+
236
+ const textContent = `This is a string with a random number: ${Math.random() * 9999}`;
237
+
238
+ // This sets the content of the text editor
239
+ act(() => {
240
+ setContent(`<p>${textContent}</p>`, { triggerChange: true });
241
+ });
242
+
243
+ const editorNode = getByLabelText(`Text editor`);
244
+ expect(editorNode).toBeTruthy();
245
+ expect(editorNode.textContent).toBe(textContent);
246
+
247
+ // Testing if clicking undo button removes text from editor
248
+ const undoButton = baseElement.querySelector('button[title="Undo (cmd+Z)"]') as HTMLButtonElement;
249
+ expect(undoButton).toBeTruthy();
250
+ fireEvent.click(undoButton);
251
+
252
+ expect(editorNode.textContent).toBe('');
253
+ });
254
+ });
@@ -0,0 +1,46 @@
1
+ import React from 'react';
2
+ import { EditorComponent, Remirror, useRemirror } from '@remirror/react';
3
+ import { RemirrorContentType, RemirrorEventListener, Extension } from '@remirror/core';
4
+ import { Toolbar, FloatingToolbar } from '../EditorToolbar';
5
+ import { Extensions } from '../Extensions/Extensions';
6
+
7
+ type EditorProps = {
8
+ content?: RemirrorContentType;
9
+ onChange?: RemirrorEventListener<Extension>;
10
+ editable?: boolean;
11
+ };
12
+
13
+ const Editor = ({ content, editable, onChange }: EditorProps) => {
14
+ const { manager, state, setState } = useRemirror({
15
+ extensions: Extensions,
16
+ content,
17
+ selection: 'start',
18
+ stringHandler: 'html',
19
+ });
20
+
21
+ const handleChange: RemirrorEventListener<Extension> = (parameter) => {
22
+ setState(parameter.state);
23
+ onChange?.(parameter);
24
+ };
25
+
26
+ return (
27
+ <div className="squiz-fte-scope">
28
+ <div className="remirror-theme formatted-text-editor editor-wrapper">
29
+ <Remirror
30
+ manager={manager}
31
+ state={state}
32
+ editable={editable}
33
+ onChange={handleChange}
34
+ placeholder="Write something"
35
+ label="Text editor"
36
+ >
37
+ <Toolbar />
38
+ <EditorComponent />
39
+ <FloatingToolbar />
40
+ </Remirror>
41
+ </div>
42
+ </div>
43
+ );
44
+ };
45
+
46
+ export default Editor;
@@ -0,0 +1,82 @@
1
+ .formatted-text-editor {
2
+ font-family: 'Open Sans' !important;
3
+
4
+ &.editor-wrapper {
5
+ @apply bg-white rounded border-gray-300 border-2 border-solid;
6
+ }
7
+
8
+ .remirror-editor-wrapper {
9
+ @apply text-gray-600 pt-0;
10
+ }
11
+
12
+ .remirror-editor {
13
+ @apply bg-white shadow-none rounded-b p-3;
14
+ min-height: 6rem;
15
+
16
+ &:active,
17
+ &:focus {
18
+ @apply outline-0;
19
+ }
20
+
21
+ p {
22
+ /* Make sure content aligned with "text-align: justify" is justified */
23
+ @apply block;
24
+ }
25
+ }
26
+
27
+ .remirror-is-empty:first-of-type::before {
28
+ position: absolute;
29
+ pointer-events: none;
30
+ height: 0;
31
+ font-style: italic;
32
+ content: attr(data-placeholder);
33
+ @apply text-gray-500;
34
+ }
35
+ }
36
+
37
+ a {
38
+ @apply text-blue-300;
39
+ text-decoration: underline;
40
+ }
41
+
42
+ .remirror-theme h1 {
43
+ font-size: 1.625rem;
44
+ font-weight: 600;
45
+ letter-spacing: -0.2px;
46
+ line-height: 2rem;
47
+ }
48
+
49
+ .remirror-theme h2 {
50
+ font-size: 1.25rem;
51
+ font-weight: 600;
52
+ letter-spacing: -0.5px;
53
+ line-height: 1.5rem;
54
+ }
55
+
56
+ .remirror-theme h3 {
57
+ font-size: 1.125rem;
58
+ font-weight: 600;
59
+ letter-spacing: -0.2px;
60
+ line-height: 1.375rem;
61
+ }
62
+
63
+ .remirror-theme h4 {
64
+ font-size: 1rem;
65
+ font-weight: 700;
66
+ letter-spacing: -0.2px;
67
+ line-height: 1.25rem;
68
+ }
69
+
70
+ .remirror-theme h5 {
71
+ font-size: 1rem;
72
+ font-weight: 600;
73
+ letter-spacing: -0.2px;
74
+ line-height: 1.25rem;
75
+ }
76
+
77
+ .remirror-theme h6 {
78
+ font-size: 0.875rem;
79
+ font-weight: 600;
80
+ letter-spacing: -0.2px;
81
+ line-height: 1.25rem;
82
+ }
@@ -0,0 +1,30 @@
1
+ import React from 'react';
2
+ import { act, screen } from '@testing-library/react';
3
+ import { renderWithEditor } from '../../tests';
4
+ import { FloatingToolbar } from './FloatingToolbar';
5
+
6
+ describe('FloatingToolbar', () => {
7
+ it.each([
8
+ ['Nothing selected', 3, 3, []],
9
+ ['Regular text selected', 3, 4, ['Bold (cmd+B)', 'Italic (cmd+I)', 'Underline (cmd+U)', 'Link (cmd+K)']],
10
+ ['Regular text + link selected', 3, 17, ['Bold (cmd+B)', 'Italic (cmd+I)', 'Underline (cmd+U)']],
11
+ ['Nothing selected, positioned directly on the left of a link', 12, 12, []],
12
+ ['Nothing selected, positioned directly on the right of a link', 19, 19, []],
13
+ ['Nothing selected, positioned within a link', 13, 13, ['Link (cmd+K)', 'Remove link']],
14
+ ['Link partially selected', 15, 17, ['Link (cmd+K)', 'Remove link']],
15
+ ])(
16
+ 'Renders formatting buttons when text is selected - %s',
17
+ async (description: string, from: number, to: number, expectedButtons: string[]) => {
18
+ const { editor } = await renderWithEditor(<FloatingToolbar />, {
19
+ content: 'My awesome <a href="https://example.org">example</a> content.',
20
+ });
21
+
22
+ await act(() => editor.selectText({ from, to }));
23
+
24
+ const buttons = screen.queryAllByRole('button');
25
+ const buttonLabels = buttons.map((button) => button.getAttribute('title'));
26
+
27
+ expect(buttonLabels).toEqual(expectedButtons);
28
+ },
29
+ );
30
+ });
@@ -0,0 +1,40 @@
1
+ import React, { useMemo } from 'react';
2
+ import ItalicButton from './Tools/Italic/ItalicButton';
3
+ import UnderlineButton from './Tools/Underline/UnderlineButton';
4
+ import BoldButton from './Tools/Bold/BoldButton';
5
+ import { useExtensionNames } from '../hooks';
6
+ import RemoveLinkButton from './Tools/Link/RemoveLinkButton';
7
+ import LinkButton from './Tools/Link/LinkButton';
8
+ import { FloatingToolbar as RemirrorFloatingToolbar, usePositioner } from '@remirror/react';
9
+ import { VerticalDivider } from '@remirror/react-components';
10
+ import { createToolbarPositioner, ToolbarPositionerRange } from '../utils/createToolbarPositioner';
11
+
12
+ // The editor main toolbar
13
+ export const FloatingToolbar = () => {
14
+ const extensionNames = useExtensionNames();
15
+ const positioner = useMemo(() => createToolbarPositioner({ types: ['link'] }), []);
16
+ const { data } = usePositioner<Partial<ToolbarPositionerRange>>(positioner, []);
17
+ let buttons = [
18
+ extensionNames.bold && <BoldButton key="bold" />,
19
+ extensionNames.italic && <ItalicButton key="italic" />,
20
+ extensionNames.underline && <UnderlineButton key="underline" />,
21
+ ];
22
+
23
+ if (data.marks?.link.isExclusivelyActive) {
24
+ // if all of the selected text is a link show the options to update/remove the link instead of the regular
25
+ // formatting options.
26
+ buttons = [<LinkButton key="update-link" inPopover={true} />, <RemoveLinkButton key="remove-link" />];
27
+ } else if (!data.marks?.link.isActive) {
28
+ // if none of the selected text is a link show the option to create a link.
29
+ buttons.push(
30
+ <VerticalDivider key="link-divider" className="link-divider" />,
31
+ <LinkButton key="add-link" inPopover={true} />,
32
+ );
33
+ }
34
+
35
+ return (
36
+ <RemirrorFloatingToolbar className="squiz-fte-scope squiz-fte-scope__floating-popover" positioner={positioner}>
37
+ {buttons}
38
+ </RemirrorFloatingToolbar>
39
+ );
40
+ };
@@ -0,0 +1,33 @@
1
+ import React from 'react';
2
+ import { Toolbar as RemirrorToolbar, VerticalDivider } from '@remirror/react-components';
3
+ import ItalicButton from './Tools/Italic/ItalicButton';
4
+ import UnderlineButton from './Tools/Underline/UnderlineButton';
5
+ import BoldButton from './Tools/Bold/BoldButton';
6
+ import TextAlignButtons from './Tools/TextAlign/TextAlignButtons';
7
+ import UndoButton from './Tools/Undo/UndoButton';
8
+ import RedoButton from './Tools/Redo/RedoButton';
9
+ import TextTypeDropdown from './Tools/TextType/TextTypeDropdown';
10
+ import { useExtensionNames } from '../hooks';
11
+ import LinkButton from './Tools/Link/LinkButton';
12
+
13
+ export const Toolbar = () => {
14
+ const extensionNames = useExtensionNames();
15
+
16
+ return (
17
+ <RemirrorToolbar className="remirror-toolbar editor-toolbar">
18
+ {extensionNames.history && (
19
+ <>
20
+ <UndoButton />
21
+ <RedoButton />
22
+ <VerticalDivider className="editor-divider" />
23
+ </>
24
+ )}
25
+ {extensionNames.heading && extensionNames.paragraph && extensionNames.preformatted && <TextTypeDropdown />}
26
+ {extensionNames.bold && <BoldButton />}
27
+ {extensionNames.italic && <ItalicButton />}
28
+ {extensionNames.underline && <UnderlineButton />}
29
+ {extensionNames.nodeFormatting && <TextAlignButtons />}
30
+ {extensionNames.link && <LinkButton />}
31
+ </RemirrorToolbar>
32
+ );
33
+ };
@@ -0,0 +1,19 @@
1
+ import React from 'react';
2
+ import '@testing-library/jest-dom';
3
+ import { render, screen, fireEvent } from '@testing-library/react';
4
+ import Editor from '../../../Editor/Editor';
5
+
6
+ describe('Bold button', () => {
7
+ it('Renders the bold button', () => {
8
+ render(<Editor />);
9
+ expect(screen.getByRole('button', { name: 'Bold (cmd+B)' })).toBeInTheDocument();
10
+ });
11
+
12
+ it('Activates the button if clicked', () => {
13
+ render(<Editor />);
14
+ expect(screen.getByRole('button', { name: 'Bold (cmd+B)' }).classList.contains('squiz-fte-btn')).toBeTruthy();
15
+ const bold = screen.getByRole('button', { name: 'Bold (cmd+B)' });
16
+ fireEvent.click(bold);
17
+ expect(bold.classList.contains('is-active')).toBeTruthy();
18
+ });
19
+ });
@@ -0,0 +1,30 @@
1
+ import React from 'react';
2
+ import { useCommands, useActive, useChainedCommands } from '@remirror/react';
3
+ import { BoldExtension } from '@remirror/extension-bold';
4
+ import ToolbarButton from '../../../ui/ToolbarButton/ToolbarButton';
5
+ import FormatBoldRoundedIcon from '@mui/icons-material/FormatBoldRounded';
6
+
7
+ const BoldButton = () => {
8
+ const { toggleBold } = useCommands();
9
+ const chain = useChainedCommands();
10
+
11
+ const active = useActive<BoldExtension>();
12
+ const enabled = toggleBold.enabled();
13
+ const handleSelect = () => {
14
+ if (toggleBold.enabled()) {
15
+ chain.toggleBold().focus().run();
16
+ }
17
+ };
18
+
19
+ return (
20
+ <ToolbarButton
21
+ handleOnClick={handleSelect}
22
+ isDisabled={!enabled}
23
+ isActive={active.bold()}
24
+ icon={<FormatBoldRoundedIcon />}
25
+ label="Bold (cmd+B)"
26
+ />
27
+ );
28
+ };
29
+
30
+ export default BoldButton;
@@ -0,0 +1,19 @@
1
+ import React from 'react';
2
+ import '@testing-library/jest-dom';
3
+ import { render, screen, fireEvent } from '@testing-library/react';
4
+ import Editor from '../../../Editor/Editor';
5
+
6
+ describe('Italic button', () => {
7
+ it('Renders the italic button', () => {
8
+ render(<Editor />);
9
+ expect(screen.getByRole('button', { name: 'Italic (cmd+I)' })).toBeInTheDocument();
10
+ });
11
+
12
+ it('Activates the button if clicked', () => {
13
+ render(<Editor />);
14
+ expect(screen.getByRole('button', { name: 'Italic (cmd+I)' }).classList.contains('squiz-fte-btn')).toBeTruthy();
15
+ const italic = screen.getByRole('button', { name: 'Italic (cmd+I)' });
16
+ fireEvent.click(italic);
17
+ expect(italic.classList.contains('is-active')).toBeTruthy();
18
+ });
19
+ });
@@ -0,0 +1,30 @@
1
+ import React from 'react';
2
+ import { useCommands, useActive, useChainedCommands } from '@remirror/react';
3
+ import { ItalicExtension } from '@remirror/extension-italic';
4
+ import ToolbarButton from '../../../ui/ToolbarButton/ToolbarButton';
5
+ import FormatItalicRoundedIcon from '@mui/icons-material/FormatItalicRounded';
6
+
7
+ const ItalicButton = () => {
8
+ const { toggleItalic } = useCommands();
9
+ const chain = useChainedCommands();
10
+
11
+ const active = useActive<ItalicExtension>();
12
+ const enabled = toggleItalic.enabled();
13
+ const handleSelect = () => {
14
+ if (toggleItalic.enabled()) {
15
+ chain.toggleItalic().focus().run();
16
+ }
17
+ };
18
+
19
+ return (
20
+ <ToolbarButton
21
+ handleOnClick={handleSelect}
22
+ isDisabled={!enabled}
23
+ isActive={active.italic()}
24
+ icon={<FormatItalicRoundedIcon />}
25
+ label="Italic (cmd+I)"
26
+ />
27
+ );
28
+ };
29
+
30
+ export default ItalicButton;
@@ -0,0 +1,30 @@
1
+ import '@testing-library/jest-dom';
2
+ import { render, screen } from '@testing-library/react';
3
+ import React from 'react';
4
+ import LinkForm from './LinkForm';
5
+
6
+ describe('Link Form', () => {
7
+ const handleSubmit = jest.fn();
8
+ const data = {
9
+ href: 'https://www.squiz.net/link-form',
10
+ target: '_blank',
11
+ title: 'Link title',
12
+ text: 'Link text',
13
+ };
14
+
15
+ it('Renders the form with the relevant fields', () => {
16
+ render(<LinkForm data={data} onSubmit={handleSubmit} />);
17
+
18
+ expect(screen.getByLabelText('URL')).toHaveValue('https://www.squiz.net/link-form');
19
+ expect(screen.getByLabelText('Text')).toHaveValue('Link text');
20
+ expect(screen.getByLabelText('Title')).toHaveValue('Link title');
21
+ expect(screen.getByLabelText('Target')).toHaveTextContent('New window');
22
+ });
23
+
24
+ it('Renders the form with the specified class', () => {
25
+ render(<LinkForm data={data} onSubmit={handleSubmit} />);
26
+
27
+ const formClass = document.querySelector('.squiz-fte-form');
28
+ expect(formClass).toBeInTheDocument();
29
+ });
30
+ });
@@ -0,0 +1,48 @@
1
+ import React, { ReactElement } from 'react';
2
+ import { TextInput } from '../../../../ui/Inputs/Text/TextInput';
3
+ import { Select, SelectOptions } from '../../../../ui/Inputs/Select/Select';
4
+ import { SubmitHandler, useForm } from 'react-hook-form';
5
+ import { UpdateLinkOptions } from '../../../../Extensions/LinkExtension/LinkExtension';
6
+
7
+ export type LinkFormData = Pick<UpdateLinkOptions, 'href' | 'target' | 'title' | 'text'>;
8
+
9
+ export type FormProps = {
10
+ data: Partial<LinkFormData>;
11
+ onSubmit: SubmitHandler<LinkFormData>;
12
+ };
13
+
14
+ const selectOptions: SelectOptions = {
15
+ _self: { label: 'Current window' },
16
+ _blank: { label: 'New window' },
17
+ };
18
+
19
+ const LinkForm = ({ data, onSubmit }: FormProps): ReactElement => {
20
+ const { register, handleSubmit, setValue } = useForm<LinkFormData>({
21
+ defaultValues: data,
22
+ });
23
+
24
+ return (
25
+ <form className="squiz-fte-form" onSubmit={handleSubmit(onSubmit)}>
26
+ <div className="squiz-fte-form-group mb-2">
27
+ <TextInput label="URL" {...register('href')} />
28
+ </div>
29
+ <div className="squiz-fte-form-group mb-2">
30
+ <TextInput label="Text" {...register('text')} />
31
+ </div>
32
+ <div className="squiz-fte-form-group mb-2">
33
+ <TextInput label="Title" {...register('title')} />
34
+ </div>
35
+ <div className="squiz-fte-form-group mb-0">
36
+ <Select
37
+ name="target"
38
+ label="Target"
39
+ value={data.target || '_self'}
40
+ options={selectOptions}
41
+ onChange={(value) => setValue('target', value)}
42
+ />
43
+ </div>
44
+ </form>
45
+ );
46
+ };
47
+
48
+ export default LinkForm;