@eccenca/gui-elements 25.1.0-rc.0 → 25.1.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/CHANGELOG.md +43 -5
  2. package/dist/cjs/cmem/ActivityControl/ActivityControlWidget.js +17 -13
  3. package/dist/cjs/cmem/ActivityControl/ActivityControlWidget.js.map +1 -1
  4. package/dist/cjs/cmem/react-flow/StickyNoteModal/StickyNoteModal.js +1 -1
  5. package/dist/cjs/cmem/react-flow/StickyNoteModal/StickyNoteModal.js.map +1 -1
  6. package/dist/cjs/common/index.js +1 -0
  7. package/dist/cjs/common/index.js.map +1 -1
  8. package/dist/cjs/common/utils/CssCustomProperties.js.map +1 -1
  9. package/dist/cjs/common/utils/colorHash.js +26 -12
  10. package/dist/cjs/common/utils/colorHash.js.map +1 -1
  11. package/dist/cjs/components/ColorField/ColorField.js +114 -0
  12. package/dist/cjs/components/ColorField/ColorField.js.map +1 -0
  13. package/dist/cjs/components/ContextOverlay/ContextOverlay.js +6 -6
  14. package/dist/cjs/components/ContextOverlay/ContextOverlay.js.map +1 -1
  15. package/dist/cjs/components/DecoupledOverlay/DecoupledOverlay.js +47 -0
  16. package/dist/cjs/components/DecoupledOverlay/DecoupledOverlay.js.map +1 -0
  17. package/dist/cjs/components/Icon/canonicalIconNames.js +3 -0
  18. package/dist/cjs/components/Icon/canonicalIconNames.js.map +1 -1
  19. package/dist/cjs/components/Icon/transformIcon.js +14 -0
  20. package/dist/cjs/components/Icon/transformIcon.js.map +1 -0
  21. package/dist/cjs/components/MultiSelect/MultiSelect.js +2 -1
  22. package/dist/cjs/components/MultiSelect/MultiSelect.js.map +1 -1
  23. package/dist/cjs/components/RadioButton/RadioButton.js +5 -2
  24. package/dist/cjs/components/RadioButton/RadioButton.js.map +1 -1
  25. package/dist/cjs/components/TextField/useTextValidation.js +17 -8
  26. package/dist/cjs/components/TextField/useTextValidation.js.map +1 -1
  27. package/dist/cjs/components/VisualTour/VisualTour.js +24 -32
  28. package/dist/cjs/components/VisualTour/VisualTour.js.map +1 -1
  29. package/dist/cjs/components/index.js +2 -0
  30. package/dist/cjs/components/index.js.map +1 -1
  31. package/dist/cjs/extensions/codemirror/CodeMirror.js +56 -18
  32. package/dist/cjs/extensions/codemirror/CodeMirror.js.map +1 -1
  33. package/dist/cjs/extensions/codemirror/toolbars/EditorAppearanceConfigMenu.js +23 -0
  34. package/dist/cjs/extensions/codemirror/toolbars/EditorAppearanceConfigMenu.js.map +1 -0
  35. package/dist/cjs/extensions/codemirror/toolbars/markdown.toolbar.js +5 -2
  36. package/dist/cjs/extensions/codemirror/toolbars/markdown.toolbar.js.map +1 -1
  37. package/dist/cjs/extensions/react-flow/edges/EdgeLabel.js +1 -1
  38. package/dist/cjs/extensions/react-flow/edges/EdgeLabel.js.map +1 -1
  39. package/dist/esm/cmem/ActivityControl/ActivityControlWidget.js +19 -14
  40. package/dist/esm/cmem/ActivityControl/ActivityControlWidget.js.map +1 -1
  41. package/dist/esm/cmem/react-flow/StickyNoteModal/StickyNoteModal.js +1 -1
  42. package/dist/esm/cmem/react-flow/StickyNoteModal/StickyNoteModal.js.map +1 -1
  43. package/dist/esm/common/index.js +2 -1
  44. package/dist/esm/common/index.js.map +1 -1
  45. package/dist/esm/common/utils/CssCustomProperties.js.map +1 -1
  46. package/dist/esm/common/utils/colorHash.js +26 -13
  47. package/dist/esm/common/utils/colorHash.js.map +1 -1
  48. package/dist/esm/components/ColorField/ColorField.js +140 -0
  49. package/dist/esm/components/ColorField/ColorField.js.map +1 -0
  50. package/dist/esm/components/ContextOverlay/ContextOverlay.js +3 -3
  51. package/dist/esm/components/ContextOverlay/ContextOverlay.js.map +1 -1
  52. package/dist/esm/components/DecoupledOverlay/DecoupledOverlay.js +41 -0
  53. package/dist/esm/components/DecoupledOverlay/DecoupledOverlay.js.map +1 -0
  54. package/dist/esm/components/Icon/canonicalIconNames.js +3 -0
  55. package/dist/esm/components/Icon/canonicalIconNames.js.map +1 -1
  56. package/dist/esm/components/Icon/transformIcon.js +21 -0
  57. package/dist/esm/components/Icon/transformIcon.js.map +1 -0
  58. package/dist/esm/components/MultiSelect/MultiSelect.js +3 -2
  59. package/dist/esm/components/MultiSelect/MultiSelect.js.map +1 -1
  60. package/dist/esm/components/RadioButton/RadioButton.js +6 -2
  61. package/dist/esm/components/RadioButton/RadioButton.js.map +1 -1
  62. package/dist/esm/components/TextField/useTextValidation.js +39 -8
  63. package/dist/esm/components/TextField/useTextValidation.js.map +1 -1
  64. package/dist/esm/components/VisualTour/VisualTour.js +25 -33
  65. package/dist/esm/components/VisualTour/VisualTour.js.map +1 -1
  66. package/dist/esm/components/index.js +2 -0
  67. package/dist/esm/components/index.js.map +1 -1
  68. package/dist/esm/extensions/codemirror/CodeMirror.js +58 -20
  69. package/dist/esm/extensions/codemirror/CodeMirror.js.map +1 -1
  70. package/dist/esm/extensions/codemirror/toolbars/EditorAppearanceConfigMenu.js +47 -0
  71. package/dist/esm/extensions/codemirror/toolbars/EditorAppearanceConfigMenu.js.map +1 -0
  72. package/dist/esm/extensions/codemirror/toolbars/markdown.toolbar.js +16 -2
  73. package/dist/esm/extensions/codemirror/toolbars/markdown.toolbar.js.map +1 -1
  74. package/dist/esm/extensions/react-flow/edges/EdgeLabel.js +1 -1
  75. package/dist/esm/extensions/react-flow/edges/EdgeLabel.js.map +1 -1
  76. package/dist/types/cmem/ActivityControl/ActivityControlWidget.d.ts +9 -0
  77. package/dist/types/common/index.d.ts +2 -1
  78. package/dist/types/common/utils/CssCustomProperties.d.ts +2 -2
  79. package/dist/types/common/utils/colorHash.d.ts +5 -4
  80. package/dist/types/components/ColorField/ColorField.d.ts +30 -0
  81. package/dist/types/components/ContextOverlay/ContextOverlay.d.ts +7 -1
  82. package/dist/types/components/DecoupledOverlay/DecoupledOverlay.d.ts +20 -0
  83. package/dist/types/components/Icon/canonicalIconNames.d.ts +2 -0
  84. package/dist/types/components/Icon/transformIcon.d.ts +2 -0
  85. package/dist/types/components/MultiSelect/MultiSelect.d.ts +1 -1
  86. package/dist/types/components/RadioButton/RadioButton.d.ts +8 -2
  87. package/dist/types/components/index.d.ts +2 -0
  88. package/dist/types/extensions/codemirror/CodeMirror.d.ts +12 -9
  89. package/dist/types/extensions/codemirror/toolbars/EditorAppearanceConfigMenu.d.ts +24 -0
  90. package/dist/types/extensions/codemirror/toolbars/markdown.toolbar.d.ts +2 -0
  91. package/package.json +1 -1
  92. package/src/cmem/ActivityControl/ActivityControlWidget.tsx +68 -35
  93. package/src/cmem/react-flow/StickyNoteModal/StickyNoteModal.tsx +1 -1
  94. package/src/common/index.ts +2 -1
  95. package/src/common/utils/CssCustomProperties.ts +5 -3
  96. package/src/common/utils/colorHash.ts +38 -20
  97. package/src/components/Application/_colors.scss +15 -0
  98. package/src/components/ColorField/ColorField.stories.tsx +72 -0
  99. package/src/components/ColorField/ColorField.test.tsx +101 -0
  100. package/src/components/ColorField/ColorField.tsx +200 -0
  101. package/src/components/ColorField/_colorfield.scss +67 -0
  102. package/src/components/ContextOverlay/ContextOverlay.tsx +20 -1
  103. package/src/components/DecoupledOverlay/DecoupledOverlay.stories.tsx +30 -0
  104. package/src/components/DecoupledOverlay/DecoupledOverlay.tsx +97 -0
  105. package/src/components/DecoupledOverlay/_decoupledoverlay.scss +46 -0
  106. package/src/components/Icon/canonicalIconNames.tsx +3 -0
  107. package/src/components/Icon/transformIcon.tsx +17 -0
  108. package/src/components/Link/Link.stories.tsx +30 -0
  109. package/src/components/Link/link.scss +28 -2
  110. package/src/components/MultiSelect/MultiSelect.tsx +12 -3
  111. package/src/components/RadioButton/RadioButton.tsx +15 -3
  112. package/src/components/RadioButton/radiobutton.scss +13 -0
  113. package/src/components/TextField/stories/TextField.stories.tsx +23 -0
  114. package/src/components/TextField/tests/useTextValidation.test.tsx +83 -0
  115. package/src/components/TextField/useTextValidation.ts +17 -8
  116. package/src/components/VisualTour/VisualTour.tsx +30 -50
  117. package/src/components/VisualTour/visualTour.scss +0 -34
  118. package/src/components/index.scss +2 -0
  119. package/src/components/index.ts +2 -0
  120. package/src/configuration/_customproperties.scss +32 -0
  121. package/src/configuration/stories/customproperties.stories.tsx +118 -0
  122. package/src/extensions/codemirror/CodeMirror.stories.tsx +9 -4
  123. package/src/extensions/codemirror/CodeMirror.tsx +87 -31
  124. package/src/extensions/codemirror/tests/CodeEditor.test.tsx +138 -0
  125. package/src/extensions/codemirror/tests/EditorAppearanceConfigMenu.test.tsx +131 -0
  126. package/src/extensions/codemirror/toolbars/EditorAppearanceConfigMenu.tsx +59 -0
  127. package/src/extensions/codemirror/toolbars/markdown.toolbar.tsx +17 -3
  128. package/src/extensions/react-flow/_config.scss +3 -3
  129. package/src/extensions/react-flow/edges/EdgeLabel.tsx +5 -3
  130. package/src/extensions/react-flow/edges/_edges.scss +3 -2
  131. package/src/index.scss +1 -0
