@centreon/ui 23.10.121 → 23.10.122

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@centreon/ui",
3
- "version": "23.10.121",
3
+ "version": "23.10.122",
4
4
  "description": "Centreon UI Components",
5
5
  "scripts": {
6
6
  "eslint": "eslint ./src --ext .js,.jsx,.ts,.tsx --max-warnings 0",
@@ -1,5 +1,6 @@
1
1
  import RichTextEditor from './RichTextEditor';
2
2
  import { standardMacros } from './plugins/ToolbarPlugin/MacrosButton';
3
+ import { labelAlignPicker } from './translatedLabels';
3
4
 
4
5
  interface CheckElementStyleOnRichTextEditorProps {
5
6
  check: boolean;
@@ -47,16 +48,30 @@ describe('Rich Text Editor', () => {
47
48
  it('displays all elements when RichTextEditor is called with required props', () => {
48
49
  cy.findByLabelText('Undo').should('be.visible');
49
50
  cy.findByLabelText('Redo').should('be.visible');
50
- cy.findByLabelText('bold').should('be.visible');
51
- cy.findByLabelText('italic').should('be.visible');
52
- cy.findByLabelText('underline').should('be.visible');
53
- cy.findByLabelText('strikethrough').should('be.visible');
51
+ cy.findByLabelText('align picker').should('be.visible');
52
+ cy.findByLabelText('block').should('be.visible');
53
+ cy.findByLabelText('format').should('be.visible');
54
54
  cy.findByLabelText('link').should('be.visible');
55
+ cy.findByLabelText('list').should('be.visible');
56
+
55
57
  cy.findByLabelText('RichTextEditor')
56
58
  .should('be.visible')
57
59
  .and('have.value', '');
58
60
 
59
61
  cy.findByLabelText('RichTextEditor').parent().contains('Type here...');
62
+
63
+ cy.makeSnapshot();
64
+ });
65
+
66
+ it('displays all format actions when the format button is clicked', () => {
67
+ cy.findByLabelText('format').click();
68
+
69
+ cy.findByLabelText('bold').should('be.visible');
70
+ cy.findByLabelText('italic').should('be.visible');
71
+ cy.findByLabelText('underline').should('be.visible');
72
+ cy.findByLabelText('strikethrough').should('be.visible');
73
+
74
+ cy.makeSnapshot();
60
75
  });
61
76
 
62
77
  it('displays changes with undo redo buttons', () => {
@@ -80,21 +95,36 @@ describe('Rich Text Editor', () => {
80
95
 
81
96
  it('displays changes with bold, italic, underline and strikethrough buttons', () => {
82
97
  cy.get('[data-testid="RichTextEditor"]').type('cypress test');
98
+
83
99
  cy.get('[data-testid="RichTextEditor"]').focus().type('{selectAll}');
84
100
 
85
- cy.get('#bold').click();
86
- cy.get('#italic').click();
87
- cy.get('#underline').click();
88
- cy.get('#strikethrough').click();
101
+ cy.findByLabelText('format').click();
102
+ cy.findByLabelText('bold').click();
103
+
104
+ cy.findByLabelText('format').click();
105
+ cy.findByLabelText('italic').click();
106
+
107
+ cy.findByLabelText('format').click();
108
+ cy.findByLabelText('underline').click();
109
+
110
+ cy.findByLabelText('format').click();
111
+ cy.findByLabelText('strikethrough').click();
89
112
 
90
113
  cy.findByTestId('RichTextEditor').then((element: HTMLElement) => {
91
114
  checkElementStyleOnRichTextEditor({ check: true, element });
92
115
  });
93
116
 
94
- cy.get('#bold').click();
95
- cy.get('#italic').click();
96
- cy.get('#underline').click();
97
- cy.get('#strikethrough').click();
117
+ cy.findByLabelText('format').click();
118
+ cy.findByLabelText('bold').click();
119
+
120
+ cy.findByLabelText('format').click();
121
+ cy.findByLabelText('italic').click();
122
+
123
+ cy.findByLabelText('format').click();
124
+ cy.findByLabelText('underline').click();
125
+
126
+ cy.findByLabelText('format').click();
127
+ cy.findByLabelText('strikethrough').click();
98
128
 
99
129
  cy.findByTestId('RichTextEditor').then((element: HTMLElement) => {
100
130
  checkElementStyleOnRichTextEditor({ check: false, element });
@@ -105,7 +135,7 @@ describe('Rich Text Editor', () => {
105
135
  cy.get('[data-testid="RichTextEditor"]').type('cypress');
106
136
  cy.get('[data-testid="RichTextEditor"]').focus().type('{selectAll}');
107
137
 
108
- cy.get('#link').click();
138
+ cy.findByLabelText('link').click();
109
139
 
110
140
  cy.findByLabelText('Saved link').should('have.text', 'https://');
111
141
 
@@ -130,7 +160,7 @@ describe('Rich Text Editor', () => {
130
160
 
131
161
  cy.get('[data-testid="RichTextEditor"]').focus().type('{selectAll}');
132
162
 
133
- cy.get('#link').click({ force: true });
163
+ cy.findByLabelText('link').click();
134
164
 
135
165
  cy.findByText('cypress')
136
166
  .parent()
@@ -153,7 +183,7 @@ describe('Rich Text Editor', () => {
153
183
  cy.get('[data-testid="RichTextEditor"]').type('cypress');
154
184
  cy.get('[data-testid="RichTextEditor"]').focus().type('{selectAll}');
155
185
 
156
- cy.get('#link').click();
186
+ cy.findByLabelText('link').click();
157
187
 
158
188
  cy.findByLabelText('Saved link').should('have.text', 'https://');
159
189
 
@@ -189,11 +219,11 @@ describe('Rich Text Editor', () => {
189
219
  it('displays editor when editable props is false and an initialState exist', () => {
190
220
  cy.findByLabelText('Undo').should('not.exist');
191
221
  cy.findByLabelText('Redo').should('not.exist');
192
- cy.findByLabelText('bold').should('not.exist');
193
- cy.findByLabelText('italic').should('not.exist');
194
- cy.findByLabelText('underline').should('not.exist');
195
- cy.findByLabelText('strikethrough').should('not.exist');
222
+ cy.findByLabelText(labelAlignPicker).should('not.exist');
223
+ cy.findByLabelText('block').should('not.exist');
224
+ cy.findByLabelText('format').should('not.exist');
196
225
  cy.findByLabelText('link').should('not.exist');
226
+ cy.findByLabelText('list').should('not.exist');
197
227
  });
198
228
  });
199
229
 
@@ -213,11 +243,11 @@ describe('Rich Text Editor', () => {
213
243
  it('displays editor when editable props is false and an initialState exist', () => {
214
244
  cy.findByLabelText('Undo').should('be.disabled');
215
245
  cy.findByLabelText('Redo').should('be.disabled');
216
- cy.findByLabelText('bold').should('be.disabled');
217
- cy.findByLabelText('italic').should('be.disabled');
218
- cy.findByLabelText('underline').should('be.disabled');
219
- cy.findByLabelText('strikethrough').should('be.disabled');
246
+ cy.findByLabelText(labelAlignPicker).should('be.disabled');
247
+ cy.findByLabelText('block').should('be.disabled');
248
+ cy.findByLabelText('format').should('be.disabled');
220
249
  cy.findByLabelText('link').should('be.disabled');
250
+ cy.findByLabelText('list').should('be.disabled');
221
251
  });
222
252
  });
223
253
 
@@ -262,7 +292,6 @@ describe('Rich Text Editor', () => {
262
292
  });
263
293
 
264
294
  it('displays the Macros button when the "displayMacrosButton" prop is set to true', () => {
265
- cy.findByLabelText('Macros').should('be.visible');
266
295
  cy.findByLabelText('Macros').click();
267
296
 
268
297
  standardMacros.forEach((macro) => {
@@ -273,10 +302,10 @@ describe('Rich Text Editor', () => {
273
302
  it('ensures that the selected macro is inserted into the RichTextEditor', () => {
274
303
  cy.get('[data-testid="RichTextEditor"]').type('macro : ');
275
304
 
276
- cy.findByLabelText('Macros').click({});
277
-
278
305
  standardMacros.forEach((macro) => {
279
- cy.findByText(macro).should('be.visible').click({ force: true });
306
+ cy.findByLabelText('Macros').click();
307
+ cy.findByText(macro).click();
308
+
280
309
  cy.get('[data-testid="RichTextEditor"]').should('contain', macro);
281
310
  });
282
311
  });
@@ -291,44 +320,59 @@ describe('Rich Text Editor', () => {
291
320
 
292
321
  const testCases = [
293
322
  {
294
- blockType: 'Heading 1',
295
- tag: 'h1'
296
- },
297
- {
298
- blockType: 'Heading 2',
299
- tag: 'h2'
300
- },
301
- {
302
- blockType: 'Heading 3',
323
+ blockType: 'Huge',
303
324
  tag: 'h3'
304
325
  },
305
326
  {
306
- blockType: 'Heading 4',
307
- tag: 'h4'
327
+ blockType: 'Large',
328
+ tag: 'h5'
308
329
  },
309
330
  {
310
- blockType: 'Heading 5',
311
- tag: 'h5'
331
+ blockType: 'Normal',
332
+ tag: 'p'
312
333
  },
313
334
  {
314
- blockType: 'Heading 6',
335
+ blockType: 'Small',
315
336
  tag: 'h6'
316
- },
337
+ }
338
+ ];
339
+
340
+ testCases.forEach(({ blockType, tag }) => {
341
+ it(`displays ${blockType} when the corresponding block type button is selected`, () => {
342
+ cy.get('[data-testid="RichTextEditor"]').type('Example');
343
+ cy.findByLabelText('block').click();
344
+ cy.findByText(blockType).click();
345
+
346
+ cy.get('[data-testid="RichTextEditor"]').find(tag).should('be.visible');
347
+
348
+ cy.makeSnapshot();
349
+ });
350
+ });
351
+ });
352
+
353
+ describe('List type', () => {
354
+ beforeEach(() => {
355
+ cy.mount({
356
+ Component: <RichTextEditor editable />
357
+ });
358
+ });
359
+
360
+ const testCases = [
317
361
  {
318
- blockType: 'Bullet List',
362
+ listType: 'Unordered List',
319
363
  tag: 'ul'
320
364
  },
321
365
  {
322
- blockType: 'Number List',
366
+ listType: 'Ordered List',
323
367
  tag: 'ol'
324
368
  }
325
369
  ];
326
370
 
327
- testCases.forEach(({ blockType, tag }) => {
328
- it(`displays ${blockType} when the corresponding block type button is selected`, () => {
371
+ testCases.forEach(({ listType, tag }) => {
372
+ it(`displays ${listType} when the corresponding list type button is selected`, () => {
329
373
  cy.get('[data-testid="RichTextEditor"]').type('Example');
330
- cy.findByTestId('Block type').click();
331
- cy.findByText(blockType).click();
374
+ cy.findByLabelText('list').click();
375
+ cy.findByLabelText(listType).click();
332
376
 
333
377
  cy.get('[data-testid="RichTextEditor"]').find(tag).should('be.visible');
334
378
 
@@ -344,14 +388,18 @@ describe('Rich Text Editor', () => {
344
388
  });
345
389
  });
346
390
 
347
- const testCases = ['Center', 'Right'];
391
+ const testCases = ['Center', 'Right', 'Left'];
348
392
 
349
393
  testCases.forEach((label) => {
350
- it(`aligns the text to the ${label} when the corresponding button is clicked`, () => {
394
+ it(`aligns the text to the ${label} when the corresponding button was clicked`, () => {
351
395
  cy.get('[data-testid="RichTextEditor"]').type('Example');
352
396
 
353
- cy.findByLabelText('left').click();
354
- cy.contains(label).click();
397
+ cy.findByLabelText(labelAlignPicker).click();
398
+ cy.findByLabelText(label).click();
399
+
400
+ cy.findByText('Example')
401
+ .parent()
402
+ .should('have.css', 'text-align', label.toLowerCase());
355
403
 
356
404
  cy.makeSnapshot();
357
405
  });
@@ -92,9 +92,9 @@ const useStyles = makeStyles<{ toolbarPositions: 'start' | 'end' }>()(
92
92
  lineHeight: theme.typography.h5.lineHeight
93
93
  },
94
94
  h6: {
95
- fontSize: theme.typography.h6.fontSize,
96
- fontWeight: theme.typography.h6.fontWeight,
97
- lineHeight: theme.typography.h6.lineHeight
95
+ fontSize: theme.typography.body2.fontSize,
96
+ fontWeight: theme.typography.body2.fontWeight,
97
+ lineHeight: theme.typography.body2.lineHeight
98
98
  },
99
99
  italic: {
100
100
  fontStyle: 'italic'
@@ -102,6 +102,11 @@ const useStyles = makeStyles<{ toolbarPositions: 'start' | 'end' }>()(
102
102
  link: {
103
103
  color: theme.palette.primary.main
104
104
  },
105
+ paragraph: {
106
+ fontSize: theme.typography.body1.fontSize,
107
+ fontWeight: theme.typography.body1.fontWeight,
108
+ lineHeight: theme.typography.body1.lineHeight
109
+ },
105
110
  strikethrough: {
106
111
  textDecoration: 'line-through'
107
112
  },
@@ -162,6 +167,7 @@ const RichTextEditor = ({
162
167
  h6: classes.h6
163
168
  },
164
169
  link: classes.link,
170
+ paragraph: classes.paragraph,
165
171
  text: {
166
172
  bold: classes.bold,
167
173
  italic: classes.italic,
@@ -18,8 +18,9 @@ import { SvgIconTypeMap } from '@mui/material';
18
18
 
19
19
  import { Menu } from '../../../components';
20
20
  import { getSelectedNode } from '../../utils/getSelectedNode';
21
+ import { labelAlignPicker } from '../../translatedLabels';
21
22
 
22
- import { useAlignPickerStyles } from './ToolbarPlugin.styles';
23
+ import { useStyles } from './ToolbarPlugin.styles';
23
24
 
24
25
  const formatOptions: Array<{
25
26
  Icon: OverridableComponent<SvgIconTypeMap<object, 'svg'>>;
@@ -48,7 +49,7 @@ interface Props {
48
49
  }
49
50
 
50
51
  const AlignPicker = ({ disabled }: Props): JSX.Element => {
51
- const { classes } = useAlignPickerStyles();
52
+ const { classes } = useStyles();
52
53
 
53
54
  const [elementFormat, setElementFormat] = useState<ElementFormatType>('left');
54
55
 
@@ -88,25 +89,25 @@ const AlignPicker = ({ disabled }: Props): JSX.Element => {
88
89
 
89
90
  return (
90
91
  <Menu>
91
- <Menu.Button ariaLabel={elementFormat} disabled={disabled}>
92
- {selectedFormat && (
93
- <div className={classes.button}>
94
- <selectedFormat.Icon /> {selectedFormat.label}
95
- </div>
96
- )}
92
+ <Menu.Button
93
+ ariaLabel={labelAlignPicker}
94
+ className={classes.button}
95
+ disabled={disabled}
96
+ >
97
+ {selectedFormat && <selectedFormat.Icon />}
97
98
  </Menu.Button>
98
- <Menu.Items>
99
- {formatOptions.map(({ Icon, label, value }) => (
100
- <Menu.Item
101
- isActive={equals(value, elementFormat)}
102
- key={value}
103
- onClick={dispatchAlignment(value)}
104
- >
105
- <div className={classes.option}>
106
- <Icon /> {label}
107
- </div>
108
- </Menu.Item>
109
- ))}
99
+ <Menu.Items className={classes.menuItems}>
100
+ <div className={classes.menu}>
101
+ {formatOptions.map(({ Icon, value, label }) => (
102
+ <Menu.Item
103
+ isActive={equals(value, elementFormat)}
104
+ key={value}
105
+ onClick={dispatchAlignment(value)}
106
+ >
107
+ <Icon aria-label={label} />
108
+ </Menu.Item>
109
+ ))}
110
+ </div>
110
111
  </Menu.Items>
111
112
  </Menu>
112
113
  );
@@ -1,13 +1,6 @@
1
1
  import { useCallback, useEffect, useState } from 'react';
2
2
 
3
3
  import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
4
- import {
5
- $isListNode,
6
- INSERT_ORDERED_LIST_COMMAND,
7
- INSERT_UNORDERED_LIST_COMMAND,
8
- ListNode,
9
- REMOVE_LIST_COMMAND
10
- } from '@lexical/list';
11
4
  import {
12
5
  $createParagraphNode,
13
6
  $getSelection,
@@ -23,43 +16,27 @@ import {
23
16
  } from '@lexical/rich-text';
24
17
  import { $setBlocksType } from '@lexical/selection';
25
18
  import { T, always, cond, equals, isNil } from 'ramda';
26
- import {
27
- $findMatchingParent,
28
- $getNearestNodeOfType,
29
- mergeRegister
30
- } from '@lexical/utils';
19
+ import { $findMatchingParent, mergeRegister } from '@lexical/utils';
20
+ import { useTranslation } from 'react-i18next';
31
21
 
32
- import { SingleAutocompleteField } from '../../..';
22
+ import TextSizeIcon from '@mui/icons-material/TextFields';
33
23
 
34
- import { useBlockButtonsStyles } from './ToolbarPlugin.styles';
24
+ import { Menu } from '../../../components';
25
+
26
+ import { useStyles } from './ToolbarPlugin.styles';
35
27
 
36
28
  interface Props {
37
29
  disabled: boolean;
38
30
  }
39
31
 
40
32
  const blockTypeToBlockName = {
41
- bullet: 'Bullet List',
42
- h1: 'Heading 1',
43
- h2: 'Heading 2',
44
- h3: 'Heading 3',
45
- h4: 'Heading 4',
46
- h5: 'Heading 5',
47
- h6: 'Heading 6',
48
- number: 'Number List',
33
+ h3: 'Huge',
34
+ h5: 'Large',
35
+ h6: 'Small',
49
36
  paragraph: 'Normal'
50
37
  };
51
38
 
52
- const blockTypes = [
53
- 'h1',
54
- 'h2',
55
- 'h3',
56
- 'h4',
57
- 'h5',
58
- 'h6',
59
- 'bullet',
60
- 'number',
61
- 'paragraph'
62
- ];
39
+ const blockTypes = ['h3', 'h5', 'paragraph', 'h6'];
63
40
 
64
41
  const blockTypeOptions = blockTypes.map((blockType) => ({
65
42
  id: blockType,
@@ -67,7 +44,8 @@ const blockTypeOptions = blockTypes.map((blockType) => ({
67
44
  }));
68
45
 
69
46
  const BlockButtons = ({ disabled }: Props): JSX.Element => {
70
- const { classes } = useBlockButtonsStyles();
47
+ const { classes } = useStyles();
48
+ const { t } = useTranslation();
71
49
 
72
50
  const [blockType, setBlockType] =
73
51
  useState<keyof typeof blockTypeToBlockName>('paragraph');
@@ -81,22 +59,6 @@ const BlockButtons = ({ disabled }: Props): JSX.Element => {
81
59
  });
82
60
  };
83
61
 
84
- const formatBulletList = (): void => {
85
- if (blockType !== 'bullet') {
86
- editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined);
87
- } else {
88
- editor.dispatchCommand(REMOVE_LIST_COMMAND, undefined);
89
- }
90
- };
91
-
92
- const formatNumberedList = (): void => {
93
- if (blockType !== 'number') {
94
- editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, undefined);
95
- } else {
96
- editor.dispatchCommand(REMOVE_LIST_COMMAND, undefined);
97
- }
98
- };
99
-
100
62
  const formatHeading = (headingSize: HeadingTagType): void => {
101
63
  if (blockType !== headingSize) {
102
64
  editor.update(() => {
@@ -108,18 +70,13 @@ const BlockButtons = ({ disabled }: Props): JSX.Element => {
108
70
  }
109
71
  };
110
72
 
111
- const changeBlockType = (_, newBlockType): void => {
73
+ const changeBlockType = (newBlockType): void => {
112
74
  const formatFunction = cond<Array<string>, (value?) => void>([
113
- [equals('bullet'), always(formatBulletList)],
114
- [equals('number'), always(formatNumberedList)],
115
- [equals('h1'), always(() => formatHeading('h1'))],
116
- [equals('h2'), always(() => formatHeading('h2'))],
117
75
  [equals('h3'), always(() => formatHeading('h3'))],
118
- [equals('h4'), always(() => formatHeading('h4'))],
119
76
  [equals('h5'), always(() => formatHeading('h5'))],
120
77
  [equals('h6'), always(() => formatHeading('h6'))],
121
78
  [T, always(formatParagraph)]
122
- ])(newBlockType?.id || '');
79
+ ])(newBlockType || '');
123
80
 
124
81
  formatFunction();
125
82
  };
@@ -142,23 +99,13 @@ const BlockButtons = ({ disabled }: Props): JSX.Element => {
142
99
  return;
143
100
  }
144
101
 
145
- if ($isListNode(element)) {
146
- const parentList = $getNearestNodeOfType(anchorNode, ListNode);
147
- const type = parentList
148
- ? parentList.getListType()
149
- : element.getListType();
150
- setBlockType(type);
151
-
152
- return;
153
- }
154
102
  const type = $isHeadingNode(element) ? element.getTag() : element.getType();
103
+
155
104
  if (type in blockTypeToBlockName) {
156
105
  setBlockType(type as keyof typeof blockTypeToBlockName);
157
106
  }
158
107
  }, [editor]);
159
108
 
160
- const value = blockTypeOptions.find((option) => option.id === blockType);
161
-
162
109
  useEffect(() => {
163
110
  return editor.registerCommand(
164
111
  SELECTION_CHANGE_COMMAND,
@@ -182,15 +129,26 @@ const BlockButtons = ({ disabled }: Props): JSX.Element => {
182
129
  }, [editor, updateToolbar]);
183
130
 
184
131
  return (
185
- <SingleAutocompleteField
186
- className={classes.autocomplete}
187
- dataTestId="Block type"
188
- disabled={disabled}
189
- label=""
190
- options={blockTypeOptions}
191
- value={value}
192
- onChange={changeBlockType}
193
- />
132
+ <Menu>
133
+ <Menu.Button
134
+ ariaLabel="block"
135
+ className={classes.button}
136
+ disabled={disabled}
137
+ >
138
+ <TextSizeIcon />
139
+ </Menu.Button>
140
+ <Menu.Items className={classes.menuItems}>
141
+ {blockTypeOptions.map(({ id, name }) => (
142
+ <Menu.Item
143
+ isActive={equals(id, blockType)}
144
+ key={name}
145
+ onClick={() => changeBlockType(id)}
146
+ >
147
+ {t(name)}
148
+ </Menu.Item>
149
+ ))}
150
+ </Menu.Items>
151
+ </Menu>
194
152
  );
195
153
  };
196
154
 
@@ -1,76 +1,38 @@
1
1
  import { useState, useCallback, useEffect, useMemo } from 'react';
2
2
 
3
- import { makeStyles } from 'tss-react/mui';
4
3
  import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
5
4
  import {
6
5
  $getSelection,
7
6
  $isRangeSelection,
8
7
  FORMAT_TEXT_COMMAND,
9
- RangeSelection,
10
8
  SELECTION_CHANGE_COMMAND,
11
- TextFormatType,
12
- ElementNode,
13
- TextNode
9
+ TextFormatType
14
10
  } from 'lexical';
15
- import { $isLinkNode, TOGGLE_LINK_COMMAND } from '@lexical/link';
16
- import { $isAtNodeEnd } from '@lexical/selection';
17
11
  import { mergeRegister } from '@lexical/utils';
18
- import { useAtom } from 'jotai';
19
12
 
13
+ import FormatTextIcon from '@mui/icons-material/FormatColorText';
20
14
  import FormatBoldIcon from '@mui/icons-material/FormatBold';
21
15
  import FormatItalicIcon from '@mui/icons-material/FormatItalic';
22
16
  import FormatUnderlinedIcon from '@mui/icons-material/FormatUnderlined';
23
17
  import StrikethroughSIcon from '@mui/icons-material/StrikethroughS';
24
- import LinkIcon from '@mui/icons-material/Link';
25
- import { alpha } from '@mui/material';
26
18
 
27
- import { IconButton } from '../../..';
28
- import { isInsertingLinkAtom } from '../../atoms';
19
+ import { Menu } from '../../../components';
29
20
 
30
- const LowPriority = 1;
21
+ import { useStyles } from './ToolbarPlugin.styles';
31
22
 
32
- const useStyles = makeStyles()((theme) => ({
33
- buttonSelected: {
34
- backgroundColor: alpha(
35
- theme.palette.primary.main,
36
- theme.palette.action.activatedOpacity
37
- )
38
- },
39
- container: {
40
- columnGap: theme.spacing(1),
41
- display: 'flex',
42
- marginBottom: theme.spacing(1)
43
- }
44
- }));
45
-
46
- const getSelectedNode = (selection: RangeSelection): ElementNode | TextNode => {
47
- const { anchor } = selection;
48
- const { focus } = selection;
49
- const anchorNode = selection.anchor.getNode();
50
- const focusNode = selection.focus.getNode();
51
- if (anchorNode === focusNode) {
52
- return anchorNode;
53
- }
54
- const isBackward = selection.isBackward();
55
- if (isBackward) {
56
- return $isAtNodeEnd(focus) ? anchorNode : focusNode;
57
- }
58
-
59
- return $isAtNodeEnd(anchor) ? focusNode : anchorNode;
60
- };
23
+ const LowPriority = 1;
61
24
 
62
25
  interface Props {
63
26
  disabled: boolean;
64
27
  }
65
28
 
66
29
  const FormatButtons = ({ disabled }: Props): JSX.Element => {
67
- const { classes, cx } = useStyles();
30
+ const { classes } = useStyles();
68
31
 
69
32
  const [isBold, setIsBold] = useState(false);
70
33
  const [isItalic, setIsItalic] = useState(false);
71
34
  const [isUnderline, setIsUnderline] = useState(false);
72
35
  const [isStrikeThrough, setIsStrikeThrough] = useState(false);
73
- const [isLink, setIsLink] = useAtom(isInsertingLinkAtom);
74
36
 
75
37
  const [editor] = useLexicalComposerContext();
76
38
 
@@ -83,24 +45,8 @@ const FormatButtons = ({ disabled }: Props): JSX.Element => {
83
45
  setIsItalic(selection.hasFormat('italic'));
84
46
  setIsUnderline(selection.hasFormat('underline'));
85
47
  setIsStrikeThrough(selection.hasFormat('strikethrough'));
86
-
87
- const node = getSelectedNode(selection);
88
- const parent = node.getParent();
89
- if ($isLinkNode(parent) || $isLinkNode(node)) {
90
- setIsLink(true);
91
- } else {
92
- setIsLink(false);
93
- }
94
48
  }, [editor]);
95
49
 
96
- const insertLink = useCallback(() => {
97
- if (!isLink) {
98
- editor.dispatchCommand(TOGGLE_LINK_COMMAND, 'https://');
99
- } else {
100
- editor.dispatchCommand(TOGGLE_LINK_COMMAND, null);
101
- }
102
- }, [editor, isLink]);
103
-
104
50
  useEffect(() => {
105
51
  return mergeRegister(
106
52
  editor.registerCommand(
@@ -144,34 +90,34 @@ const FormatButtons = ({ disabled }: Props): JSX.Element => {
144
90
  isSelected: isStrikeThrough,
145
91
  onClickFunction: toggleTextFormat('strikethrough'),
146
92
  type: 'strikethrough'
147
- },
148
- {
149
- Icon: LinkIcon,
150
- isSelected: isLink,
151
- onClickFunction: insertLink,
152
- type: 'link'
153
93
  }
154
94
  ],
155
- [isBold, isItalic, isUnderline, isStrikeThrough, isLink]
95
+ [isBold, isItalic, isUnderline, isStrikeThrough]
156
96
  );
157
97
 
158
98
  return (
159
- <>
160
- {formatButtons.map(({ Icon, onClickFunction, isSelected, type }) => (
161
- <IconButton
162
- ariaLabel={type}
163
- className={cx(isSelected && classes.buttonSelected)}
164
- disabled={disabled}
165
- key={type}
166
- size="medium"
167
- title={type}
168
- tooltipPlacement="top"
169
- onClick={onClickFunction}
170
- >
171
- <Icon />
172
- </IconButton>
173
- ))}
174
- </>
99
+ <Menu>
100
+ <Menu.Button
101
+ ariaLabel="format"
102
+ className={classes.button}
103
+ disabled={disabled}
104
+ >
105
+ <FormatTextIcon />
106
+ </Menu.Button>
107
+ <Menu.Items className={classes.menuItems}>
108
+ <div className={classes.menu}>
109
+ {formatButtons.map(({ Icon, onClickFunction, isSelected, type }) => (
110
+ <Menu.Item
111
+ isActive={isSelected}
112
+ key={type}
113
+ onClick={onClickFunction}
114
+ >
115
+ <Icon aria-label={type} />
116
+ </Menu.Item>
117
+ ))}
118
+ </div>
119
+ </Menu.Items>
120
+ </Menu>
175
121
  );
176
122
  };
177
123
 
@@ -0,0 +1,106 @@
1
+ import { useCallback, useEffect } from 'react';
2
+
3
+ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
4
+ import {
5
+ $getSelection,
6
+ $isRangeSelection,
7
+ RangeSelection,
8
+ SELECTION_CHANGE_COMMAND,
9
+ ElementNode,
10
+ TextNode
11
+ } from 'lexical';
12
+ import { $isLinkNode, TOGGLE_LINK_COMMAND } from '@lexical/link';
13
+ import { $isAtNodeEnd } from '@lexical/selection';
14
+ import { mergeRegister } from '@lexical/utils';
15
+ import { useAtom } from 'jotai';
16
+
17
+ import LinkIcon from '@mui/icons-material/Link';
18
+
19
+ import { IconButton } from '../../..';
20
+ import { isInsertingLinkAtom } from '../../atoms';
21
+
22
+ import { useStyles } from './ToolbarPlugin.styles';
23
+
24
+ const LowPriority = 1;
25
+
26
+ const getSelectedNode = (selection: RangeSelection): ElementNode | TextNode => {
27
+ const { anchor } = selection;
28
+ const { focus } = selection;
29
+ const anchorNode = selection.anchor.getNode();
30
+ const focusNode = selection.focus.getNode();
31
+ if (anchorNode === focusNode) {
32
+ return anchorNode;
33
+ }
34
+ const isBackward = selection.isBackward();
35
+ if (isBackward) {
36
+ return $isAtNodeEnd(focus) ? anchorNode : focusNode;
37
+ }
38
+
39
+ return $isAtNodeEnd(anchor) ? focusNode : anchorNode;
40
+ };
41
+
42
+ interface Props {
43
+ disabled: boolean;
44
+ }
45
+
46
+ const LinkButton = ({ disabled }: Props): JSX.Element => {
47
+ const { classes, cx } = useStyles();
48
+
49
+ const [isLink, setIsLink] = useAtom(isInsertingLinkAtom);
50
+
51
+ const [editor] = useLexicalComposerContext();
52
+
53
+ const updateToolbar = useCallback((): void => {
54
+ const selection = $getSelection();
55
+ if (!$isRangeSelection(selection)) {
56
+ return;
57
+ }
58
+
59
+ const node = getSelectedNode(selection);
60
+ const parent = node.getParent();
61
+ if ($isLinkNode(parent) || $isLinkNode(node)) {
62
+ setIsLink(true);
63
+ } else {
64
+ setIsLink(false);
65
+ }
66
+ }, [editor]);
67
+
68
+ const insertLink = useCallback(() => {
69
+ if (!isLink) {
70
+ editor.dispatchCommand(TOGGLE_LINK_COMMAND, 'https://');
71
+ } else {
72
+ editor.dispatchCommand(TOGGLE_LINK_COMMAND, null);
73
+ }
74
+ }, [editor, isLink]);
75
+
76
+ useEffect(() => {
77
+ return mergeRegister(
78
+ editor.registerCommand(
79
+ SELECTION_CHANGE_COMMAND,
80
+ () => {
81
+ updateToolbar();
82
+
83
+ return false;
84
+ },
85
+ LowPriority
86
+ )
87
+ );
88
+ }, [editor, updateToolbar]);
89
+
90
+ return (
91
+ <IconButton
92
+ ariaLabel="link"
93
+ className={cx(classes.button, { [classes.buttonSelected]: isLink })}
94
+ disabled={disabled}
95
+ key="link"
96
+ size="medium"
97
+ title="link"
98
+ tooltipPlacement="top"
99
+ onClick={insertLink}
100
+ >
101
+ <LinkIcon />
102
+ </IconButton>
103
+ );
104
+ };
105
+
106
+ export default LinkButton;
@@ -0,0 +1,155 @@
1
+ import { useCallback, useEffect, useState } from 'react';
2
+
3
+ import {
4
+ $isListNode,
5
+ INSERT_ORDERED_LIST_COMMAND,
6
+ INSERT_UNORDERED_LIST_COMMAND,
7
+ ListNode,
8
+ REMOVE_LIST_COMMAND
9
+ } from '@lexical/list';
10
+ import { $getSelection, $isRootOrShadowRoot } from 'lexical';
11
+ import {
12
+ $findMatchingParent,
13
+ $getNearestNodeOfType,
14
+ mergeRegister
15
+ } from '@lexical/utils';
16
+ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
17
+ import { equals, isNil } from 'ramda';
18
+ import { useTranslation } from 'react-i18next';
19
+
20
+ import UnorderedListIcon from '@mui/icons-material/FormatListBulleted';
21
+ import OrderedListIcon from '@mui/icons-material/FormatListNumbered';
22
+
23
+ import { Menu } from '../../../components';
24
+ import { labelOrderedList, labelUnorderedList } from '../../translatedLabels';
25
+
26
+ import { useStyles } from './ToolbarPlugin.styles';
27
+
28
+ const options = [
29
+ {
30
+ Icon: UnorderedListIcon,
31
+ label: labelUnorderedList,
32
+ value: 'bullet'
33
+ },
34
+ {
35
+ Icon: OrderedListIcon,
36
+ label: labelOrderedList,
37
+ value: 'number'
38
+ }
39
+ ];
40
+
41
+ interface Props {
42
+ disabled?: boolean;
43
+ }
44
+
45
+ const ListButton = ({ disabled }: Props): JSX.Element => {
46
+ const { classes } = useStyles();
47
+ const { t } = useTranslation();
48
+
49
+ const [editor] = useLexicalComposerContext();
50
+
51
+ const [elementList, setElementList] = useState(null);
52
+
53
+ const formatBulletList = (): void => {
54
+ if (elementList !== 'bullet') {
55
+ editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined);
56
+ } else {
57
+ editor.dispatchCommand(REMOVE_LIST_COMMAND, undefined);
58
+ }
59
+ };
60
+
61
+ const formatNumberedList = (): void => {
62
+ if (elementList !== 'number') {
63
+ editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, undefined);
64
+ } else {
65
+ editor.dispatchCommand(REMOVE_LIST_COMMAND, undefined);
66
+ }
67
+ };
68
+
69
+ const formatList = (type): void => {
70
+ if (equals(type, elementList)) {
71
+ setElementList(null);
72
+ }
73
+
74
+ if (equals(type, 'bullet')) {
75
+ formatBulletList();
76
+
77
+ return;
78
+ }
79
+ formatNumberedList();
80
+ };
81
+
82
+ const updateToolbar = useCallback(() => {
83
+ const selection = $getSelection();
84
+ const anchorNode = selection?.anchor.getNode();
85
+ const element = equals(anchorNode?.getKey(), 'root')
86
+ ? anchorNode
87
+ : $findMatchingParent(anchorNode, (e) => {
88
+ const parent = e.getParent();
89
+
90
+ return parent !== null && $isRootOrShadowRoot(parent);
91
+ }) || anchorNode?.getTopLevelElementOrThrow();
92
+
93
+ const elementKey = element?.getKey();
94
+ const elementDOM = editor.getElementByKey(elementKey);
95
+
96
+ if (isNil(elementDOM)) {
97
+ return;
98
+ }
99
+
100
+ if ($isListNode(element)) {
101
+ const parentList = $getNearestNodeOfType(anchorNode, ListNode);
102
+ const type = parentList
103
+ ? parentList.getListType()
104
+ : element.getListType();
105
+ setElementList(type);
106
+ }
107
+ }, [editor]);
108
+
109
+ const selectedList = options.find(({ value }) => equals(value, elementList));
110
+
111
+ useEffect(() => {
112
+ return editor.registerUpdateListener(({ editorState }) => {
113
+ editorState.read(() => {
114
+ updateToolbar();
115
+ });
116
+ });
117
+ }, [editor, updateToolbar]);
118
+
119
+ useEffect(() => {
120
+ return mergeRegister(
121
+ editor.registerUpdateListener(({ editorState }) => {
122
+ editorState.read(() => {
123
+ updateToolbar();
124
+ });
125
+ })
126
+ );
127
+ }, [editor, updateToolbar]);
128
+
129
+ return (
130
+ <Menu>
131
+ <Menu.Button
132
+ ariaLabel="list"
133
+ className={classes.button}
134
+ disabled={disabled}
135
+ >
136
+ {selectedList ? <selectedList.Icon /> : <UnorderedListIcon />}
137
+ </Menu.Button>
138
+ <Menu.Items className={classes.menuItems}>
139
+ <div className={classes.menu}>
140
+ {options.map(({ Icon, value, label }) => (
141
+ <Menu.Item
142
+ isActive={equals(value, elementList)}
143
+ key={value}
144
+ onClick={() => formatList(value)}
145
+ >
146
+ <Icon aria-label={t(label)} />
147
+ </Menu.Item>
148
+ ))}
149
+ </div>
150
+ </Menu.Items>
151
+ </Menu>
152
+ );
153
+ };
154
+
155
+ export default ListButton;
@@ -1,13 +1,11 @@
1
- import { useState } from 'react';
2
-
3
1
  import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
4
2
  import { createCommand, LexicalCommand } from 'lexical';
5
3
 
6
4
  import MacrosIcon from '@mui/icons-material/TerminalOutlined';
7
5
 
8
- import { ActionsList, IconButton, PopoverMenu } from '../../..';
6
+ import { Menu } from '../../../components';
9
7
 
10
- import { useStyles } from '.';
8
+ import { useStyles } from './ToolbarPlugin.styles';
11
9
 
12
10
  const LowPriority = 1;
13
11
 
@@ -27,15 +25,12 @@ export const standardMacros = [
27
25
 
28
26
  const MacrosButton = ({ disabled }: Props): JSX.Element => {
29
27
  const { classes } = useStyles();
30
- const [arMacrosDiplayed, setAreMacrosDiplayed] = useState(false);
31
28
 
32
29
  const [editor] = useLexicalComposerContext();
33
30
 
34
31
  const INSERT_MACROS_COMMAND: LexicalCommand<string> = createCommand();
35
32
 
36
- const onClick = (e): void => {
37
- const macro = e.target.id || e.target.textContent.trim();
38
-
33
+ const onClick = (macro): void => {
39
34
  editor.dispatchCommand(INSERT_MACROS_COMMAND, macro);
40
35
  };
41
36
 
@@ -53,38 +48,27 @@ const MacrosButton = ({ disabled }: Props): JSX.Element => {
53
48
  LowPriority
54
49
  );
55
50
 
56
- const displayMacros = (): void => {
57
- setAreMacrosDiplayed(true);
58
- };
59
-
60
- const actions = standardMacros.map((macro) => ({
61
- label: macro,
62
- onClick
63
- }));
64
-
65
51
  return (
66
- <PopoverMenu
67
- icon={
68
- <IconButton
69
- ariaLabel="Macros"
70
- className={classes.macrosButton}
71
- disabled={disabled}
72
- title="Macros"
73
- tooltipPlacement="top"
74
- onClick={displayMacros}
75
- >
76
- <MacrosIcon />
77
- </IconButton>
78
- }
79
- >
80
- {(): JSX.Element => (
81
- <div>
82
- {arMacrosDiplayed && (
83
- <ActionsList actions={actions} listItemClassName={classes.macros} />
84
- )}
85
- </div>
86
- )}
87
- </PopoverMenu>
52
+ <Menu>
53
+ <Menu.Button
54
+ ariaLabel="Macros"
55
+ className={classes.button}
56
+ disabled={disabled}
57
+ >
58
+ <MacrosIcon />
59
+ </Menu.Button>
60
+ <Menu.Items className={classes.menuItems}>
61
+ {standardMacros.map((name) => (
62
+ <Menu.Item
63
+ className={classes.menuItem}
64
+ key={name}
65
+ onClick={(): void => onClick(name)}
66
+ >
67
+ {name}
68
+ </Menu.Item>
69
+ ))}
70
+ </Menu.Items>
71
+ </Menu>
88
72
  );
89
73
  };
90
74
 
@@ -1,22 +1,32 @@
1
1
  import { makeStyles } from 'tss-react/mui';
2
2
 
3
- export const useBlockButtonsStyles = makeStyles()((theme) => ({
4
- autocomplete: {
5
- width: theme.spacing(17)
6
- }
7
- }));
3
+ import { alpha } from '@mui/system';
8
4
 
9
- export const useAlignPickerStyles = makeStyles()((theme) => ({
5
+ export const useStyles = makeStyles()((theme) => ({
10
6
  button: {
11
- alignItems: 'center',
12
- display: 'flex',
13
- flexDirection: 'row',
14
- gap: theme.spacing(1)
7
+ color: theme.palette.text.primary
15
8
  },
16
- option: {
17
- alignItems: 'center',
9
+ buttonSelected: {
10
+ backgroundColor: alpha(
11
+ theme.palette.primary.main,
12
+ theme.palette.action.activatedOpacity
13
+ )
14
+ },
15
+ menu: {
18
16
  display: 'flex',
19
- flexDirection: 'row',
20
- gap: theme.spacing(2)
17
+ flexDirection: 'row'
18
+ },
19
+ menuItem: {
20
+ '&, &.MuiMenuItem-root': {
21
+ fontSize: theme.typography.body2.fontSize
22
+ }
23
+ },
24
+ menuItems: {
25
+ '& > .MuiPaper-root.MuiMenu-paper': {
26
+ minWidth: 0
27
+ },
28
+ '& ul, & ul.MuiMenu-list': {
29
+ padding: 0
30
+ }
21
31
  }
22
32
  }));
@@ -16,6 +16,8 @@ import RedoIcon from '@mui/icons-material/Redo';
16
16
  import { IconButton } from '../../..';
17
17
  import { labelRedo, labelUndo } from '../../translatedLabels';
18
18
 
19
+ import { useStyles } from './ToolbarPlugin.styles';
20
+
19
21
  const LowPriority = 1;
20
22
 
21
23
  interface Props {
@@ -23,6 +25,8 @@ interface Props {
23
25
  }
24
26
 
25
27
  const UndoRedoButtons = ({ disabled }: Props): JSX.Element => {
28
+ const { classes } = useStyles();
29
+
26
30
  const { t } = useTranslation();
27
31
 
28
32
  const [canUndo, setCanUndo] = useState(false);
@@ -68,6 +72,7 @@ const UndoRedoButtons = ({ disabled }: Props): JSX.Element => {
68
72
  <>
69
73
  <IconButton
70
74
  ariaLabel={translatedUndo}
75
+ className={classes.button}
71
76
  disabled={!canUndo || disabled}
72
77
  title={translatedUndo}
73
78
  tooltipPlacement="top"
@@ -77,6 +82,7 @@ const UndoRedoButtons = ({ disabled }: Props): JSX.Element => {
77
82
  </IconButton>
78
83
  <IconButton
79
84
  ariaLabel={translatedRedo}
85
+ className={classes.button}
80
86
  disabled={!canRedo || disabled}
81
87
  title={translatedRedo}
82
88
  tooltipPlacement="top"
@@ -5,8 +5,10 @@ import { Divider } from '@mui/material';
5
5
  import FormatButtons from './FormatButtons';
6
6
  import UndoRedoButtons from './UndoRedoButtons';
7
7
  import MacrosButton from './MacrosButton';
8
- import BlockButtons from './BlockButtons';
9
8
  import AlignPicker from './AlignPicker';
9
+ import ListButton from './ListButton';
10
+ import LinkButton from './LinkButton';
11
+ import BlockButtons from './BlockButtons';
10
12
 
11
13
  interface Props {
12
14
  className?: string;
@@ -22,14 +24,6 @@ export const useStyles = makeStyles()((theme) => ({
22
24
  columnGap: theme.spacing(1),
23
25
  display: 'flex',
24
26
  marginBottom: theme.spacing(1)
25
- },
26
- macros: {
27
- '& span': {
28
- fontSize: theme.typography.caption.fontSize
29
- }
30
- },
31
- macrosButton: {
32
- marginBottom: theme.spacing(0.5)
33
27
  }
34
28
  }));
35
29
 
@@ -51,9 +45,10 @@ const ToolbarPlugin = ({
51
45
  <BlockButtons disabled={disabled} />
52
46
  </>
53
47
  )}
54
- <Divider flexItem orientation="vertical" />
55
48
  <FormatButtons disabled={disabled} />
56
49
  <AlignPicker disabled={disabled} />
50
+ <ListButton disabled={disabled} />
51
+ <LinkButton disabled={disabled} />
57
52
  {displayMacrosButton && (
58
53
  <>
59
54
  <Divider flexItem orientation="vertical" />
@@ -3,3 +3,6 @@ export const labelRedo = 'Redo';
3
3
  export const labelInputLink = 'Insert link';
4
4
  export const labelSavedLink = 'Saved link';
5
5
  export const labelEditLink = 'Edit link';
6
+ export const labelAlignPicker = 'align picker';
7
+ export const labelUnorderedList = 'Unordered List';
8
+ export const labelOrderedList = 'Ordered List';
@@ -15,6 +15,7 @@ import { useStyles } from './MenuButton.styles';
15
15
  type MenuButtonProps = {
16
16
  ariaLabel?: string;
17
17
  children?: ReactNode;
18
+ className?: string;
18
19
  hasArrow?: boolean;
19
20
  isOpen?: boolean;
20
21
  onClick?: (args: { isOpen: boolean }) => void;
@@ -29,9 +30,10 @@ const MenuButton = ({
29
30
  size = 'small',
30
31
  variant = 'ghost',
31
32
  ariaLabel,
33
+ className,
32
34
  ...attr
33
35
  }: MenuButtonProps): ReactElement => {
34
- const { classes } = useStyles();
36
+ const { cx, classes } = useStyles();
35
37
 
36
38
  const { isMenuOpen, setIsMenuOpen, setAnchorEl, onOpen } = useMenu();
37
39
 
@@ -50,7 +52,7 @@ const MenuButton = ({
50
52
  <Button
51
53
  {...attr}
52
54
  aria-label={ariaLabel}
53
- className={classes.menuButton}
55
+ className={cx(classes.menuButton, className)}
54
56
  data-is-active={isMenuOpen}
55
57
  size={size}
56
58
  variant={variant}
@@ -6,6 +6,7 @@ import { useStyles } from './Menu.styles';
6
6
 
7
7
  type MenuItemProps = {
8
8
  children?: ReactNode;
9
+ className?: string;
9
10
  isActive?: boolean;
10
11
  isDisabled?: boolean;
11
12
  onClick?: () => void;
@@ -15,13 +16,14 @@ const MenuItem = ({
15
16
  children,
16
17
  onClick,
17
18
  isActive = false,
18
- isDisabled = false
19
+ isDisabled = false,
20
+ className
19
21
  }: MenuItemProps): ReactElement => {
20
- const { classes } = useStyles();
22
+ const { cx, classes } = useStyles();
21
23
 
22
24
  return (
23
25
  <MuiMenuItem
24
- className={classes.menuItem}
26
+ className={cx(classes.menuItem, className)}
25
27
  data-is-active={isActive}
26
28
  data-is-disabled={isDisabled}
27
29
  disabled={isDisabled}
@@ -7,9 +7,10 @@ import { useMenu } from './useMenu';
7
7
 
8
8
  type MenuItemsProps = {
9
9
  children?: ReactNode | Array<ReactNode>;
10
+ className?: string;
10
11
  };
11
- const MenuItems = ({ children }: MenuItemsProps): ReactElement => {
12
- const { classes } = useStyles();
12
+ const MenuItems = ({ children, className }: MenuItemsProps): ReactElement => {
13
+ const { cx, classes } = useStyles();
13
14
 
14
15
  const { isMenuOpen, setIsMenuOpen, anchorEl, onClose } = useMenu();
15
16
 
@@ -21,7 +22,7 @@ const MenuItems = ({ children }: MenuItemsProps): ReactElement => {
21
22
  return (
22
23
  <MuiMenu
23
24
  anchorEl={anchorEl}
24
- className={classes.menuItems}
25
+ className={cx(classes.menuItems, className)}
25
26
  open={isMenuOpen}
26
27
  variant="menu"
27
28
  onClick={onCloseMenu}