@pie-lib/editable-html-tip-tap 2.1.19 → 2.1.20-beta.10

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 (84) hide show
  1. package/CHANGELOG.md +0 -130
  2. package/lib/__tests__/EditableHtml.test.js +831 -0
  3. package/lib/__tests__/constants.test.js +21 -0
  4. package/lib/__tests__/div-to-paragraph-conversion.test.js +213 -0
  5. package/lib/__tests__/extensions.test.js +209 -0
  6. package/lib/__tests__/index.test.js +250 -0
  7. package/lib/__tests__/size-utils.test.js +57 -0
  8. package/lib/__tests__/spellcheck.test.js +142 -0
  9. package/lib/__tests__/theme.test.js +17 -0
  10. package/lib/components/EditableHtml.js +0 -56
  11. package/lib/components/EditableHtml.js.map +1 -1
  12. package/lib/components/MenuBar.js +12 -81
  13. package/lib/components/MenuBar.js.map +1 -1
  14. package/lib/components/TiptapContainer.js +2 -2
  15. package/lib/components/TiptapContainer.js.map +1 -1
  16. package/lib/components/__tests__/AltDialog.test.js +201 -0
  17. package/lib/components/__tests__/CharacterPicker.test.js +455 -0
  18. package/lib/components/__tests__/CssIcon.test.js +58 -0
  19. package/lib/components/__tests__/DragInTheBlank.test.js +295 -0
  20. package/lib/components/__tests__/ExplicitConstructedResponse.test.js +345 -0
  21. package/lib/components/__tests__/ImageToolbar.test.js +185 -0
  22. package/lib/components/__tests__/InlineDropdown.test.js +839 -0
  23. package/lib/components/__tests__/InsertImageHandler.test.js +171 -0
  24. package/lib/components/__tests__/MediaDialog.test.js +433 -0
  25. package/lib/components/__tests__/MediaToolbar.test.js +126 -0
  26. package/lib/components/__tests__/MediaWrapper.test.js +96 -0
  27. package/lib/components/__tests__/MenuBar.test.js +502 -0
  28. package/lib/components/__tests__/RespArea.test.js +171 -0
  29. package/lib/components/__tests__/TableIcons.test.js +153 -0
  30. package/lib/components/__tests__/TextAlign.test.js +209 -0
  31. package/lib/components/__tests__/TiptapContainer.test.js +196 -0
  32. package/lib/components/__tests__/characterUtils.test.js +178 -0
  33. package/lib/components/__tests__/choice.test.js +213 -0
  34. package/lib/components/__tests__/custom-popper.test.js +108 -0
  35. package/lib/components/__tests__/done-button.test.js +72 -0
  36. package/lib/components/__tests__/toolbar-buttons.test.js +277 -0
  37. package/lib/components/common/toolbar-buttons.js +5 -6
  38. package/lib/components/common/toolbar-buttons.js.map +1 -1
  39. package/lib/components/icons/TableIcons.js +1 -3
  40. package/lib/components/icons/TableIcons.js.map +1 -1
  41. package/lib/components/icons/TextAlign.js +5 -14
  42. package/lib/components/icons/TextAlign.js.map +1 -1
  43. package/lib/components/respArea/DragInTheBlank/__tests__/choice.test.js +90 -0
  44. package/lib/components/respArea/MathTemplated.js +8 -14
  45. package/lib/components/respArea/MathTemplated.js.map +1 -1
  46. package/lib/components/respArea/__tests__/MathTemplated.test.js +217 -0
  47. package/lib/extensions/__tests__/css.test.js +214 -0
  48. package/lib/extensions/__tests__/custom-toolbar-wrapper.test.js +175 -0
  49. package/lib/extensions/__tests__/divNode.test.js +108 -0
  50. package/lib/extensions/__tests__/ensure-empty-root-div.test.js +50 -0
  51. package/lib/extensions/__tests__/ensure-list-item-content-is-div.test.js +60 -0
  52. package/lib/extensions/__tests__/extended-list-item.test.js +13 -0
  53. package/lib/extensions/__tests__/extended-table-cell.test.js +15 -0
  54. package/lib/extensions/__tests__/extended-table.test.js +200 -0
  55. package/lib/extensions/__tests__/image-component.test.js +432 -0
  56. package/lib/extensions/__tests__/image.test.js +313 -0
  57. package/lib/extensions/__tests__/math.test.js +1411 -0
  58. package/lib/extensions/__tests__/media-node-view.test.js +307 -0
  59. package/lib/extensions/__tests__/media.test.js +292 -0
  60. package/lib/extensions/__tests__/responseArea.test.js +736 -0
  61. package/lib/extensions/math.js +37 -19
  62. package/lib/extensions/math.js.map +1 -1
  63. package/lib/styles/editorContainerStyles.js +2 -2
  64. package/lib/styles/editorContainerStyles.js.map +1 -1
  65. package/lib/utils/__tests__/helper.test.js +107 -0
  66. package/lib/utils/__tests__/toolbar.test.js +38 -0
  67. package/package.json +9 -9
  68. package/src/__tests__/EditableHtml.test.jsx +0 -80
  69. package/src/components/EditableHtml.jsx +0 -52
  70. package/src/components/MenuBar.jsx +9 -91
  71. package/src/components/TiptapContainer.jsx +2 -2
  72. package/src/components/__tests__/MenuBar.test.jsx +9 -97
  73. package/src/components/__tests__/toolbar-buttons.test.jsx +2 -15
  74. package/src/components/common/toolbar-buttons.jsx +4 -6
  75. package/src/components/icons/TableIcons.jsx +1 -9
  76. package/src/components/icons/TextAlign.jsx +5 -39
  77. package/src/components/respArea/MathTemplated.jsx +6 -13
  78. package/src/components/respArea/__tests__/MathTemplated.test.jsx +1 -1
  79. package/src/extensions/__tests__/math.test.js +11 -9
  80. package/src/extensions/math.js +19 -11
  81. package/src/styles/editorContainerStyles.js +2 -2
  82. package/LICENSE.md +0 -5
  83. package/src/__tests__/editor-attributes.test.jsx +0 -77
  84. package/src/__tests__/spellcheck.test.jsx +0 -63