@@ -6,6 +6,7 @@ import { DOMEventHandlers, EditorView, KeyBinding, keymap, Rect, ViewUpdate } fr
6
6
  import { minimalSetup } from "codemirror";
7
7
 
8
8
  import { Markdown } from "../../cmem/markdown/Markdown";
9
+ import { EditorAppearanceConfigMenu } from "./toolbars/EditorAppearanceConfigMenu";
9
10
  import { IntentTypes } from "../../common/Intent";
10
11
  import { markField } from "../../components/AutoSuggestion/extensions/markText";
11
12
  import { TestableComponent } from "../../components/interfaces";
@@ -36,7 +37,17 @@ import {
36
37
  import { MarkdownToolbar } from "./toolbars/markdown.toolbar";
37
38
  import { ExtensionCreator } from "./types";
38
39
 
39
- export interface CodeEditorProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "translate" | "onChange" | "onKeyDown" | "onMouseDown" | "onScroll">, TestableComponent {
40
+ interface EditorAppearance {
41
+ /**
42
+ * If enabled the code editor won't show numbers before each line.
43
+ */
44
+ preventLineNumbers?: boolean;
45
+
46
+ /** Long lines are wrapped and displayed on multiple lines */
47
+ wrapLines?: boolean;
48
+ }
49
+
50
+ export interface CodeEditorProps extends EditorAppearance, Omit<React.HTMLAttributes<HTMLDivElement>, "translate" | "onChange" | "onKeyDown" | "onMouseDown" | "onScroll">, TestableComponent {
40
51
  // Is called with the editor instance that allows access via the CodeMirror API
41
52
  setEditorView?: (editor: EditorView | undefined) => void;
42
53
  /**
@@ -86,10 +97,6 @@ export interface CodeEditorProps extends Omit<React.HTMLAttributes<HTMLDivElemen
86
97
  * Default value used first when the editor is instanciated.
87
98
  */
88
99
  defaultValue?: string;
89
- /**
90
- * If enabled the code editor won't show numbers before each line.
91
- */
92
- preventLineNumbers?: boolean;
93
100
 
94
101
  /** Set read-only mode. Default: false */
95
102
  readOnly?: boolean;
@@ -97,11 +104,8 @@ export interface CodeEditorProps extends Omit<React.HTMLAttributes<HTMLDivElemen
97
104
  /** Optional height of the component */
98
105
  height?: number | string;
99
106
 
100
- /** Long lines are wrapped and displayed on multiple lines */
101
- wrapLines?: boolean;
102
-
103
107
  /**
104
- * Add properties to the `div` used as warpper element.
108
+ * Add properties to the `div` used as wrapper element.
105
109
  * @deprecated (v26) You can now use all properties directly on `CodeEditor`.
106
110
  */
107
111
  outerDivAttributes?: Omit<React.HTMLAttributes<HTMLDivElement>, "id" | "data-test-id" | "data-testid" | "translate" | "onChange" | "onKeyDown" | "onMouseDown" | "onScroll">;
@@ -186,6 +190,18 @@ const ModeLinterMap: ReadonlyMap<SupportedCodeEditorModes, ReadonlyArray<Extensi
186
190
 
187
191
  const ModeToolbarSupport: ReadonlyArray<SupportedCodeEditorModes> = ["markdown"];
188
192
 
193
+ const defaultAppearanceForModeWithToolbar: ReadonlyMap<SupportedCodeEditorModes, EditorAppearance> = new Map([
194
+ ["markdown", { wrapLines: true, preventLineNumbers: true }]
195
+ ]);
196
+
197
+ const getDefaultAppearanceForModeWithToolbar = (hasToolbar: boolean, mode?: SupportedCodeEditorModes): EditorAppearance | undefined => {
198
+ if (hasToolbar && mode) {
199
+ return defaultAppearanceForModeWithToolbar.get(mode);
200
+ }
201
+
202
+ return undefined;
203
+ }
204
+
189
205
  /**
190
206
  * Includes a code editor, currently we use CodeMirror library as base.
191
207
  */
@@ -200,11 +216,11 @@ export const CodeEditor = ({
200
216
  name,
201
217
  id,
202
218
  mode,
203
- preventLineNumbers = false,
219
+ preventLineNumbers,
220
+ wrapLines,
204
221
  defaultValue = "",
205
222
  readOnly = false,
206
223
  shouldHaveMinimalSetup = true,
207
- wrapLines = false,
208
224
  onScroll,
209
225
  setEditorView,
210
226
  supportCodeFolding = false,
@@ -221,16 +237,30 @@ export const CodeEditor = ({
221
237
  autoFocus = false,
222
238
  disabled = false,
223
239
  intent,
224
- useToolbar,
240
+ useToolbar = false,
225
241
  translate,
226
242
  ...otherCodeEditorProps
227
243
  }: CodeEditorProps) => {
228
244
  const parent = useRef<any>(undefined);
229
245
  const [view, setView] = React.useState<EditorView | undefined>();
246
+ const defaultAppearanceForModeWithToolbar = getDefaultAppearanceForModeWithToolbar(useToolbar, mode);
247
+ const [editorAppearance, setEditorAppearance] = React.useState<{[s: string]: boolean;}>(
248
+ {
249
+ // we also set the fallback default here
250
+ wrapLines: wrapLines ?? defaultAppearanceForModeWithToolbar?.wrapLines ?? false,
251
+ preventLineNumbers: preventLineNumbers ?? defaultAppearanceForModeWithToolbar?.preventLineNumbers ?? false,
252
+ }
253
+ )
230
254
  const currentView = React.useRef<EditorView>()
231
255
  currentView.current = view
232
256
  const currentReadOnly = React.useRef(readOnly)
233
257
  currentReadOnly.current = readOnly
258
+ const currentOnChange = React.useRef(onChange)
259
+ currentOnChange.current = onChange
260
+ const currentDisabled = React.useRef(disabled)
261
+ currentDisabled.current = disabled
262
+ const currentIntent = React.useRef(intent)
263
+ currentIntent.current = intent
234
264
  const [showPreview, setShowPreview] = React.useState<boolean>(false);
235
265
  // CodeMirror Compartments in order to allow for re-configuration after initialization
236
266
  const readOnlyCompartment = React.useRef<Compartment>(compartment())
@@ -319,18 +349,18 @@ export const CodeEditor = ({
319
349
  disabledCompartment.current.of(EditorView?.editable.of(!disabled)),
320
350
  AdaptedEditorViewDomEventHandlers(domEventHandlers) as Extension,
321
351
  EditorView?.updateListener.of((v: ViewUpdate) => {
322
- if (disabled) return;
352
+ if (currentDisabled.current) return;
323
353
 
324
- if (onChange && v.docChanged) {
354
+ if (currentOnChange.current && v.docChanged) {
325
355
  // Only fire if the text has actually been changed
326
- onChange(v.state.doc.toString());
356
+ currentOnChange.current(v.state.doc.toString());
327
357
  }
328
358
 
329
359
  if (onSelection)
330
360
  onSelection(v.state.selection.ranges.filter((r) => !r.empty).map(({ from, to }) => ({ from, to })));
331
361
 
332
- if (onFocusChange && intent && !v.view.dom.classList?.contains(`${eccgui}-intent--${intent}`)) {
333
- v.view.dom.classList.add(`${eccgui}-intent--${intent}`);
362
+ if (onFocusChange && currentIntent.current && !v.view.dom.classList?.contains(`${eccgui}-intent--${currentIntent.current}`)) {
363
+ v.view.dom.classList.add(`${eccgui}-intent--${currentIntent.current}`);
334
364
  }
335
365
 
336
366
  if (onCursorChange) {
@@ -353,9 +383,9 @@ export const CodeEditor = ({
353
383
  }
354
384
  }),
355
385
  shouldHaveMinimalSetupCompartment.current.of(addExtensionsFor(shouldHaveMinimalSetup, minimalSetup)),
356
- preventLineNumbersCompartment.current.of(addExtensionsFor(!preventLineNumbers, adaptedLineNumbers())),
386
+ preventLineNumbersCompartment.current.of(addExtensionsFor(!editorAppearance.preventLineNumbers, adaptedLineNumbers())),
357
387
  shouldHighlightActiveLineCompartment.current.of(addExtensionsFor(shouldHighlightActiveLine, adaptedHighlightActiveLine())),
358
- wrapLinesCompartment.current.of(addExtensionsFor(wrapLines, EditorView?.lineWrapping)),
388
+ wrapLinesCompartment.current.of(addExtensionsFor((editorAppearance.wrapLines!), EditorView?.lineWrapping)),
359
389
  supportCodeFoldingCompartment.current.of(addExtensionsFor(supportCodeFolding, adaptedFoldGutter(), adaptedCodeFolding())),
360
390
  useLintingCompartment.current.of(addExtensionsFor(useLinting, ...linters)),
361
391
  adaptedSyntaxHighlighting(defaultHighlightStyle),
@@ -377,11 +407,11 @@ export const CodeEditor = ({
377
407
  }
378
408
 
379
409
  if (disabled) {
380
- view.dom.className += ` ${eccgui}-disabled`;
410
+ view.dom.classList.add(`${eccgui}-disabled`);
381
411
  }
382
412
 
383
- if (intent) {
384
- view.dom.className += ` ${eccgui}-intent--${intent}`;
413
+ if (currentIntent.current) {
414
+ view.dom.className += ` ${eccgui}-intent--${currentIntent.current}`;
385
415
  }
386
416
 
387
417
  if (autoFocus) {
@@ -432,24 +462,39 @@ export const CodeEditor = ({
432
462
  }, [tabIntentSize])
433
463
 
434
464
  React.useEffect(() => {
435
- updateExtension(EditorView?.editable.of(!disabled), disabledCompartment.current)
465
+ updateExtension(EditorView?.editable.of(!disabled), disabledCompartment.current);
466
+ if (view?.dom) {
467
+ if (disabled) {
468
+ view.dom.classList.add(`${eccgui}-disabled`);
469
+ } else {
470
+ view.dom.classList.remove(`${eccgui}-disabled`);
471
+ }
472
+ }
436
473
  }, [disabled])
437
474
 
438
475
  React.useEffect(() => {
439
- updateExtension(addExtensionsFor(shouldHaveMinimalSetup ?? true, minimalSetup), shouldHaveMinimalSetupCompartment.current)
440
- }, [shouldHaveMinimalSetup])
476
+ setEditorAppearance({
477
+ ...editorAppearance,
478
+ preventLineNumbers: preventLineNumbers ?? editorAppearance?.preventLineNumbers ?? false,
479
+ });
480
+ updateExtension(addExtensionsFor(!editorAppearance.preventLineNumbers, adaptedLineNumbers()), preventLineNumbersCompartment.current)
481
+ }, [preventLineNumbers, editorAppearance.preventLineNumbers])
441
482
 
442
483
  React.useEffect(() => {
443
- updateExtension(addExtensionsFor(!preventLineNumbers, adaptedLineNumbers()), preventLineNumbersCompartment.current)
444
- }, [preventLineNumbers])
484
+ setEditorAppearance({
485
+ ...editorAppearance,
486
+ wrapLines: wrapLines ?? editorAppearance?.wrapLines ?? false,
487
+ });
488
+ updateExtension(addExtensionsFor(editorAppearance.wrapLines!, EditorView?.lineWrapping), wrapLinesCompartment.current)
489
+ }, [wrapLines, editorAppearance.wrapLines])
445
490
 
446
491
  React.useEffect(() => {
447
- updateExtension(addExtensionsFor(shouldHighlightActiveLine ?? false, adaptedHighlightActiveLine()), shouldHighlightActiveLineCompartment.current)
448
- }, [shouldHighlightActiveLine])
492
+ updateExtension(addExtensionsFor(shouldHaveMinimalSetup ?? true, minimalSetup), shouldHaveMinimalSetupCompartment.current)
493
+ }, [shouldHaveMinimalSetup])
449
494
 
450
495
  React.useEffect(() => {
451
- updateExtension(addExtensionsFor(wrapLines ?? false, EditorView?.lineWrapping), wrapLinesCompartment.current)
452
- }, [wrapLines])
496
+ updateExtension(addExtensionsFor(shouldHighlightActiveLine ?? false, adaptedHighlightActiveLine()), shouldHighlightActiveLineCompartment.current)
497
+ }, [shouldHighlightActiveLine])
453
498
 
454
499
  React.useEffect(() => {
455
500
  updateExtension(addExtensionsFor(supportCodeFolding ?? false, adaptedFoldGutter(), adaptedCodeFolding()), supportCodeFoldingCompartment.current)
@@ -474,6 +519,17 @@ export const CodeEditor = ({
474
519
  translate={getTranslation}
475
520
  disabled={disabled}
476
521
  readonly={readOnly}
522
+ configMenu={(
523
+ <EditorAppearanceConfigMenu
524
+ config={{...editorAppearance}}
525
+ configLocked={{
526
+ wrapLines,
527
+ preventLineNumbers,
528
+ }}
529
+ setConfig={setEditorAppearance}
530
+ configPropertyTranslate={getTranslation}
531
+ />
532
+ )}
477
533
  />
478
534
  </div>
479
535
  {showPreview && (
@@ -0,0 +1,138 @@
1
+ import React from "react";
2
+ import { fireEvent, render, screen } from "@testing-library/react";
3
+
4
+ import "@testing-library/jest-dom";
5
+
6
+ import { CLASSPREFIX as eccgui } from "../../../configuration/constants";
7
+ import { CodeEditor } from "../CodeMirror";
8
+
9
+ const contextOverlayClass = `${eccgui}-contextoverlay`;
10
+
11
+ const setupDocumentRange = () => {
12
+ document.createRange = () => {
13
+ const range = new Range();
14
+ range.getBoundingClientRect = jest.fn();
15
+ range.getClientRects = () => ({
16
+ item: () => null,
17
+ length: 0,
18
+ [Symbol.iterator]: jest.fn(),
19
+ });
20
+ return range;
21
+ };
22
+ };
23
+
24
+ describe("CodeEditor - markdown mode with toolbar", () => {
25
+ beforeAll(() => {
26
+ setupDocumentRange();
27
+ });
28
+
29
+ // The toolbar contains a Paragraphs ContextMenu first, then the EditorAppearanceConfigMenu last.
30
+ const getConfigMenuOverlay = (container: HTMLElement) => {
31
+ const overlays = container.getElementsByClassName(contextOverlayClass);
32
+ return overlays[overlays.length - 1] as HTMLElement;
33
+ };
34
+
35
+ it("renders toolbar when mode is markdown and useToolbar is true", () => {
36
+ const { container } = render(<CodeEditor name="test-editor" mode="markdown" useToolbar={true} />);
37
+ expect(container.querySelector(`.${eccgui}-codeeditor__toolbar`)).not.toBeNull();
38
+ });
39
+
40
+ it("does not render toolbar when useToolbar is false", () => {
41
+ const { container } = render(<CodeEditor name="test-editor" mode="markdown" useToolbar={false} />);
42
+ expect(container.querySelector(`.${eccgui}-codeeditor__toolbar`)).toBeNull();
43
+ });
44
+
45
+ it("does not render toolbar for non-markdown modes even when useToolbar is true", () => {
46
+ const { container } = render(<CodeEditor name="test-editor" mode="yaml" useToolbar={true} />);
47
+ expect(container.querySelector(`.${eccgui}-codeeditor__toolbar`)).toBeNull();
48
+ });
49
+
50
+ it("includes the EditorAppearanceConfigMenu in the markdown toolbar", () => {
51
+ const { container } = render(<CodeEditor name="test-editor" mode="markdown" useToolbar={true} />);
52
+ const toolbar = container.querySelector(`.${eccgui}-codeeditor__toolbar`);
53
+ // Toolbar contains at least the Paragraphs menu and the EditorAppearanceConfigMenu
54
+ expect(toolbar?.getElementsByClassName(contextOverlayClass).length).toBeGreaterThanOrEqual(2);
55
+ });
56
+
57
+ it("defaults wrapLines to true in markdown mode with toolbar", async () => {
58
+ const { container } = render(<CodeEditor name="test-editor" mode="markdown" useToolbar={true} />);
59
+
60
+ fireEvent.click(getConfigMenuOverlay(container));
61
+
62
+ const wrapLinesItem = await screen.findByText("wrapLines");
63
+ expect(wrapLinesItem.closest("[aria-selected='true']")).not.toBeNull();
64
+ });
65
+
66
+ it("defaults preventLineNumbers to true in markdown mode with toolbar", async () => {
67
+ const { container } = render(<CodeEditor name="test-editor" mode="markdown" useToolbar={true} />);
68
+
69
+ fireEvent.click(getConfigMenuOverlay(container));
70
+
71
+ const preventLineNumbersItem = await screen.findByText("preventLineNumbers");
72
+ expect(preventLineNumbersItem.closest("[aria-selected='true']")).not.toBeNull();
73
+ });
74
+
75
+ it("locks wrapLines in config menu when wrapLines prop is explicitly provided", async () => {
76
+ const { container } = render(
77
+ <CodeEditor name="test-editor" mode="markdown" useToolbar={true} wrapLines={false} />
78
+ );
79
+
80
+ fireEvent.click(getConfigMenuOverlay(container));
81
+
82
+ const wrapLinesItem = await screen.findByText("wrapLines");
83
+ expect(wrapLinesItem.closest("[aria-disabled='true']")).not.toBeNull();
84
+ });
85
+
86
+ it("locks preventLineNumbers in config menu when preventLineNumbers prop is explicitly provided", async () => {
87
+ const { container } = render(
88
+ <CodeEditor name="test-editor" mode="markdown" useToolbar={true} preventLineNumbers={false} />
89
+ );
90
+
91
+ fireEvent.click(getConfigMenuOverlay(container));
92
+
93
+ const preventLineNumbersItem = await screen.findByText("preventLineNumbers");
94
+ expect(preventLineNumbersItem.closest("[aria-disabled='true']")).not.toBeNull();
95
+ });
96
+
97
+ it("does not lock wrapLines in config menu when wrapLines prop is not provided", async () => {
98
+ const { container } = render(<CodeEditor name="test-editor" mode="markdown" useToolbar={true} />);
99
+
100
+ fireEvent.click(getConfigMenuOverlay(container));
101
+
102
+ const wrapLinesItem = await screen.findByText("wrapLines");
103
+ expect(wrapLinesItem.closest("[aria-disabled='true']")).toBeNull();
104
+ });
105
+
106
+ it("does not lock preventLineNumbers in config menu when preventLineNumbers prop is not provided", async () => {
107
+ const { container } = render(<CodeEditor name="test-editor" mode="markdown" useToolbar={true} />);
108
+
109
+ fireEvent.click(getConfigMenuOverlay(container));
110
+
111
+ const preventLineNumbersItem = await screen.findByText("preventLineNumbers");
112
+ expect(preventLineNumbersItem.closest("[aria-disabled='true']")).toBeNull();
113
+ });
114
+
115
+ it("disables config menu trigger when both wrapLines and preventLineNumbers props are provided", () => {
116
+ const { container } = render(
117
+ <CodeEditor
118
+ name="test-editor"
119
+ mode="markdown"
120
+ useToolbar={true}
121
+ wrapLines={true}
122
+ preventLineNumbers={true}
123
+ />
124
+ );
125
+
126
+ const configMenuTrigger = getConfigMenuOverlay(container).querySelector("button");
127
+ expect(configMenuTrigger).toBeDisabled();
128
+ });
129
+
130
+ it("disables config menu trigger when editor is disabled", () => {
131
+ const { container } = render(
132
+ <CodeEditor name="test-editor" mode="markdown" useToolbar={true} disabled={true} />
133
+ );
134
+
135
+ const configMenuTrigger = getConfigMenuOverlay(container).querySelector("button");
136
+ expect(configMenuTrigger).toBeDisabled();
137
+ });
138
+ });
@@ -0,0 +1,131 @@
1
+ import React from "react";
2
+ import { fireEvent, render, screen } from "@testing-library/react";
3
+
4
+ import "@testing-library/jest-dom";
5
+
6
+ import { CLASSPREFIX as eccgui } from "../../../configuration/constants";
7
+ import { EditorAppearanceConfigMenu } from "../toolbars/EditorAppearanceConfigMenu";
8
+
9
+ const contextOverlayClass = `${eccgui}-contextoverlay`;
10
+
11
+ describe("EditorAppearanceConfigMenu", () => {
12
+ it("renders menu items for each config property, using key as fallback label", async () => {
13
+ const config = { wrapLines: true, preventLineNumbers: false };
14
+ const setConfig = jest.fn();
15
+
16
+ const { container } = render(<EditorAppearanceConfigMenu config={config} setConfig={setConfig} />);
17
+
18
+ fireEvent.click(container.getElementsByClassName(contextOverlayClass)[0]);
19
+
20
+ expect(await screen.findByText("wrapLines")).toBeVisible();
21
+ expect(await screen.findByText("preventLineNumbers")).toBeVisible();
22
+ });
23
+
24
+ it("uses configPropertyTranslate for menu item labels", async () => {
25
+ const config = { wrapLines: true, preventLineNumbers: false };
26
+ const setConfig = jest.fn();
27
+ const translate = (key: string) => `Label_${key}` as string | false;
28
+
29
+ const { container } = render(
30
+ <EditorAppearanceConfigMenu config={config} setConfig={setConfig} configPropertyTranslate={translate} />
31
+ );
32
+
33
+ fireEvent.click(container.getElementsByClassName(contextOverlayClass)[0]);
34
+
35
+ expect(await screen.findByText("Label_wrapLines")).toBeVisible();
36
+ expect(await screen.findByText("Label_preventLineNumbers")).toBeVisible();
37
+ });
38
+
39
+ it("calls setConfig with the toggled value when a menu item is clicked", async () => {
40
+ const config = { wrapLines: true, preventLineNumbers: false };
41
+ const setConfig = jest.fn();
42
+
43
+ const { container } = render(<EditorAppearanceConfigMenu config={config} setConfig={setConfig} />);
44
+
45
+ fireEvent.click(container.getElementsByClassName(contextOverlayClass)[0]);
46
+ const wrapLinesItem = await screen.findByText("wrapLines");
47
+ fireEvent.click(wrapLinesItem);
48
+
49
+ expect(setConfig).toHaveBeenCalledWith({ wrapLines: false, preventLineNumbers: false });
50
+ });
51
+
52
+ it("menu trigger is disabled when all config properties are locked", () => {
53
+ const config = { wrapLines: true, preventLineNumbers: false };
54
+ const configLocked = { wrapLines: true, preventLineNumbers: true };
55
+ const setConfig = jest.fn();
56
+
57
+ const { container } = render(
58
+ <EditorAppearanceConfigMenu config={config} configLocked={configLocked} setConfig={setConfig} />
59
+ );
60
+
61
+ const trigger = container.getElementsByClassName(contextOverlayClass)[0].querySelector("button");
62
+ expect(trigger).toBeDisabled();
63
+ });
64
+
65
+ it("menu trigger is enabled when not all config properties are locked", () => {
66
+ const config = { wrapLines: true, preventLineNumbers: false };
67
+ const configLocked = { wrapLines: true }; // only one locked
68
+ const setConfig = jest.fn();
69
+
70
+ const { container } = render(
71
+ <EditorAppearanceConfigMenu config={config} configLocked={configLocked} setConfig={setConfig} />
72
+ );
73
+
74
+ const trigger = container.getElementsByClassName(contextOverlayClass)[0].querySelector("button");
75
+ expect(trigger).not.toBeDisabled();
76
+ });
77
+
78
+ it("locked config property has a disabled menu item", async () => {
79
+ const config = { wrapLines: true, preventLineNumbers: false };
80
+ const configLocked = { wrapLines: true };
81
+ const setConfig = jest.fn();
82
+
83
+ const { container } = render(
84
+ <EditorAppearanceConfigMenu config={config} configLocked={configLocked} setConfig={setConfig} />
85
+ );
86
+
87
+ fireEvent.click(container.getElementsByClassName(contextOverlayClass)[0]);
88
+
89
+ const wrapLinesItem = await screen.findByText("wrapLines");
90
+ expect(wrapLinesItem.closest("[aria-disabled='true']")).not.toBeNull();
91
+ });
92
+
93
+ it("unlocked config property has an enabled menu item", async () => {
94
+ const config = { wrapLines: true, preventLineNumbers: false };
95
+ const configLocked = { wrapLines: true }; // only wrapLines is locked
96
+ const setConfig = jest.fn();
97
+
98
+ const { container } = render(
99
+ <EditorAppearanceConfigMenu config={config} configLocked={configLocked} setConfig={setConfig} />
100
+ );
101
+
102
+ fireEvent.click(container.getElementsByClassName(contextOverlayClass)[0]);
103
+
104
+ const preventLineNumbersItem = await screen.findByText("preventLineNumbers");
105
+ expect(preventLineNumbersItem.closest("[aria-disabled='true']")).toBeNull();
106
+ });
107
+
108
+ it("selected config property is marked as selected in the menu", async () => {
109
+ const config = { wrapLines: true, preventLineNumbers: false };
110
+ const setConfig = jest.fn();
111
+
112
+ const { container } = render(<EditorAppearanceConfigMenu config={config} setConfig={setConfig} />);
113
+
114
+ fireEvent.click(container.getElementsByClassName(contextOverlayClass)[0]);
115
+
116
+ const wrapLinesItem = await screen.findByText("wrapLines");
117
+ expect(wrapLinesItem.closest("[aria-selected='true']")).not.toBeNull();
118
+ });
119
+
120
+ it("unselected config property is not marked as selected in the menu", async () => {
121
+ const config = { wrapLines: true, preventLineNumbers: false };
122
+ const setConfig = jest.fn();
123
+
124
+ const { container } = render(<EditorAppearanceConfigMenu config={config} setConfig={setConfig} />);
125
+
126
+ fireEvent.click(container.getElementsByClassName(contextOverlayClass)[0]);
127
+
128
+ const preventLineNumbersItem = await screen.findByText("preventLineNumbers");
129
+ expect(preventLineNumbersItem.closest("[aria-selected='true']")).toBeNull();
130
+ });
131
+ });
@@ -0,0 +1,59 @@
1
+ import React from "react";
2
+
3
+ import { ContextMenu, ContextMenuProps, MenuItem } from "../../../components";
4
+
5
+ export interface EditorAppearanceConfigMenuProps {
6
+ /** Object containing a `true`/`false` value for each property */
7
+ config: { [s: string]: boolean };
8
+ /** Object containing `true` for each property that cannot be changed by user */
9
+ configLocked?: { [s: string]: boolean | undefined };
10
+ /** Handler that returns a translation for each config property key */
11
+ configPropertyTranslate?: (key: string) => string | false;
12
+ /** Handler to update config after user changes */
13
+ setConfig: React.Dispatch<React.SetStateAction<{ [s: string]: boolean }>>;
14
+ /** Additional properties used for the included `ContextMenu` */
15
+ contextMenuProps?: ContextMenuProps;
16
+ }
17
+
18
+ /**
19
+ * Returns a simple context menu that provides switches to control the editor appearance.
20
+ */
21
+ export function EditorAppearanceConfigMenu({
22
+ config,
23
+ configLocked = {},
24
+ configPropertyTranslate = (s) => s,
25
+ setConfig,
26
+ contextMenuProps,
27
+ }: EditorAppearanceConfigMenuProps) {
28
+ return (
29
+ <ContextMenu
30
+ togglerElement={"item-settings"}
31
+ {...contextMenuProps}
32
+ disabled={
33
+ contextMenuProps?.disabled ??
34
+ Object.values(config).length ===
35
+ Object.values(configLocked).filter((value) => {
36
+ return typeof value !== "undefined";
37
+ }).length
38
+ }
39
+ >
40
+ {Object.entries(config).map(([key, value]) => {
41
+ return (
42
+ <MenuItem
43
+ key={key}
44
+ roleStructure={"listoption"}
45
+ selected={value}
46
+ text={configPropertyTranslate(key) || key}
47
+ disabled={typeof configLocked[key] !== "undefined"}
48
+ onClick={() => {
49
+ setConfig({
50
+ ...config,
51
+ [key]: !value,
52
+ });
53
+ }}
54
+ />
55
+ );
56
+ })}
57
+ </ContextMenu>
58
+ );
59
+ }
@@ -9,6 +9,7 @@ import { Spacing } from "../../../components/Separation/Spacing";
9
9
  import { Toolbar, ToolbarSection } from "../../../components/Toolbar";
10
10
 
11
11
  import MarkdownCommand from "./commands/markdown.command";
12
+ import { EditorAppearanceConfigMenu } from "./EditorAppearanceConfigMenu";
12
13
 
13
14
  interface MarkdownToolbarProps {
14
15
  view?: EditorView;
@@ -17,6 +18,7 @@ interface MarkdownToolbarProps {
17
18
  translate: (key: string) => string | false;
18
19
  disabled?: boolean;
19
20
  readonly?: boolean;
21
+ configMenu?: React.ReactElement<typeof EditorAppearanceConfigMenu>;
20
22
  }
21
23
 
22
24
  export const MarkdownToolbar: React.FC<MarkdownToolbarProps> = ({
@@ -25,7 +27,8 @@ export const MarkdownToolbar: React.FC<MarkdownToolbarProps> = ({
25
27
  showPreview,
26
28
  disabled,
27
29
  readonly,
28
- translate
30
+ translate,
31
+ configMenu,
29
32
  }) => {
30
33
  const commandRef = React.useRef<MarkdownCommand | null>(null);
31
34
 
@@ -35,10 +38,10 @@ export const MarkdownToolbar: React.FC<MarkdownToolbarProps> = ({
35
38
  }
36
39
  }, [view]);
37
40
 
38
- const getTranslation = (fallback: string) : string => {
41
+ const getTranslation = (fallback: string): string => {
39
42
  const key = fallback.toLowerCase().replace(" ", "-");
40
43
  return translate(key) || fallback;
41
- }
44
+ };
42
45
 
43
46
  const { basic, lists, attachments } = MarkdownCommand.commands;
44
47
  return (
@@ -112,6 +115,17 @@ export const MarkdownToolbar: React.FC<MarkdownToolbarProps> = ({
112
115
  disabled={disabled}
113
116
  />
114
117
  </ToolbarSection>
118
+ {configMenu && (
119
+ <ToolbarSection>
120
+ <Spacing vertical size="small" hasDivider />
121
+ {React.cloneElement(configMenu, {
122
+ ...{
123
+ ...configMenu.props,
124
+ contextMenuProps: { disabled: showPreview || disabled ? true : undefined },
125
+ },
126
+ })}
127
+ </ToolbarSection>
128
+ )}
115
129
  </Toolbar>
116
130
  );
117
131
  };
@@ -8,9 +8,9 @@ $reactflow-node-font-size: $eccgui-size-typo-caption !default;
8
8
  $reactflow-node-border-width: 2 * $button-border-width !default;
9
9
  $reactflow-node-border-radius: $button-border-radius !default;
10
10
  $reactflow-edge-rendering: geometricprecision !default;
11
- $reactflow-edge-stroke-width-default: 2px !default;
12
- $reactflow-edge-stroke-width-hover: 2px !default;
13
- $reactflow-edge-stroke-width-selected: 2px !default;
11
+ $reactflow-edge-stroke-width-default: 1px !default;
12
+ $reactflow-edge-stroke-width-hover: 1px !default;
13
+ $reactflow-edge-stroke-width-selected: 1px !default;
14
14
  $reactflow-edge-stroke-opacity-default: $eccgui-opacity-muted !default;
15
15
  $reactflow-edge-stroke-opacity-hover: $eccgui-opacity-narrow !default;
16
16
  $reactflow-edge-stroke-opacity-selected: $eccgui-opacity-regular !default;
@@ -81,9 +81,11 @@ export const EdgeLabel = memo(
81
81
  })}
82
82
  </div>
83
83
  )}
84
- <div className={`${eccgui}-graphviz__edge-label__text`} title={title}>
85
- {typeof text === "string" ? <OverflowText>{text}</OverflowText> : text}
86
- </div>
84
+ {(title || text) && (
85
+ <div className={`${eccgui}-graphviz__edge-label__text`} title={title??undefined}>
86
+ {text && (typeof text === "string" ? <OverflowText>{text}</OverflowText> : text)}
87
+ </div>
88
+ )}
87
89
  {!!actions && <div className={`${eccgui}-graphviz__edge-label__aux`}>{actions}</div>}
88
90
  </div>
89
91
  );