@crystallize/design-system 1.9.0 → 1.11.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 (30) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/index.css +15 -17
  3. package/dist/index.d.ts +10 -3
  4. package/dist/index.js +528 -470
  5. package/dist/index.mjs +477 -423
  6. package/package.json +1 -1
  7. package/src/rich-text-editor/i18n/i18n.test.ts +14 -0
  8. package/src/rich-text-editor/i18n/index.tsx +64 -0
  9. package/src/rich-text-editor/i18n/translations/en.ts +66 -0
  10. package/src/rich-text-editor/i18n/types.ts +62 -0
  11. package/src/rich-text-editor/model/crystallize-to-lexical.ts +29 -1
  12. package/src/rich-text-editor/model/lexical-to-crystallize.ts +54 -29
  13. package/src/rich-text-editor/plugins/ActionsPlugin/index.tsx +5 -22
  14. package/src/rich-text-editor/plugins/CodeActionMenuPlugin/components/CopyButton/index.tsx +4 -1
  15. package/src/rich-text-editor/plugins/CodeActionMenuPlugin/components/PrettierButton/index.tsx +11 -1
  16. package/src/rich-text-editor/plugins/CodeActionMenuPlugin/index.tsx +2 -1
  17. package/src/rich-text-editor/plugins/FloatingLinkEditorPlugin/index.tsx +23 -5
  18. package/src/rich-text-editor/plugins/FloatingTextFormatToolbarPlugin/index.tsx +21 -10
  19. package/src/rich-text-editor/plugins/TabFocusPlugin/index.tsx +4 -12
  20. package/src/rich-text-editor/plugins/TableActionMenuPlugin/index.tsx +23 -14
  21. package/src/rich-text-editor/plugins/ToolbarPlugin/index.tsx +33 -33
  22. package/src/rich-text-editor/plugins/ToolbarPlugin/insert-table.tsx +6 -4
  23. package/src/rich-text-editor/rich-text-editor.css +6 -0
  24. package/src/rich-text-editor/rich-text-editor.stories.tsx +38 -0
  25. package/src/rich-text-editor/rich-text-editor.tsx +15 -9
  26. package/src/rich-text-editor/tests/rich-text-editor-model-conversions.test.tsx +23 -1
  27. package/src/rich-text-editor/types/crystallize-rich-text-types/index.ts +2 -4
  28. package/src/rich-text-editor/ui/LinkPreview.tsx +3 -1
  29. package/src/rich-text-editor/ui/ContentEditable.css +0 -13
  30. package/src/rich-text-editor/ui/ContentEditable.tsx +0 -15
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crystallize/design-system",
3
- "version": "1.9.0",
3
+ "version": "1.11.0",
4
4
  "types": "./dist/index.d.ts",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -0,0 +1,14 @@