@@ -11,7 +11,6 @@ jest.mock('@tiptap/react', () => ({
11
11
  const mockEditor = {
12
12
  getHTML: jest.fn(() => '<p>test content</p>'),
13
13
  setEditable: jest.fn(),
14
- setOptions: jest.fn(),
15
14
  commands: {
16
15
  setContent: jest.fn(),
17
16
  },
@@ -586,83 +585,4 @@ describe('EditableHtml', () => {
586
585
  }).not.toThrow();
587
586
  });
588
587
  });
589
-
590
- describe('spellCheck', () => {
591
- const lastEditorAttributes = () => {
592
- const editorConfig = useEditor.mock.calls[useEditor.mock.calls.length - 1][0];
593
-
594
- return editorConfig.editorProps.attributes;
595
- };
596
-
597
- it('disables browser spellcheck on the contenteditable when spellCheck is false', async () => {
598
- render(<EditableHtml {...defaultProps} spellCheck={false} />);
599
-
600
- await waitFor(() => {
601
- expect(useEditor).toHaveBeenCalled();
602
- });
603
-
604
- expect(lastEditorAttributes()).toEqual({
605
- role: 'textbox',
606
- 'data-pie-editor': 'true',
607
- spellcheck: 'false',
608
- autocorrect: 'off',
609
- autocapitalize: 'off',
610
- });
611
- });
612
-
613
- it('enables browser spellcheck when spellCheck is true', async () => {
614
- render(<EditableHtml {...defaultProps} spellCheck={true} />);
615
-
616
- await waitFor(() => {
617
- expect(useEditor).toHaveBeenCalled();
618
- });
619
-
620
- expect(lastEditorAttributes()).toEqual({
621
- role: 'textbox',
622
- 'data-pie-editor': 'true',
623
- spellcheck: 'true',
624
- autocorrect: 'on',
625
- autocapitalize: 'on',
626
- });
627
- });
628
-
629
- it('keeps the browser default when spellCheck is not provided', async () => {
630
- render(<EditableHtml {...defaultProps} />);
631
-
632
- await waitFor(() => {
633
- expect(useEditor).toHaveBeenCalled();
634
- });
635
-
636
- expect(lastEditorAttributes().spellcheck).toEqual('true');
637
- });
638
-
639
- it('pushes spellCheck changes onto an existing editor', async () => {
640
- const editorRef = jest.fn();
641
- const { rerender } = render(<EditableHtml {...defaultProps} spellCheck={true} editorRef={editorRef} />);
642
-
643
- await waitFor(() => {
644
- expect(editorRef).toHaveBeenCalled();
645
- });
646
-
647
- rerender(<EditableHtml {...defaultProps} spellCheck={false} editorRef={editorRef} />);
648
-
649
- await waitFor(() => {
650
- expect(lastEditorAttributes().spellcheck).toEqual('false');
651
- });
652
-
653
- const editor = editorRef.mock.calls[editorRef.mock.calls.length - 1][0];
654
-
655
- expect(editor.setOptions).toHaveBeenCalledWith({
656
- editorProps: expect.objectContaining({
657
- attributes: {
658
- role: 'textbox',
659
- 'data-pie-editor': 'true',
660
- spellcheck: 'false',
661
- autocorrect: 'off',
662
- autocapitalize: 'off',
663
- },
664
- }),
665
- });
666
- });
667
- });
668
588
  });
@@ -96,25 +96,6 @@ const cssVariables = {
96
96
  0px 3.618px 9.949px 0px rgba(0, 0, 0, .04)`,
97
97
  };
98
98
 
99
- // Attributes PIE puts on the editor's contenteditable element.
100
- //
101
- // ProseMirror rebuilds the element's attribute set from editorProps.attributes on every view
102
- // update and removes anything no longer listed, and Tiptap merges its own role="textbox" only in
103
- // createView - not into editor.options, which is what setOptions re-pushes. So every attribute we
104
- // want to survive has to be listed here. See PIE-1015.
105
- const editorAttributes = (spellCheckEnabled) => ({
106
- // Tiptap's own attribute, re-declared so it survives setOptions. setEditable alone re-pushes
107
- // editorProps on every mount, which is what removed it in 2.1.17.
108
- role: 'textbox',
109
- // PIE-owned hook for external consumers - see PIE-1016. Query [data-pie-editor], or
110
- // [data-pie-editor][contenteditable="true"] to skip read-only renders.
111
- 'data-pie-editor': 'true',
112
- // Without these the browser spellchecks the editor regardless of the spellCheck prop.
113
- spellcheck: spellCheckEnabled ? 'true' : 'false',
114
- autocorrect: spellCheckEnabled ? 'on' : 'off',
115
- autocapitalize: spellCheckEnabled ? 'on' : 'off',
116
- });
117
-
118
99
  export const EditableHtml = (props) => {
119
100
  const { showParagraphs, separateParagraphs } = props.pluginProps || {};
120
101
  const [pendingImages, setPendingImages] = useState([]);
@@ -209,13 +190,6 @@ export const EditableHtml = (props) => {
209
190
  node: 'paragraph',
210
191
  notAfter: ['paragraph', 'div'],
211
192
  },
212
- // StarterKit bundles the link extension, which turns anything that looks like a URL into
213
- // a hyperlink as it is typed or pasted. See PIE-980 - we don't want that in any editor.
214
- // The mark itself stays registered so links already in the markup keep round-tripping.
215
- link: {
216
- autolink: false,
217
- linkOnPaste: false,
218
- },
219
193
  }),
220
194
  ExtendedListItem,
221
195
  DivNode,
@@ -321,17 +295,11 @@ export const EditableHtml = (props) => {
321
295
  }),
322
296
  ];
323
297
 
324
- // Callers that don't pass spellCheck keep the browser default (on), as they did with the
325
- // slate-based editor. Players that have to default to off - extended-text-entry, see
326
- // PIE-978 - resolve that in their controller and pass an explicit false.
327
- const spellCheckEnabled = props.spellCheck !== false;
328
-
329
298
  const editor = useEditor(
330
299
  {
331
300
  extensions,
332
301
  immediatelyRender: false,
333
302
  editorProps: {
334
- attributes: editorAttributes(spellCheckEnabled),
335
303
  handleKeyDown(view, event) {
336
304
  if (props.onKeyDown) {
337
305
  return props.onKeyDown(event);
@@ -375,26 +343,6 @@ export const EditableHtml = (props) => {
375
343
  editor?.setEditable(!props.disabled);
376
344
  }, [props.disabled, editor]);
377
345
 
378
- // useEditor only re-applies options on its own when it is called with an empty dependency
379
- // array, and this call site depends on charactersLimit, so a spellCheck change on a mounted
380
- // editor has to be pushed in. The rest of editorProps stays as the editor already has it, and
381
- // the attributes are merged rather than replaced so an attribute contributed by Tiptap or an
382
- // extension can't be dropped the way role="textbox" was - see PIE-1015.
383
- useEffect(() => {
384
- if (!editor) {
385
- return;
386
- }
387
-
388
- const currentEditorProps = editor.options?.editorProps;
389
- const attributes = { ...currentEditorProps?.attributes, ...editorAttributes(spellCheckEnabled) };
390
-
391
- if (currentEditorProps?.attributes?.spellcheck === attributes.spellcheck) {
392
- return;
393
- }
394
-
395
- editor.setOptions({ editorProps: { ...currentEditorProps, attributes } });
396
- }, [spellCheckEnabled, editor]);
397
-
398
346
  useEffect(() => {
399
347
  if (!editor) {
400
348
  return;
@@ -2,7 +2,6 @@ import React, { useMemo, useState } from 'react';
2
2
  import classNames from 'classnames';
3
3
  import { styled, useTheme } from '@mui/material/styles';
4
4
  import { NodeSelection } from 'prosemirror-state';
5
- import { color } from '@pie-lib/render-ui';
6
5
 
7
6
  import Bold from '@mui/icons-material/FormatBold';
8
7
  import Italic from '@mui/icons-material/FormatItalic';
@@ -35,15 +34,7 @@ import { CharacterIcon, CharacterPicker } from './CharacterPicker';
35
34
  import { DoneButton } from './common/done-button';
36
35
 
37
36
  const SuperscriptIcon = () => (
38
- <svg
39
- xmlns="http://www.w3.org/2000/svg"
40
- height="24px"
41
- viewBox="0 0 24 24"
42
- width="24px"
43
- fill="none"
44
- aria-hidden="true"
45
- focusable="false"
46
- >
37
+ <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="none">
47
38
  <path
48
39
  d="M22,7h-2v1h3v1h-4V7c0-0.55,0.45-1,1-1h2V5h-3V4h3c0.55,0,1,0.45,1,1v1C23,6.55,22.55,7,22,7z M5.88,20h2.66l3.4-5.42h0.12 l3.4,5.42h2.66l-4.65-7.27L17.81,6h-2.68l-3.07,4.99h-0.12L8.85,6H6.19l4.32,6.73L5.88,20z"
49
40
  fill="currentColor"
@@ -52,15 +43,7 @@ const SuperscriptIcon = () => (
52
43
  );
53
44
 
54
45
  const SubscriptIcon = () => (
55
- <svg
56
- xmlns="http://www.w3.org/2000/svg"
57
- height="24px"
58
- viewBox="0 0 24 24"
59
- width="24px"
60
- fill="none"
61
- aria-hidden="true"
62
- focusable="false"
63
- >
46
+ <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="none">
64
47
  <path
65
48
  d="M22,18h-2v1h3v1h-4v-2c0-0.55,0.45-1,1-1h2v-1h-3v-1h3c0.55,0,1,0.45,1,1v1C23,17.55,22.55,18,22,18z M5.88,18h2.66 l3.4-5.42h0.12l3.4,5.42h2.66l-4.65-7.27L17.81,4h-2.68l-3.07,4.99h-0.12L8.85,4H6.19l4.32,6.73L5.88,18z"
66
49
  fill="currentColor"
@@ -76,8 +59,6 @@ const HeadingIcon = () => (
76
59
  fill="none"
77
60
  xmlns="http://www.w3.org/2000/svg"
78
61
  style={{ width: '20px', height: '18px' }}
79
- aria-hidden="true"
80
- focusable="false"
81
62
  >
82
63
  <path
83
64
  d="M27 4V24H29C29.5 24 30 24.5 30 25V27C30 27.5625 29.5 28 29 28H19C18.4375 28 18 27.5625 18 27V25C18 24.5 18.4375 24 19 24H21V16H9V24H11C11.5 24 12 24.5 12 25V27C12 27.5625 11.5 28 11 28H1C0.4375 28 0 27.5625 0 27V25C0 24.5 0.4375 24 1 24H3V4H1C0.4375 4 0 3.5625 0 3V1C0 0.5 0.4375 0 1 0H11C11.5 0 12 0.5 12 1V3C12 3.5625 11.5 4 11 4H9V12H21V4H19C18.4375 4 18 3.5625 18 3V1C18 0.5 18.4375 0 19 0H29C29.5 0 30 0.5 30 1V3C30 3.5625 29.5 4 29 4H27Z"
@@ -185,7 +166,6 @@ function MenuBar({
185
166
  () => [
186
167
  {
187
168
  icon: <GridOn />,
188
- label: 'Insert table',
189
169
  onClick: (editor) => editor.chain().focus().insertTable({ rows: 2, cols: 2, withHeaderRow: false }).run(),
190
170
  hidden: (state) => !activePlugins?.includes('table') || state.isTable,
191
171
  isActive: (state) => state.isTable,
@@ -193,7 +173,6 @@ function MenuBar({
193
173
  },
194
174
  {
195
175
  icon: <AddRow />,
196
- label: 'Add table row',
197
176
  onClick: (editor) => editor.chain().focus().addRowAfter().run(),
198
177
  hidden: (state) => !(state.isTable && !state.hasTextSelectionInTable),
199
178
  isActive: (state) => state.isTable,
@@ -201,7 +180,6 @@ function MenuBar({
201
180
  },
202
181
  {
203
182
  icon: <RemoveRow />,
204
- label: 'Remove table row',
205
183
  onClick: (editor) => editor.chain().focus().deleteRow().run(),
206
184
  hidden: (state) => !(state.isTable && !state.hasTextSelectionInTable),
207
185
  isActive: (state) => state.isTable,
@@ -209,7 +187,6 @@ function MenuBar({
209
187
  },
210
188
  {
211
189
  icon: <AddColumn />,
212
- label: 'Add table column',
213
190
  onClick: (editor) => editor.chain().focus().addColumnAfter().run(),
214
191
  hidden: (state) => !(state.isTable && !state.hasTextSelectionInTable),
215
192
  isActive: (state) => state.isTable,
@@ -217,7 +194,6 @@ function MenuBar({
217
194
  },
218
195
  {
219
196
  icon: <RemoveColumn />,
220
- label: 'Remove table column',
221
197
  onClick: (editor) => editor.chain().focus().deleteColumn().run(),
222
198
  hidden: (state) => !(state.isTable && !state.hasTextSelectionInTable),
223
199
  isActive: (state) => state.isTable,
@@ -225,7 +201,6 @@ function MenuBar({
225
201
  },
226
202
  {
227
203
  icon: <RemoveTable />,
228
- label: 'Remove table',
229
204
  onClick: (editor) => editor.chain().focus().deleteTable().run(),
230
205
  hidden: (state) => !(state.isTable && !state.hasTextSelectionInTable),
231
206
  isActive: (state) => state.isTable,
@@ -233,8 +208,6 @@ function MenuBar({
233
208
  },
234
209
  {
235
210
  icon: <BorderAll />,
236
- label: 'Table borders',
237
- toggle: true,
238
211
  onClick: (editor) => {
239
212
  const tableAttrs = editor.getAttributes('table');
240
213
 
@@ -251,8 +224,6 @@ function MenuBar({
251
224
  },
252
225
  {
253
226
  icon: <Bold />,
254
- label: 'Bold',
255
- toggle: true,
256
227
  onClick: (editor) => editor.chain().focus().toggleBold().run(),
257
228
  hidden: () => !activePlugins?.includes('bold'),
258
229
  isActive: (state) => state.isBold,
@@ -260,8 +231,6 @@ function MenuBar({
260
231
  },
261
232
  {
262
233
  icon: <Italic />,
263
- label: 'Italic',
264
- toggle: true,
265
234
  onClick: (editor) => editor.chain().focus().toggleItalic().run(),
266
235
  hidden: () => !activePlugins?.includes('italic'),
267
236
  isActive: (state) => state.isItalic,
@@ -269,8 +238,6 @@ function MenuBar({
269
238
  },
270
239
  {
271
240
  icon: <Strikethrough />,
272
- label: 'Strikethrough',
273
- toggle: true,
274
241
  onClick: (editor) => editor.chain().focus().toggleStrike().run(),
275
242
  hidden: () => !activePlugins?.includes('strikethrough'),
276
243
  isActive: (state) => state.isStrike,
@@ -278,8 +245,6 @@ function MenuBar({
278
245
  },
279
246
  {
280
247
  icon: <Code />,
281
- label: 'Code',
282
- toggle: true,
283
248
  onClick: (editor) => editor.chain().focus().toggleCode().run(),
284
249
  hidden: () => !activePlugins?.includes('code'),
285
250
  isActive: (state) => state.isCode,
@@ -287,118 +252,94 @@ function MenuBar({
287
252
  },
288
253
  {
289
254
  icon: <Underline />,
290
- label: 'Underline',
291
- toggle: true,
292
255
  onClick: (editor) => editor.chain().focus().toggleUnderline().run(),
293
256
  hidden: () => !activePlugins?.includes('underline'),
294
257
  isActive: (state) => state.isUnderline,
295
258
  },
296
259
  {
297
260
  icon: <SubscriptIcon />,
298
- label: 'Subscript',
299
- toggle: true,
300
261
  onClick: (editor) => editor.chain().focus().toggleSubscript().run(),
301
262
  hidden: () => !activePlugins?.includes('subscript'),
302
263
  isActive: (state) => state.isSubScript,
303
264
  },
304
265
  {
305
266
  icon: <SuperscriptIcon />,
306
- label: 'Superscript',
307
- toggle: true,
308
267
  onClick: (editor) => editor.chain().focus().toggleSuperscript().run(),
309
268
  hidden: () => !activePlugins?.includes('superscript'),
310
269
  isActive: (state) => state.isSuperScript,
311
270
  },
312
271
  {
313
272
  icon: <ImageIcon />,
314
- label: 'Insert image',
315
273
  hidden: () => !activePlugins?.includes('image'),
316
274
  onClick: (editor) => editor.chain().focus().setImageUploadNode().run(),
317
275
  },
318
276
  {
319
277
  icon: <TheatersIcon />,
320
- label: 'Insert video',
321
278
  hidden: () => !activePlugins?.includes('video'),
322
279
  onClick: (editor) => editor.chain().focus().insertMedia({ type: 'video' }).run(),
323
280
  },
324
281
  {
325
282
  icon: <VolumeUpIcon />,
326
- label: 'Insert audio',
327
283
  hidden: () => !activePlugins?.includes('audio'),
328
284
  onClick: (editor) => editor.chain().focus().insertMedia({ type: 'audio', tag: 'audio' }).run(),
329
285
  },
330
286
  {
331
287
  icon: <CSSIcon />,
332
- label: 'CSS classes',
333
288
  hidden: () => !activePlugins?.includes('css'),
334
289
  onClick: (editor) => editor.commands.openCSSClassDialog(),
335
290
  },
336
291
  {
337
292
  icon: <FormatQuote />,
338
- label: 'Blockquote',
339
- toggle: true,
340
293
  hidden: () => !activePlugins?.includes('blockquote'),
341
294
  onClick: (editor) => editor.chain().focus().toggleBlockquote().run(),
342
295
  isActive: (state) => state.isBlockquote,
343
296
  },
344
297
  {
345
298
  icon: <HeadingIcon />,
346
- label: 'Heading',
347
- toggle: true,
348
299
  hidden: () => !activePlugins?.includes('h3'),
349
300
  onClick: (editor) => editor.chain().focus().toggleHeadingParagraph().run(),
350
301
  isActive: (state) => state.isHeadingParagraph,
351
302
  },
352
303
  {
353
304
  icon: <Functions />,
354
- label: 'Insert math',
355
305
  hidden: () => !activePlugins?.includes('math'),
356
306
  onClick: (editor) => editor.chain().focus().insertMath('').run(),
357
307
  },
358
308
  {
359
309
  icon: <CharacterIcon letter="ñ" />,
360
- label: 'Spanish characters (ñ)',
361
310
  hidden: () => !activePlugins?.includes('languageCharacters'),
362
311
  onClick: () => setShowPicker(spanishConfig),
363
312
  },
364
313
  {
365
314
  icon: <CharacterIcon letter="€" />,
366
- label: 'Special characters (€)',
367
315
  hidden: () => activePlugins?.filter((p) => p === 'languageCharacters').length !== 2,
368
316
  onClick: () => setShowPicker(specialConfig),
369
317
  },
370
318
  {
371
319
  icon: <TextAlignIcon editor={editor} />,
372
- label: 'Text alignment',
373
320
  hidden: () => !activePlugins?.includes('text-align'),
374
321
  onClick: () => {},
375
322
  },
376
323
  {
377
324
  icon: <BulletedListIcon />,
378
- label: 'Bulleted list',
379
- toggle: true,
380
325
  hidden: () => !activePlugins?.includes('bulleted-list'),
381
326
  onClick: (editor) => editor.chain().focus().toggleBulletList().run(),
382
327
  isActive: (state) => state.isBulletList,
383
328
  },
384
329
  {
385
330
  icon: <NumberedListIcon />,
386
- label: 'Numbered list',
387
- toggle: true,
388
331
  hidden: () => !activePlugins?.includes('numbered-list'),
389
332
  onClick: (editor) => editor.chain().focus().toggleOrderedList().run(),
390
333
  isActive: (state) => state.isOrderedList,
391
334
  },
392
335
  {
393
336
  icon: <Undo />,
394
- label: 'Undo',
395
337
  hidden: () => !activePlugins?.includes('undo'),
396
338
  onClick: (editor) => editor.chain().focus().undo().run(),
397
339
  isDisabled: (state) => !state.canUndo,
398
340
  },
399
341
  {
400
342
  icon: <Redo />,
401
- label: 'Redo',
402
343
  hidden: () => !activePlugins?.includes('redo'),
403
344
  onClick: (editor) => editor.chain().focus().redo().run(),
404
345
  isDisabled: (state) => !state.canRedo,
@@ -424,7 +365,6 @@ function MenuBar({
424
365
  onChange?.(editor.getHTML());
425
366
  }}
426
367
  aria-label="Delete response area"
427
- title="Delete response area"
428
368
  >
429
369
  <Delete />
430
370
  </button>
@@ -443,11 +383,7 @@ function MenuBar({
443
383
  return (
444
384
  <button
445
385
  key={index}
446
- type="button"
447
386
  disabled={disabled}
448
- aria-label={btn.label}
449
- title={btn.label}
450
- aria-pressed={btn.toggle ? !!active : undefined}
451
387
  onClick={(e) => {
452
388
  e.preventDefault();
453
389
  btn.onClick(editor);
@@ -461,9 +397,6 @@ function MenuBar({
461
397
  </div>
462
398
  {activePlugins?.includes('responseArea') && (
463
399
  <button
464
- type="button"
465
- aria-label="Insert response area"
466
- title="Insert response area"
467
400
  onClick={() => {
468
401
  editor.chain().focus().insertResponseArea(responseAreaProps.type).run();
469
402
  }}
@@ -529,11 +462,7 @@ const StyledMenuBar = (props) => {
529
462
  );
530
463
  };
531
464
 
532
- const StyledMenuBarRoot = styled('div')(() => ({
533
- // A zero-height slot the toolbar overflows out of, so the toolbar rides normal flow while the
534
- // root's geometry stays as it was.
535
- height: 0,
536
- overflow: 'visible',
465
+ const StyledMenuBarRoot = styled('div')(({ theme }) => ({
537
466
  '& .defaultToolbar': {
538
467
  display: 'flex',
539
468
  width: '100%',
@@ -557,26 +486,20 @@ const StyledMenuBarRoot = styled('div')(() => ({
557
486
  height: '24px',
558
487
  },
559
488
  '&:hover': {
560
- color: color.text(),
489
+ color: 'black',
561
490
  },
562
491
  '&:focus': {
563
- /*
564
- * A focus ring is the one thing a keyboard user cannot do without, so it takes
565
- * the registered focus token rather than a grey. `--pie-button-focus-outline`
566
- * measures at least 3.68:1 against every scheme's own background; MUI's
567
- * grey[700] fell to 1.28:1 on yellow-on-navy.
568
- */
569
- outline: `2px solid ${color.buttonFocusOutline()}`,
492
+ outline: `2px solid ${theme.palette.grey[700]}`,
570
493
  },
571
494
  },
572
495
  '& .active': {
573
- color: color.text(),
496
+ color: 'black',
574
497
  },
575
498
  '& .disabled': {
576
499
  opacity: 0.7,
577
500
  cursor: 'not-allowed',
578
501
  '& :hover': {
579
- color: color.disabled(),
502
+ color: 'grey',
580
503
  },
581
504
  },
582
505
  '& .isActive': {
@@ -584,16 +507,13 @@ const StyledMenuBarRoot = styled('div')(() => ({
584
507
  color: 'var(--white)',
585
508
  },
586
509
  '& .toolbar': {
587
- // Offset from its own in-flow position: no containing block and no static position to resolve,
588
- // either of which a lockdown browser extension can get wrong as the editor grows.
589
- position: 'relative',
590
- top: '5px',
510
+ position: 'absolute',
591
511
  zIndex: 20,
592
512
  cursor: 'pointer',
593
513
  justifyContent: 'space-between',
594
514
  background: 'var(--editable-html-toolbar-bg, #efefef)',
595
515
  minWidth: '280px',
596
- margin: 0,
516
+ margin: '5px 0 0 0',
597
517
  padding: '2px',
598
518
  boxShadow:
599
519
  '0px 1px 5px 0px rgba(0, 0, 0, 0.2), 0px 2px 2px 0px rgba(0, 0, 0, 0.14), 0px 3px 1px -2px rgba(0, 0, 0, 0.12)',
@@ -606,8 +526,6 @@ const StyledMenuBarRoot = styled('div')(() => ({
606
526
  minWidth: '265px',
607
527
  },
608
528
  '& .toolbarTop': {
609
- // Anchored to the root's top, which never depended on the editor's height.
610
- position: 'absolute',
611
529
  top: '-45px',
612
530
  },
613
531
  '& .toolbarRight': {
@@ -89,14 +89,14 @@ const StyledRoot = styled('div', {
89
89
  backgroundColor: color.background(),
90
90
  },
91
91
  '& table:not([border="1"]) tr': {
92
- borderTop: `1px solid ${color.tableGridLight()}`,
92
+ borderTop: '1px solid #dfe2e5',
93
93
  },
94
94
  '& td, th': {
95
95
  padding: '.6em 1em',
96
96
  textAlign: 'center',
97
97
  },
98
98
  '& table:not([border="1"]) td, th': {
99
- border: `1px solid ${color.tableGridLight()}`,
99
+ border: '1px solid #dfe2e5',
100
100
  },
101
101
  ...(error && {
102
102
  border: `2px solid ${theme.palette.error.main} !important`,
@@ -2,27 +2,21 @@ import React from 'react';
2
2
  import { fireEvent, render } from '@testing-library/react';
3
3
  import StyledMenuBar from '../MenuBar';
4
4
 
5
- // flipped to false by the accessibility tests to render the toolbar in its disabled state
6
- let mockCanRunCommands = true;
7
- // node/mark types the accessibility tests want the editor state to report as active
8
- let mockActiveTypes = [];
9
-
10
5
  jest.mock('@tiptap/react', () => ({
11
6
  EditorContent: ({ editor }) => <div data-testid="editor-content" />,
12
7
  useEditorState: ({ selector }) => {
13
- const can = () => ({ run: jest.fn(() => mockCanRunCommands) });
14
8
  const mockEditor = {
15
- isActive: jest.fn((name) => mockActiveTypes.includes(name)),
9
+ isActive: jest.fn(() => false),
16
10
  can: jest.fn(() => ({
17
11
  chain: jest.fn(() => ({
18
- toggleBold: can,
19
- insertTable: can,
20
- toggleItalic: can,
21
- toggleStrike: can,
22
- toggleCode: can,
23
- unsetAllMarks: can,
24
- undo: can,
25
- redo: can,
12
+ toggleBold: jest.fn(() => ({ run: jest.fn(() => true) })),
13
+ insertTable: jest.fn(() => ({ run: jest.fn(() => true) })),
14
+ toggleItalic: jest.fn(() => ({ run: jest.fn(() => true) })),
15
+ toggleStrike: jest.fn(() => ({ run: jest.fn(() => true) })),
16
+ toggleCode: jest.fn(() => ({ run: jest.fn(() => true) })),
17
+ unsetAllMarks: jest.fn(() => ({ run: jest.fn(() => true) })),
18
+ undo: jest.fn(() => ({ run: jest.fn(() => true) })),
19
+ redo: jest.fn(() => ({ run: jest.fn(() => true) })),
26
20
  })),
27
21
  })),
28
22
  getAttributes: jest.fn(() => ({ border: '1' })),
@@ -253,86 +247,4 @@ describe('StyledMenuBar', () => {
253
247
  const { container } = render(<StyledMenuBar {...defaultProps} activePlugins={['table', 'bold', 'italic']} />);
254
248
  expect(container).toBeInTheDocument();
255
249
  });
256
- describe('accessible names', () => {
257
- const allPlugins = [
258
- 'table',
259
- 'bold',
260
- 'italic',
261
- 'strikethrough',
262
- 'code',
263
- 'underline',
264
- 'subscript',
265
- 'superscript',
266
- 'image',
267
- 'video',
268
- 'audio',
269
- 'css',
270
- 'blockquote',
271
- 'h3',
272
- 'math',
273
- 'languageCharacters',
274
- 'bulleted-list',
275
- 'numbered-list',
276
- 'undo',
277
- 'redo',
278
- 'responseArea',
279
- ];
280
-
281
- const accessibleName = (button) => button.getAttribute('aria-label') || button.textContent.trim();
282
-
283
- afterEach(() => {
284
- mockCanRunCommands = true;
285
- mockActiveTypes = [];
286
- });
287
-
288
- it('gives every toolbar button an accessible name', () => {
289
- const { container } = render(<StyledMenuBar {...defaultProps} activePlugins={allPlugins} />);
290
- const buttons = Array.from(container.querySelectorAll('button'));
291
-
292
- expect(buttons.length).toBeGreaterThan(0);
293
- buttons.forEach((button) => expect(accessibleName(button)).not.toEqual(''));
294
- });
295
-
296
- it('keeps accessible names on disabled buttons', () => {
297
- mockCanRunCommands = false;
298
-
299
- const { container } = render(<StyledMenuBar {...defaultProps} activePlugins={allPlugins} />);
300
- const disabled = Array.from(container.querySelectorAll('button[disabled]'));
301
-
302
- expect(disabled.length).toBeGreaterThan(0);
303
- disabled.forEach((button) => expect(accessibleName(button)).not.toEqual(''));
304
- });
305
-
306
- it('labels the formatting and history buttons', () => {
307
- const { getByLabelText } = render(<StyledMenuBar {...defaultProps} activePlugins={allPlugins} />);
308
-
309
- ['Bold', 'Italic', 'Underline', 'Insert table', 'Undo', 'Redo'].forEach((label) =>
310
- expect(getByLabelText(label)).toBeInTheDocument(),
311
- );
312
- });
313
-
314
- it('labels the table buttons', () => {
315
- mockActiveTypes = ['table'];
316
-
317
- const { getByLabelText } = render(<StyledMenuBar {...defaultProps} activePlugins={allPlugins} />);
318
-
319
- ['Add table row', 'Remove table row', 'Add table column', 'Remove table column', 'Remove table'].forEach(
320
- (label) => expect(getByLabelText(label)).toBeInTheDocument(),
321
- );
322
- expect(getByLabelText('Table borders')).toHaveAttribute('aria-pressed', 'true');
323
- });
324
-
325
- it('labels the response area insert button', () => {
326
- const { getByLabelText } = render(<StyledMenuBar {...defaultProps} activePlugins={['responseArea']} />);
327
- expect(getByLabelText('Insert response area')).toBeInTheDocument();
328
- });
329
-
330
- it('exposes toggle state via aria-pressed on mark buttons', () => {
331
- const { getByLabelText } = render(<StyledMenuBar {...defaultProps} activePlugins={allPlugins} />);
332
-
333
- expect(getByLabelText('Bold')).toHaveAttribute('aria-pressed', 'false');
334
- // one-shot actions are not toggles, so they must not report a pressed state
335
- expect(getByLabelText('Undo')).not.toHaveAttribute('aria-pressed');
336
- });
337
- });
338
250
  });