1
+ import { replaceI18nVariablesInString } from '.';
2
+
3
+ describe('RichTextEditor i18n', () => {
4
+ it('replaces variable parts of a translation string correctly', async () => {
5
+ expect(replaceI18nVariablesInString('Hi {{name}}!', 'you')).toBe('Hi you!');
6
+ expect(replaceI18nVariablesInString('Hi {{whatever-goes here}}!', 'you')).toBe('Hi you!');
7
+ expect(replaceI18nVariablesInString('It can be replace {{name}} many times {{name}}', 'you')).toBe(
8
+ 'It can be replace you many times you',
9
+ );
10
+ expect(replaceI18nVariablesInString('It needs correct formatting {name}}', 'you')).toBe(
11
+ 'It needs correct formatting {name}}',
12
+ );
13
+ });
14
+ });
@@ -0,0 +1,64 @@
1
+ import { createContext, ReactNode, useContext, useEffect, useState } from 'react';
2
+
3
+ import type { I18N } from './types';
4
+
5
+ const I18nContext = createContext<I18N | null>(null);
6
+
7
+ export type SupportedLanguages = 'en';
8
+
9
+ export { default as labelTranslationEn } from './translations/en';
10
+
11
+ export function I18nProvider({
12
+ language,
13
+ labelTranslations,
14
+ children,
15
+ }: {
16
+ language: SupportedLanguages;
17
+ labelTranslations?: I18N;
18
+ children: ReactNode;
19
+ }) {
20
+ const [translations, setTranslations] = useState<I18N | null>(labelTranslations || null);
21
+
22
+ useEffect(() => {
23
+ let unmounted = false;
24
+ (async function load() {
25
+ if (!labelTranslations) {
26
+ const resource = await import(`./translations/${language}.ts`);
27
+ if (!unmounted) {
28
+ setTranslations(resource.default);
29
+ }
30
+ }
31
+ })();
32
+
33
+ return () => {
34
+ unmounted = true;
35
+ };
36
+ }, [language, labelTranslations]);
37
+
38
+ return <I18nContext.Provider value={translations}>{children}</I18nContext.Provider>;
39
+ }
40
+
41
+ export function replaceI18nVariablesInString(str: string, replaceWith: string) {
42
+ return str.replace(/({{[^}]+}})/g, replaceWith);
43
+ }
44
+
45
+ export function useTr() {
46
+ const context = useContext(I18nContext);
47
+
48
+ return (key: keyof I18N, units?: number) => {
49
+ const thereAreUnits = typeof units === 'number';
50
+ const keyToUse = thereAreUnits && units > 1 ? `${key}_plural` : key;
51
+
52
+ if (context && keyToUse in context) {
53
+ // @ts-expect-error
54
+ const tr: string = context[keyToUse];
55
+
56
+ if (thereAreUnits) {
57
+ return replaceI18nVariablesInString(tr, units.toString());
58
+ }
59
+ return tr;
60
+ }
61
+
62
+ return '';
63
+ };
64
+ }
@@ -0,0 +1,66 @@
1
+ import type { I18N } from '../types';
2
+
3
+ const translations: I18N = {
4
+ actionClearTextFormatting: 'Clear text formatting',
5
+ actionTextFormattingOptions: 'Formatting options for additional text styles',
6
+ actionFormatAsStrongLabel: 'Format text as bold',
7
+ actionFormatAsStrongTitle: 'Bold (Ctrl+B)',
8
+ actionFormatAsStrongTitleApple: 'Bold (⌘B)',
9
+ actionFormatAsEmphasizedLabel: 'Format text as italics',
10
+ actionFormatAsEmphasizedTitle: 'Italic (Ctrl+I)',
11
+ actionFormatAsEmphasizedTitleApple: 'Italic (⌘I)',
12
+ actionFormatAsUnderlinedLabel: 'Underline text',
13
+ actionFormatAsUnderlinedTitle: 'Underlined (Ctrl+U)',
14
+ actionFormatAsUnderlinedTitleApple: 'Underlined (⌘U)',
15
+ actionFormatWithStrikethroughLabel: 'Format text with a strikethrough',
16
+ actionFormatWithStrikethroughTitle: 'Strikethrough',
17
+ actionFormatWithSubscriptLabel: 'Format text as subscript',
18
+ actionFormatWithSubscriptTitle: 'Subscript',
19
+ actionFormatWithSuperscriptLabel: 'Format text as superscript',
20
+ actionFormatWithSuperscriptTitle: 'Superscript',
21
+ actionInsertCodeBlock: 'Insert code block',
22
+ actionInsertlink: 'Insert link',
23
+ actionCopyJSON: 'Copy JSON',
24
+ actionCopyCode: 'Copy code',
25
+ actionClear: 'Clear editor',
26
+ actionFormatCode: 'Format code with Prettier',
27
+ actionTableInsertRowsAbove: 'Insert row above',
28
+ actionTableInsertRowsAbove_plural: 'Insert {{rows}} rows above',
29
+ actionTableInsertRowsBelow: 'Insert row above',
30
+ actionTableInsertRowsBelow_plural: 'Insert {{rows}} rows above',
31
+ actionTableInsertColumnsBefore: 'Insert column left',
32
+ actionTableInsertColumnsBefore_plural: 'Insert {{columns}} columns left',
33
+ actionTableInsertColumnsAfter: 'Insert column right',
34
+ actionTableInsertColumnsAfter_plural: 'Insert {{columns}} columns right',
35
+ actionTableAddRowHeader: 'Add row header',
36
+ actionTableRemoveRowHeader: 'Remove row header',
37
+ actionTableAddColumnHeader: 'Add column header',
38
+ actionTableRemoveColumnHeader: 'Remove column header',
39
+ actionTableDeleteColumn: 'Delete column',
40
+ actionTableDeleteRow: 'Delete row',
41
+ actionTableDeleteTable: 'Delete table',
42
+ actionTableOpenOptions: 'Open table options',
43
+ actionUndoLabel: 'Undo',
44
+ actionUndoTitle: 'Undo (Ctrl+Z)',
45
+ actionUndoTitleApple: 'Undo (⌘Z)',
46
+ actionRedoLabel: 'Undo',
47
+ actionRedoTitle: 'Undo (Ctrl+Y)',
48
+ actionRedoTitleApple: 'Redo (⌘Y)',
49
+ codeSelectLanguage: 'Select language',
50
+ linkEditorLink: 'Link',
51
+ linkEditorRel: 'Rel',
52
+ linkEditorTarget: 'Target',
53
+ linkEditorCommit: 'Done',
54
+ linkEditorEdit: 'Edit',
55
+ linkPreviewReplaceTextWithTitle: 'Replace link text with its title',
56
+ horizontalRule: 'Horizontal rule',
57
+ table: 'Table',
58
+ insertTableTitle: 'Insert table',
59
+ insertTableDescription:
60
+ 'Define your starting point of a table, you can add and remove columns and rows after creation.',
61
+ insertTableRows: 'Rows',
62
+ insertTableColumns: 'Columns',
63
+ insertTableCommit: 'Insert table',
64
+ } as const;
65
+
66
+ export default translations;
@@ -0,0 +1,62 @@
1
+ export type I18NKeys =
2
+ | 'actionClearTextFormatting'
3
+ | 'actionTextFormattingOptions'
4
+ | 'actionFormatAsStrongLabel'
5
+ | 'actionFormatAsStrongTitle'
6
+ | 'actionFormatAsStrongTitleApple'
7
+ | 'actionFormatAsEmphasizedLabel'
8
+ | 'actionFormatAsEmphasizedTitle'
9
+ | 'actionFormatAsEmphasizedTitleApple'
10
+ | 'actionFormatAsUnderlinedLabel'
11
+ | 'actionFormatAsUnderlinedTitle'
12
+ | 'actionFormatAsUnderlinedTitleApple'
13
+ | 'actionFormatWithStrikethroughLabel'
14
+ | 'actionFormatWithStrikethroughTitle'
15
+ | 'actionFormatWithSubscriptLabel'
16
+ | 'actionFormatWithSubscriptTitle'
17
+ | 'actionFormatWithSuperscriptLabel'
18
+ | 'actionFormatWithSuperscriptTitle'
19
+ | 'actionCopyJSON'
20
+ | 'actionClear'
21
+ | 'actionFormatCode'
22
+ | 'actionCopyCode'
23
+ | 'actionInsertCodeBlock'
24
+ | 'actionInsertlink'
25
+ | 'actionTableInsertRowsAbove'
26
+ | 'actionTableInsertRowsAbove_plural'
27
+ | 'actionTableInsertRowsBelow'
28
+ | 'actionTableInsertRowsBelow_plural'
29
+ | 'actionTableInsertColumnsBefore'
30
+ | 'actionTableInsertColumnsBefore_plural'
31
+ | 'actionTableInsertColumnsAfter'
32
+ | 'actionTableInsertColumnsAfter_plural'
33
+ | 'actionTableAddRowHeader'
34
+ | 'actionTableRemoveRowHeader'
35
+ | 'actionTableAddColumnHeader'
36
+ | 'actionTableRemoveColumnHeader'
37
+ | 'actionTableDeleteColumn'
38
+ | 'actionTableDeleteRow'
39
+ | 'actionTableDeleteTable'
40
+ | 'actionTableOpenOptions'
41
+ | 'actionUndoLabel'
42
+ | 'actionUndoTitle'
43
+ | 'actionUndoTitleApple'
44
+ | 'actionRedoLabel'
45
+ | 'actionRedoTitle'
46
+ | 'actionRedoTitleApple'
47
+ | 'codeSelectLanguage'
48
+ | 'linkEditorLink'
49
+ | 'linkEditorRel'
50
+ | 'linkEditorTarget'
51
+ | 'linkEditorCommit'
52
+ | 'linkEditorEdit'
53
+ | 'linkPreviewReplaceTextWithTitle'
54
+ | 'horizontalRule'
55
+ | 'table'
56
+ | 'insertTableTitle'
57
+ | 'insertTableDescription'
58
+ | 'insertTableRows'
59
+ | 'insertTableColumns'
60
+ | 'insertTableCommit';
61
+
62
+ export type I18N = Record<I18NKeys, string>;
@@ -3,6 +3,7 @@ import {
3
3
  $createParagraphNode,
4
4
  $createTextNode,
5
5
  $getRoot,
6
+ $isTextNode,
6
7
  LexicalNode,
7
8
  TextFormatType,
8
9
  } from 'lexical';
@@ -161,7 +162,34 @@ export function composeInitialState({ richText }: { richText: CrystallizeRichTex
161
162
 
162
163
  if (lexicalNode) {
163
164
  if (Array.isArray(children)) {
164
- children.forEach(n => handleNode({ crystallizeContentNode: n, lexicalParent: lexicalNode as LexicalNode }));
165
+ /**
166
+ * Support multiple nested inline text formats. A text can have both
167
+ * a strong _and_ underlined (and more) formats applied.
168
+ */
169
+ if ($isTextNode(lexicalNode)) {
170
+ const handleInlineTextChild = (child: CrystallizeRichTextNode) => {
171
+ if ($isTextNode(lexicalNode)) {
172
+ const textFormat = getLexicalTextFormat(child);
173
+
174
+ if (textFormat && !lexicalNode.hasFormat(textFormat)) {
175
+ lexicalNode.toggleFormat(textFormat);
176
+ }
177
+
178
+ // Text content will be stored at the deepest nested child
179
+ if (child.textContent) {
180
+ lexicalNode.setTextContent(child.textContent);
181
+ }
182
+ }
183
+
184
+ if ('children' in child) {
185
+ child.children?.forEach(handleInlineTextChild);
186
+ }
187
+ };
188
+
189
+ children.forEach(handleInlineTextChild);
190
+ } else {
191
+ children.forEach(n => handleNode({ crystallizeContentNode: n, lexicalParent: lexicalNode as LexicalNode }));
192
+ }
165
193
  }
166
194
 
167
195
  lexicalParent.append(lexicalNode);
@@ -8,6 +8,7 @@ import {
8
8
  type ElementNode,
9
9
  type EditorState,
10
10
  LexicalEditor,
11
+ TextFormatType,
11
12
  } from 'lexical';
12
13
  import { $isCodeNode } from '@lexical/code';
13
14
  import { $isLinkNode } from '@lexical/link';
@@ -16,7 +17,11 @@ import { $isHorizontalRuleNode } from '@lexical/react/LexicalHorizontalRuleNode'
16
17
  import { $isHeadingNode, $isQuoteNode, HeadingTagType } from '@lexical/rich-text';
17
18
  import { $isTableCellNode, $isTableNode, $isTableRowNode } from '@lexical/table';
18
19
 
19
- import type { CrystallizeRichTextNode, CrystallizeRichText } from '../types/crystallize-rich-text-types';
20
+ import type {
21
+ CrystallizeRichTextNode,
22
+ CrystallizeRichText,
23
+ CrystallizeRichTextInlineFormattedNodes,
24
+ } from '../types/crystallize-rich-text-types';
20
25
  import type { CrystallizeRichTextHeadingTypes } from '../types/crystallize-rich-text-types/headings';
21
26
 
22
27
  const headingMapper: Record<HeadingTagType, CrystallizeRichTextHeadingTypes> = {
@@ -182,10 +187,8 @@ export function lexicalToCrystallizeRichText({
182
187
  kind: 'block',
183
188
  type: 'horizontal-line',
184
189
  });
185
- } else {
186
- if ($isTextNode(childNode)) {
187
- parentChildrenToUse.push(getTextChild(childNode));
188
- }
190
+ } else if ($isTextNode(childNode)) {
191
+ parentChildrenToUse.push(getTextChild(childNode));
189
192
  }
190
193
  });
191
194
  }
@@ -197,36 +200,58 @@ export function lexicalToCrystallizeRichText({
197
200
  return crystallizeRichText;
198
201
  }
199
202
 
203
+ const lexicalFormatToCrystallizeType: Record<TextFormatType, CrystallizeRichTextInlineFormattedNodes['type']> = {
204
+ bold: 'strong',
205
+ italic: 'emphasized',
206
+ underline: 'underlined',
207
+ strikethrough: 'deleted',
208
+ subscript: 'subscripted',
209
+ superscript: 'superscripted',
210
+ highlight: 'emphasized',
211
+ code: 'code',
212
+ };
213
+
214
+ /**
215
+ * Creates a rich text node with the ability to have
216
+ * nested inline text nodes.
217
+ */
200
218
  function getTextChild(n: TextNode): CrystallizeRichTextNode {
201
- const textContent = n.getTextContent();
219
+ let currentChild: CrystallizeRichTextNode = {
220
+ kind: 'inline',
221
+ };
222
+ let textChild: CrystallizeRichTextNode | null = null;
223
+
224
+ function checkFormat(format: TextFormatType) {
225
+ const type = lexicalFormatToCrystallizeType[format];
226
+ if (n.hasFormat(format) && type) {
227
+ if (!textChild) {
228
+ currentChild = textChild = {
229
+ kind: 'inline',
230
+ type,
231
+ } as CrystallizeRichTextNode;
232
+ } else {
233
+ currentChild.children = [
234
+ {
235
+ kind: 'inline',
236
+ type,
237
+ },
238
+ ];
202
239
 
203
- // Deal with code seperately, since it can be both "inline" and "block"
204
- if (n.hasFormat('code')) {
205
- return {
206
- type: 'code',
240
+ currentChild = currentChild.children[0];
241
+ }
242
+ }
243
+ }
244
+
245
+ (Object.keys(lexicalFormatToCrystallizeType) as TextFormatType[]).forEach(checkFormat);
246
+
247
+ // Safe guard for unknown lexical types
248
+ if (!textChild) {
249
+ currentChild = textChild = {
207
250
  kind: 'inline',
208
- textContent,
209
251
  };
210
252
  }
211
253
 
212
- const textChild = {
213
- kind: 'inline',
214
- textContent,
215
- } as CrystallizeRichTextNode;
216
-
217
- if (n.hasFormat('bold')) {
218
- textChild.type = 'strong';
219
- } else if (n.hasFormat('italic')) {
220
- textChild.type = 'emphasized';
221
- } else if (n.hasFormat('underline')) {
222
- textChild.type = 'underlined';
223
- } else if (n.hasFormat('strikethrough')) {
224
- textChild.type = 'deleted';
225
- } else if (n.hasFormat('subscript')) {
226
- textChild.type = 'subscripted';
227
- } else if (n.hasFormat('superscript')) {
228
- textChild.type = 'superscripted';
229
- }
254
+ currentChild.textContent = n.getTextContent();
230
255
 
231
256
  return textChild;
232
257
  }
@@ -11,6 +11,7 @@ import { CLEAR_EDITOR_COMMAND, LexicalEditor } from 'lexical';
11
11
  import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
12
12
 
13
13
  import { ActionMenu } from '../../../action-menu';
14
+ import { useTr } from '../../i18n';
14
15
  import { lexicalToCrystallizeRichText } from '../../model/lexical-to-crystallize';
15
16
  import type { CrystallizeRichTextActionMenuItem } from '../../types/types';
16
17
 
@@ -24,25 +25,6 @@ async function copyJson(editor: LexicalEditor) {
24
25
  }
25
26
  }
26
27
 
27
- async function exportJson(editor: LexicalEditor) {
28
- const json = lexicalToCrystallizeRichText({ editorState: editor.getEditorState() });
29
-
30
- const blob = new Blob([JSON.stringify(json, null, 1)], {
31
- type: 'application/json',
32
- });
33
- const href = URL.createObjectURL(blob);
34
-
35
- const link = document.createElement('a');
36
- link.href = href;
37
- link.download = 'crystallizeRichText.json';
38
-
39
- document.body.appendChild(link);
40
- link.click();
41
- document.body.removeChild(link);
42
-
43
- URL.revokeObjectURL(href);
44
- }
45
-
46
28
  export default function ActionsPlugin({
47
29
  append,
48
30
  prepend,
@@ -51,6 +33,8 @@ export default function ActionsPlugin({
51
33
  prepend?: CrystallizeRichTextActionMenuItem[];
52
34
  }): JSX.Element {
53
35
  const [editor] = useLexicalComposerContext();
36
+ const tr = useTr();
37
+
54
38
  return (
55
39
  <div className="z-50 flex items-center ">
56
40
  <div></div>
@@ -66,8 +50,7 @@ export default function ActionsPlugin({
66
50
  {actionItem.title}
67
51
  </ActionMenu.Item>
68
52
  ))}
69
- <ActionMenu.Item onSelect={() => copyJson(editor)}>Copy JSON</ActionMenu.Item>
70
- <ActionMenu.Item onSelect={() => exportJson(editor)}>Export JSON</ActionMenu.Item>
53
+ <ActionMenu.Item onSelect={() => copyJson(editor)}>{tr('actionCopyJSON')}</ActionMenu.Item>
71
54
  <ActionMenu.Item
72
55
  className="danger"
73
56
  onSelect={() => {
@@ -75,7 +58,7 @@ export default function ActionsPlugin({
75
58
  editor.focus();
76
59
  }}
77
60
  >
78
- Clear paragraph
61
+ {tr('actionClear')}
79
62
  </ActionMenu.Item>
80
63
  {!append
81
64
  ? null
@@ -12,6 +12,8 @@ import { $getNearestNodeFromDOMNode, $getSelection, $setSelection, LexicalEditor
12
12
  import { useDebouncedCallback } from 'use-debounce';
13
13
  import { $isCodeNode } from '@lexical/code';
14
14
 
15
+ import { useTr } from '../../../../i18n';
16
+
15
17
  interface Props {
16
18
  editor: LexicalEditor;
17
19
  getCodeDOMNode: () => HTMLElement | null;
@@ -19,6 +21,7 @@ interface Props {
19
21
 
20
22
  export function CopyButton({ editor, getCodeDOMNode }: Props) {
21
23
  const [isCopyCompleted, setCopyCompleted] = useState<boolean>(false);
24
+ const tr = useTr();
22
25
 
23
26
  const removeSuccessIcon = useDebouncedCallback(() => {
24
27
  setCopyCompleted(false);
@@ -54,7 +57,7 @@ export function CopyButton({ editor, getCodeDOMNode }: Props) {
54
57
  }
55
58
 
56
59
  return (
57
- <button className="menu-item" onClick={handleClick} aria-label="copy">
60
+ <button className="menu-item" onClick={handleClick} aria-label={tr('actionCopyCode')}>
58
61
  {isCopyCompleted ? <i className="format success" /> : <i className="format copy" />}
59
62
  </button>
60
63
  );
@@ -12,6 +12,8 @@ import { $getNearestNodeFromDOMNode, LexicalEditor } from 'lexical';
12
12
  import type { Options } from 'prettier';
13
13
  import { $isCodeNode } from '@lexical/code';
14
14
 
15
+ import { useTr } from '../../../../i18n';
16
+
15
17
  interface Props {
16
18
  lang: string;
17
19
  editor: LexicalEditor;
@@ -69,6 +71,7 @@ function getPrettierOptions(lang: string): Options {
69
71
  export function PrettierButton({ lang, editor, getCodeDOMNode }: Props) {
70
72
  const [syntaxError, setSyntaxError] = useState<string>('');
71
73
  const [tipsVisible, setTipsVisible] = useState<boolean>(false);
74
+ const tr = useTr();
72
75
 
73
76
  async function handleClick(): Promise<void> {
74
77
  const codeDOMNode = getCodeDOMNode();
@@ -92,6 +95,13 @@ export function PrettierButton({ lang, editor, getCodeDOMNode }: Props) {
92
95
 
93
96
  parsed = format(content, options);
94
97
 
98
+ /**
99
+ * Remove EOF from prettier output. This is useful when
100
+ * using prettier on files, but becomes weird when the code
101
+ * is embedded within a larger portion of text.
102
+ */
103
+ parsed = parsed.replace(/[\r\n]+$/, '');
104
+
95
105
  if (parsed !== '') {
96
106
  const selection = codeNode.select(0);
97
107
  selection.insertText(parsed);
@@ -129,7 +139,7 @@ export function PrettierButton({ lang, editor, getCodeDOMNode }: Props) {
129
139
  onClick={handleClick}
130
140
  onMouseEnter={handleMouseEnter}
131
141
  onMouseLeave={handleMouseLeave}
132
- aria-label="prettier"
142
+ aria-label={tr('actionFormatCode')}
133
143
  >
134
144
  {syntaxError ? <i className="format prettier-error" /> : <i className="format prettier" />}
135
145
  </button>
@@ -29,7 +29,7 @@ function CodeActionMenuContainer({ anchorElem }: { anchorElem: HTMLElement }): J
29
29
 
30
30
  const [lang, setLang] = useState('');
31
31
  const [isShown, setShown] = useState<boolean>(false);
32
- const [shouldListenMouseMove, setShouldListenMouseMove] = useState<boolean>(false);
32
+ const [shouldListenMouseMove, setShouldListenMouseMove] = useState<boolean>(true);
33
33
  const [position, setPosition] = useState<Position>({
34
34
  right: '0',
35
35
  top: '0',
@@ -43,6 +43,7 @@ function CodeActionMenuContainer({ anchorElem }: { anchorElem: HTMLElement }): J
43
43
 
44
44
  const debouncedOnMouseMove = useDebouncedCallback((event: MouseEvent) => {
45
45
  const { codeDOMNode, isOutside } = getMouseInfo(event);
46
+
46
47
  if (isOutside) {
47
48
  setShown(false);
48
49
  return;
@@ -30,6 +30,7 @@ import { Button } from '../../../button';
30
30
  import { IconButton } from '../../../icon-button';
31
31
  import { Icon } from '../../../iconography';
32
32
  import { InputWithLabel } from '../../../input-with-label';
33
+ import { useTr } from '../../i18n';
33
34
  import LinkPreview from '../../ui/LinkPreview';
34
35
  import { getSelectedNode } from '../../utils/getSelectedNode';
35
36
  import { setFloatingElemPosition } from '../../utils/setFloatingElemPosition';
@@ -53,6 +54,7 @@ function FloatingLinkEditor({
53
54
  const [target, setTarget] = useState<string | null>(null);
54
55
  const [isEditMode, setEditMode] = useState(false);
55
56
  const [lastSelection, setLastSelection] = useState<RangeSelection | GridSelection | NodeSelection | null>(null);
57
+ const tr = useTr();
56
58
 
57
59
  const updateLinkEditor = useCallback(() => {
58
60
  const selection = $getSelection();
@@ -187,14 +189,29 @@ function FloatingLinkEditor({
187
189
  {isEditMode ? (
188
190
  <div>
189
191
  <div className="border-0 border-b border-gray-100-800 border-solid px-3">
190
- <InputWithLabel label="Link" type="text" value={linkUrl} onChange={e => setLinkUrl(e.target.value)} />
192
+ <InputWithLabel
193
+ label={tr('linkEditorLink')}
194
+ type="text"
195
+ value={linkUrl}
196
+ onChange={e => setLinkUrl(e.target.value)}
197
+ />
191
198
  </div>
192
199
 
193
200
  <div className="border-0 border-b border-gray-100-800 border-solid px-3">
194
- <InputWithLabel label="Rel" type="text" value={rel ?? ''} onChange={e => setRel(e.target.value)} />
201
+ <InputWithLabel
202
+ label={tr('linkEditorRel')}
203
+ type="text"
204
+ value={rel ?? ''}
205
+ onChange={e => setRel(e.target.value)}
206
+ />
195
207
  </div>
196
208
  <div className="border-0 border-b border-gray-100-800 border-solid px-3">
197
- <InputWithLabel label="Target" type="text" value={target ?? ''} onChange={e => setTarget(e.target.value)} />
209
+ <InputWithLabel
210
+ label={tr('linkEditorTarget')}
211
+ type="text"
212
+ value={target ?? ''}
213
+ onChange={e => setTarget(e.target.value)}
214
+ />
198
215
  </div>
199
216
  <div className="flex px-6 py-2 justify-end">
200
217
  <Button
@@ -211,14 +228,14 @@ function FloatingLinkEditor({
211
228
  }
212
229
  }}
213
230
  >
214
- Done
231
+ {tr('linkEditorCommit')}
215
232
  </Button>
216
233
  </div>
217
234
  </div>
218
235
  ) : (
219
236
  <>
220
237
  <div className="link-input !flex flex-nowrap justify-between items-center max-w-full ">
221
- <div className=" grid">
238
+ <div className="grid">
222
239
  <a href={linkUrl} target="_blank" rel="noopener noreferrer">
223
240
  {linkUrl}
224
241
  </a>
@@ -241,6 +258,7 @@ function FloatingLinkEditor({
241
258
  tabIndex={0}
242
259
  onMouseDown={event => event.preventDefault()}
243
260
  onClick={() => setEditMode(true)}
261
+ aria-label={tr('linkEditorEdit')}
244
262
  >
245
263
  <Icon.Edit />
246
264
  </IconButton>