@eccenca/gui-elements 25.1.0-rc.1 → 25.1.0-rc.3

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 (94) hide show
  1. package/CHANGELOG.md +16 -1
  2. package/dist/cjs/cmem/react-flow/StickyNoteModal/StickyNoteModal.js +1 -1
  3. package/dist/cjs/cmem/react-flow/StickyNoteModal/StickyNoteModal.js.map +1 -1
  4. package/dist/cjs/common/index.js +1 -0
  5. package/dist/cjs/common/index.js.map +1 -1
  6. package/dist/cjs/common/utils/CssCustomProperties.js.map +1 -1
  7. package/dist/cjs/common/utils/colorHash.js +26 -12
  8. package/dist/cjs/common/utils/colorHash.js.map +1 -1
  9. package/dist/cjs/components/ColorField/ColorField.js +114 -0
  10. package/dist/cjs/components/ColorField/ColorField.js.map +1 -0
  11. package/dist/cjs/components/RadioButton/RadioButton.js +5 -2
  12. package/dist/cjs/components/RadioButton/RadioButton.js.map +1 -1
  13. package/dist/cjs/components/TextField/useTextValidation.js +17 -8
  14. package/dist/cjs/components/TextField/useTextValidation.js.map +1 -1
  15. package/dist/cjs/components/Tooltip/Tooltip.js +11 -7
  16. package/dist/cjs/components/Tooltip/Tooltip.js.map +1 -1
  17. package/dist/cjs/components/index.js +1 -0
  18. package/dist/cjs/components/index.js.map +1 -1
  19. package/dist/cjs/extensions/codemirror/CodeMirror.js +40 -14
  20. package/dist/cjs/extensions/codemirror/CodeMirror.js.map +1 -1
  21. package/dist/cjs/extensions/codemirror/toolbars/EditorAppearanceConfigMenu.js +23 -0
  22. package/dist/cjs/extensions/codemirror/toolbars/EditorAppearanceConfigMenu.js.map +1 -0
  23. package/dist/cjs/extensions/codemirror/toolbars/markdown.toolbar.js +5 -2
  24. package/dist/cjs/extensions/codemirror/toolbars/markdown.toolbar.js.map +1 -1
  25. package/dist/esm/cmem/react-flow/StickyNoteModal/StickyNoteModal.js +1 -1
  26. package/dist/esm/cmem/react-flow/StickyNoteModal/StickyNoteModal.js.map +1 -1
  27. package/dist/esm/common/index.js +2 -1
  28. package/dist/esm/common/index.js.map +1 -1
  29. package/dist/esm/common/utils/CssCustomProperties.js.map +1 -1
  30. package/dist/esm/common/utils/colorHash.js +26 -13
  31. package/dist/esm/common/utils/colorHash.js.map +1 -1
  32. package/dist/esm/components/ColorField/ColorField.js +140 -0
  33. package/dist/esm/components/ColorField/ColorField.js.map +1 -0
  34. package/dist/esm/components/RadioButton/RadioButton.js +6 -2
  35. package/dist/esm/components/RadioButton/RadioButton.js.map +1 -1
  36. package/dist/esm/components/TextField/useTextValidation.js +39 -8
  37. package/dist/esm/components/TextField/useTextValidation.js.map +1 -1
  38. package/dist/esm/components/Tooltip/Tooltip.js +11 -7
  39. package/dist/esm/components/Tooltip/Tooltip.js.map +1 -1
  40. package/dist/esm/components/index.js +1 -0
  41. package/dist/esm/components/index.js.map +1 -1
  42. package/dist/esm/extensions/codemirror/CodeMirror.js +42 -16
  43. package/dist/esm/extensions/codemirror/CodeMirror.js.map +1 -1
  44. package/dist/esm/extensions/codemirror/toolbars/EditorAppearanceConfigMenu.js +47 -0
  45. package/dist/esm/extensions/codemirror/toolbars/EditorAppearanceConfigMenu.js.map +1 -0
  46. package/dist/esm/extensions/codemirror/toolbars/markdown.toolbar.js +16 -2
  47. package/dist/esm/extensions/codemirror/toolbars/markdown.toolbar.js.map +1 -1
  48. package/dist/types/common/index.d.ts +2 -1
  49. package/dist/types/common/utils/CssCustomProperties.d.ts +2 -2
  50. package/dist/types/common/utils/colorHash.d.ts +5 -4
  51. package/dist/types/components/ColorField/ColorField.d.ts +30 -0
  52. package/dist/types/components/RadioButton/RadioButton.d.ts +8 -2
  53. package/dist/types/components/index.d.ts +1 -0
  54. package/dist/types/extensions/codemirror/CodeMirror.d.ts +12 -9
  55. package/dist/types/extensions/codemirror/toolbars/EditorAppearanceConfigMenu.d.ts +24 -0
  56. package/dist/types/extensions/codemirror/toolbars/markdown.toolbar.d.ts +2 -0
  57. package/package.json +1 -1
  58. package/src/_shame.scss +1 -35
  59. package/src/cmem/react-flow/StickyNoteModal/StickyNoteModal.tsx +1 -1
  60. package/src/common/index.ts +2 -1
  61. package/src/common/scss/_accessibility-defaults.scss +101 -0
  62. package/src/common/utils/CssCustomProperties.ts +5 -3
  63. package/src/common/utils/colorHash.ts +38 -20
  64. package/src/components/Application/_header.scss +21 -9
  65. package/src/components/Application/_sidebar.scss +6 -0
  66. package/src/components/Application/_toolbar.scss +3 -3
  67. package/src/components/AutoSuggestion/AutoSuggestion.scss +3 -1
  68. package/src/components/Checkbox/checkbox.scss +9 -1
  69. package/src/components/ColorField/ColorField.stories.tsx +72 -0
  70. package/src/components/ColorField/ColorField.test.tsx +101 -0
  71. package/src/components/ColorField/ColorField.tsx +200 -0
  72. package/src/components/ColorField/_colorfield.scss +67 -0
  73. package/src/components/Dialog/dialog.scss +8 -0
  74. package/src/components/Link/link.scss +5 -6
  75. package/src/components/MultiSuggestField/_multisuggestfield.scss +18 -0
  76. package/src/components/RadioButton/RadioButton.tsx +15 -3
  77. package/src/components/RadioButton/radiobutton.scss +18 -1
  78. package/src/components/TextField/stories/TextField.stories.tsx +23 -0
  79. package/src/components/TextField/tests/useTextValidation.test.tsx +83 -0
  80. package/src/components/TextField/textfield.scss +20 -0
  81. package/src/components/TextField/useTextValidation.ts +17 -8
  82. package/src/components/Tooltip/Tooltip.test.tsx +40 -5
  83. package/src/components/Tooltip/Tooltip.tsx +14 -10
  84. package/src/components/index.scss +1 -0
  85. package/src/components/index.ts +1 -0
  86. package/src/configuration/stories/customproperties.stories.tsx +4 -0
  87. package/src/extensions/codemirror/CodeMirror.stories.tsx +9 -4
  88. package/src/extensions/codemirror/CodeMirror.tsx +71 -26
  89. package/src/extensions/codemirror/_codemirror.scss +18 -28
  90. package/src/extensions/codemirror/tests/CodeEditor.test.tsx +138 -0
  91. package/src/extensions/codemirror/tests/EditorAppearanceConfigMenu.test.tsx +131 -0
  92. package/src/extensions/codemirror/toolbars/EditorAppearanceConfigMenu.tsx +59 -0
  93. package/src/extensions/codemirror/toolbars/markdown.toolbar.tsx +17 -3
  94. package/src/index.scss +1 -0
@@ -44,19 +44,28 @@ export const useTextValidation = <T>({ value, onChange, invisibleCharacterWarnin
44
44
  state.current.detectedCodePoints = new Set();
45
45
  }, []);
46
46
  const detectionRegex = React.useMemo(() => chars.invisibleZeroWidthCharacters.createRegex(), []);
47
+ const segmenter = React.useMemo(() => new Intl.Segmenter(undefined, { granularity: "grapheme" }), []);
48
+ const emojiRegex = React.useMemo(() => new RegExp("\\p{Extended_Pictographic}|\\u20E3", "u"), []);
49
+
47
50
  const detectIssues = React.useCallback(
48
51
  (value: string): void => {
49
- detectionRegex.lastIndex = 0;
50
- let matchArray = detectionRegex.exec(value);
51
- while (matchArray) {
52
- const codePoint = matchArray[0].codePointAt(0);
53
- if (codePoint) {
54
- state.current.detectedCodePoints.add(codePoint);
52
+ for (const { segment } of segmenter.segment(value)) {
53
+ if (emojiRegex.test(segment)) {
54
+ // skip emoji clusters since they legitimately contain variation selectors, ZWJ, tags, etc.
55
+ } else {
56
+ detectionRegex.lastIndex = 0;
57
+ let matchArray = detectionRegex.exec(segment);
58
+ while (matchArray) {
59
+ const codePoint = matchArray[0].codePointAt(0);
60
+ if (codePoint) {
61
+ state.current.detectedCodePoints.add(codePoint);
62
+ }
63
+ matchArray = detectionRegex.exec(segment);
64
+ }
55
65
  }
56
- matchArray = detectionRegex.exec(value);
57
66
  }
58
67
  },
59
- [detectionRegex]
68
+ [detectionRegex, segmenter, emojiRegex]
60
69
  );
61
70
  // Checks if the value contains any problematic characters with a small delay.
62
71
  const checkValue = React.useCallback(
@@ -1,5 +1,6 @@
1
1
  import React from "react";
2
2
  import { fireEvent, render, screen, waitFor } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
3
4
 
4
5
  import "@testing-library/jest-dom";
5
6
 
@@ -45,19 +46,53 @@ describe("Tooltip", () => {
45
46
  fireEvent.mouseEnter(container.getElementsByClassName(`${eccgui}-tooltip__wrapper--placeholder`)[0]);
46
47
  checkForPlaceholderClass(container, 1);
47
48
  await waitFor(() => {
48
- expect(screen.queryAllByText(TooltipStory.args.content)).toHaveLength(0);
49
49
  checkForPlaceholderClass(container, 0);
50
50
  });
51
+ expect(screen.queryAllByText(TooltipStory.args.content as string)).toHaveLength(0);
51
52
  });
52
53
  it("should be displayed on two continues mouse hover when placeholder is used", async () => {
53
54
  const { container } = render(<Tooltip {...TooltipStory.args} usePlaceholder={true} />);
54
55
  fireEvent.mouseEnter(container.getElementsByClassName(`${eccgui}-tooltip__wrapper`)[0]);
55
56
  checkForPlaceholderClass(container, 1);
56
- await waitFor(async () => {
57
- expect(screen.queryAllByText(TooltipStory.args.content)).toHaveLength(0);
57
+ await waitFor(() => {
58
+ checkForPlaceholderClass(container, 0);
59
+ });
60
+ expect(screen.queryAllByText(TooltipStory.args.content as string)).toHaveLength(0);
61
+ fireEvent.mouseEnter(container.getElementsByClassName(`${eccgui}-tooltip__wrapper`)[0]);
62
+ expect(await screen.findByText(TooltipStory.args.content as string)).toBeVisible();
63
+ });
64
+ it("should be displayed on focus when no placeholder is used", async () => {
65
+ // Blueprint ignores focus events with null relatedTarget (page-refocus guard), so we tab
66
+ // from a preceding element to produce a non-null relatedTarget.
67
+ render(
68
+ <>
69
+ <button>previous element</button>
70
+ <Tooltip {...TooltipStory.args} usePlaceholder={false} />
71
+ </>
72
+ );
73
+ const user = userEvent.setup();
74
+ await user.tab(); // focuses "previous element"
75
+ await user.tab(); // focuses tooltip target, relatedTarget is non-null → Blueprint opens
76
+ expect(await screen.findByText(TooltipStory.args.content as string)).toBeVisible();
77
+ });
78
+ it("should be displayed after keyboard focus when placeholder is used", async () => {
79
+ // Use a focusable button child so refocus() can call .focus() on it after the swap.
80
+ // Tab from a preceding element so relatedTarget is non-null when Blueprint handles focus.
81
+ const { container } = render(
82
+ <>
83
+ <button>previous element</button>
84
+ <Tooltip {...TooltipStory.args} usePlaceholder={true}>
85
+ <button>tooltip target</button>
86
+ </Tooltip>
87
+ </>
88
+ );
89
+ const user = userEvent.setup();
90
+ await user.tab(); // focuses "previous element"
91
+ await user.tab(); // focuses placeholder inner button, triggers focusin swap
92
+ checkForPlaceholderClass(container, 1);
93
+ await waitFor(() => {
58
94
  checkForPlaceholderClass(container, 0);
59
- fireEvent.mouseOver(container.getElementsByClassName(`${eccgui}-tooltip__wrapper`)[0]);
60
- expect(await screen.findByText(TooltipStory.args.content)).toBeVisible();
61
95
  });
96
+ expect(await screen.findByText(TooltipStory.args.content as string)).toBeVisible();
62
97
  });
63
98
  });
@@ -50,7 +50,7 @@ export interface TooltipProps extends Omit<BlueprintTooltipProps, "position"> {
50
50
  swapPlaceholderDelay?: number;
51
51
  }
52
52
 
53
- export type TooltipSize = "small" | "medium" | "large"
53
+ export type TooltipSize = "small" | "medium" | "large";
54
54
 
55
55
  export const Tooltip = ({
56
56
  children,
@@ -100,15 +100,19 @@ export const Tooltip = ({
100
100
  }, swapDelayTime);
101
101
  if (placeholderRef.current !== null) {
102
102
  const eventType = ev.type === "focusin" ? "focusout" : "mouseleave";
103
- (placeholderRef.current as HTMLElement).addEventListener(eventType, () => {
104
- if (
105
- (eventType === "focusout" && eventMemory.current === "afterfocus") ||
106
- (eventType === "mouseleave" && eventMemory.current === "afterhover")
107
- ) {
108
- eventMemory.current = null;
109
- }
110
- clearTimeout(swapDelay.current as NodeJS.Timeout);
111
- });
103
+ const innerFocusTarget = (placeholderRef.current as HTMLElement).querySelector("[tabindex='0']")
104
+ ?.children[0];
105
+ if (innerFocusTarget) {
106
+ (innerFocusTarget as HTMLElement).addEventListener(eventType, () => {
107
+ if (
108
+ (eventType === "focusout" && eventMemory.current === "afterfocus") ||
109
+ (eventType === "mouseleave" && eventMemory.current === "afterhover")
110
+ ) {
111
+ eventMemory.current = null;
112
+ }
113
+ clearTimeout(swapDelay.current as NodeJS.Timeout);
114
+ });
115
+ }
112
116
  }
113
117
  };
114
118
  (placeholderRef.current as HTMLElement).addEventListener("mouseenter", swap);
@@ -33,6 +33,7 @@
33
33
  @import "./Tabs/tabs";
34
34
  @import "./Tag/tag";
35
35
  @import "./TextField/textfield";
36
+ @import "./ColorField/colorfield";
36
37
  @import "./TagInput/taginput";
37
38
  @import "./Toolbar/toolbar";
38
39
  @import "./Tooltip/tooltip";
@@ -7,6 +7,7 @@ export * from "./Card";
7
7
  export * from "./Chat";
8
8
  export * from "./Checkbox/Checkbox";
9
9
  export * from "./CodeAutocompleteField";
10
+ export * from "./ColorField/ColorField";
10
11
  export * from "./ContentGroup/ContentGroup";
11
12
  export * from "./ContextOverlay";
12
13
  export * from "./DecoupledOverlay/DecoupledOverlay";
@@ -34,6 +34,10 @@ const groups: { title: string; filterName: (name: string) => boolean }[] = [
34
34
  title: "Color aliases",
35
35
  filterName: (name) => name.startsWith(`--${eccgui}-color`) && !name.startsWith(`--${eccgui}-color-palette`),
36
36
  },
37
+ {
38
+ title: "Accessibility",
39
+ filterName: (name) => name.startsWith(`--${eccgui}-a11y`),
40
+ },
37
41
  {
38
42
  title: "Opacity",
39
43
  filterName: (name) => name.startsWith(`--${eccgui}-opacity`),
@@ -24,17 +24,22 @@ const TemplateFull: StoryFn<typeof CodeEditor> = (args) => <CodeEditor {...args}
24
24
 
25
25
  export const BasicExample = TemplateFull.bind({});
26
26
  BasicExample.args = {
27
- name: "codeinput",
27
+ name: "jsinput",
28
+ mode: "json",
29
+ defaultValue: '{ json: "true" }',
30
+ };
31
+
32
+ export const MarkdownWithToolbar = TemplateFull.bind({});
33
+ MarkdownWithToolbar.args = {
34
+ name: "mdinput",
28
35
  mode: "markdown",
29
36
  defaultValue: "**test me**",
30
37
  useToolbar: true,
31
- disabled: false,
32
- readOnly: true,
33
38
  };
34
39
 
35
40
  export const LinterExample = TemplateFull.bind({});
36
41
  LinterExample.args = {
37
- name: "codeinput",
42
+ name: "lintinput",
38
43
  defaultValue: "**test me**",
39
44
  mode: "javascript",
40
45
  useLinting: true,
@@ -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,12 +237,20 @@ 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)
@@ -235,6 +259,8 @@ export const CodeEditor = ({
235
259
  currentOnChange.current = onChange
236
260
  const currentDisabled = React.useRef(disabled)
237
261
  currentDisabled.current = disabled
262
+ const currentIntent = React.useRef(intent)
263
+ currentIntent.current = intent
238
264
  const [showPreview, setShowPreview] = React.useState<boolean>(false);
239
265
  // CodeMirror Compartments in order to allow for re-configuration after initialization
240
266
  const readOnlyCompartment = React.useRef<Compartment>(compartment())
@@ -333,8 +359,8 @@ export const CodeEditor = ({
333
359
  if (onSelection)
334
360
  onSelection(v.state.selection.ranges.filter((r) => !r.empty).map(({ from, to }) => ({ from, to })));
335
361
 
336
- if (onFocusChange && intent && !v.view.dom.classList?.contains(`${eccgui}-intent--${intent}`)) {
337
- 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}`);
338
364
  }
339
365
 
340
366
  if (onCursorChange) {
@@ -357,9 +383,9 @@ export const CodeEditor = ({
357
383
  }
358
384
  }),
359
385
  shouldHaveMinimalSetupCompartment.current.of(addExtensionsFor(shouldHaveMinimalSetup, minimalSetup)),
360
- preventLineNumbersCompartment.current.of(addExtensionsFor(!preventLineNumbers, adaptedLineNumbers())),
386
+ preventLineNumbersCompartment.current.of(addExtensionsFor(!editorAppearance.preventLineNumbers, adaptedLineNumbers())),
361
387
  shouldHighlightActiveLineCompartment.current.of(addExtensionsFor(shouldHighlightActiveLine, adaptedHighlightActiveLine())),
362
- wrapLinesCompartment.current.of(addExtensionsFor(wrapLines, EditorView?.lineWrapping)),
388
+ wrapLinesCompartment.current.of(addExtensionsFor((editorAppearance.wrapLines!), EditorView?.lineWrapping)),
363
389
  supportCodeFoldingCompartment.current.of(addExtensionsFor(supportCodeFolding, adaptedFoldGutter(), adaptedCodeFolding())),
364
390
  useLintingCompartment.current.of(addExtensionsFor(useLinting, ...linters)),
365
391
  adaptedSyntaxHighlighting(defaultHighlightStyle),
@@ -384,8 +410,8 @@ export const CodeEditor = ({
384
410
  view.dom.classList.add(`${eccgui}-disabled`);
385
411
  }
386
412
 
387
- if (intent) {
388
- view.dom.className += ` ${eccgui}-intent--${intent}`;
413
+ if (currentIntent.current) {
414
+ view.dom.className += ` ${eccgui}-intent--${currentIntent.current}`;
389
415
  }
390
416
 
391
417
  if (autoFocus) {
@@ -447,20 +473,28 @@ export const CodeEditor = ({
447
473
  }, [disabled])
448
474
 
449
475
  React.useEffect(() => {
450
- updateExtension(addExtensionsFor(shouldHaveMinimalSetup ?? true, minimalSetup), shouldHaveMinimalSetupCompartment.current)
451
- }, [shouldHaveMinimalSetup])
476
+ setEditorAppearance({
477
+ ...editorAppearance,
478
+ preventLineNumbers: preventLineNumbers ?? editorAppearance?.preventLineNumbers ?? false,
479
+ });
480
+ updateExtension(addExtensionsFor(!editorAppearance.preventLineNumbers, adaptedLineNumbers()), preventLineNumbersCompartment.current)
481
+ }, [preventLineNumbers, editorAppearance.preventLineNumbers])
452
482
 
453
483
  React.useEffect(() => {
454
- updateExtension(addExtensionsFor(!preventLineNumbers, adaptedLineNumbers()), preventLineNumbersCompartment.current)
455
- }, [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])
456
490
 
457
491
  React.useEffect(() => {
458
- updateExtension(addExtensionsFor(shouldHighlightActiveLine ?? false, adaptedHighlightActiveLine()), shouldHighlightActiveLineCompartment.current)
459
- }, [shouldHighlightActiveLine])
492
+ updateExtension(addExtensionsFor(shouldHaveMinimalSetup ?? true, minimalSetup), shouldHaveMinimalSetupCompartment.current)
493
+ }, [shouldHaveMinimalSetup])
460
494
 
461
495
  React.useEffect(() => {
462
- updateExtension(addExtensionsFor(wrapLines ?? false, EditorView?.lineWrapping), wrapLinesCompartment.current)
463
- }, [wrapLines])
496
+ updateExtension(addExtensionsFor(shouldHighlightActiveLine ?? false, adaptedHighlightActiveLine()), shouldHighlightActiveLineCompartment.current)
497
+ }, [shouldHighlightActiveLine])
464
498
 
465
499
  React.useEffect(() => {
466
500
  updateExtension(addExtensionsFor(supportCodeFolding ?? false, adaptedFoldGutter(), adaptedCodeFolding()), supportCodeFoldingCompartment.current)
@@ -485,6 +519,17 @@ export const CodeEditor = ({
485
519
  translate={getTranslation}
486
520
  disabled={disabled}
487
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
+ )}
488
533
  />
489
534
  </div>
490
535
  {showPreview && (
@@ -19,13 +19,13 @@ $eccgui-size-codeeditor-toolbar-height: $button-height !default;
19
19
 
20
20
  &__toolbar {
21
21
  position: absolute;
22
- z-index: 3;
23
- left: 1px;
24
- right: 1px;
25
22
  top: 1px;
26
- border-radius: $pt-border-radius $pt-border-radius 0 0;
27
- border-bottom: solid 1px $eccgui-color-codeeditor-separation;
23
+ right: 1px;
24
+ left: 1px;
25
+ z-index: 3;
28
26
  background-color: $eccgui-color-codeeditor-background;
27
+ border-bottom: solid 1px $eccgui-color-codeeditor-separation;
28
+ border-radius: $pt-border-radius $pt-border-radius 0 0;
29
29
  }
30
30
 
31
31
  &--has-toolbar {
@@ -37,9 +37,9 @@ $eccgui-size-codeeditor-toolbar-height: $button-height !default;
37
37
  &__preview {
38
38
  position: absolute;
39
39
  top: calc(#{$eccgui-size-codeeditor-toolbar-height} + 1px) !important;
40
- left: 1px;
41
40
  right: 1px;
42
41
  bottom: 1px;
42
+ left: 1px;
43
43
  z-index: 2;
44
44
  padding: $button-padding;
45
45
  overflow-y: auto;
@@ -50,12 +50,12 @@ $eccgui-size-codeeditor-toolbar-height: $button-height !default;
50
50
  .cm-editor {
51
51
  width: 100%;
52
52
  height: $eccgui-size-codeeditor-height;
53
- clip-path: unset !important; // we may check later why they set inset(0) now
54
53
  background-color: $eccgui-color-codeeditor-background;
55
54
  border-radius: $pt-border-radius;
56
55
 
57
56
  // get them a "border" like input boxes from blueprintjs
58
57
  box-shadow: input-transition-shadow($input-shadow-color-focus), $pt-input-box-shadow;
58
+ clip-path: unset !important; // we may check later why they set inset(0) now
59
59
 
60
60
  &.#{eccgui}-disabled {
61
61
  @extend .#{$ns}-input, .#{$ns}-disabled;
@@ -89,7 +89,7 @@ $eccgui-size-codeeditor-toolbar-height: $button-height !default;
89
89
  }
90
90
 
91
91
  &.#{eccgui}-intent--primary {
92
- @include pt-input-intent($eccgui-color-info-text);
92
+ @include pt-input-intent($eccgui-color-primary);
93
93
  }
94
94
 
95
95
  &.#{eccgui}-intent--info {
@@ -97,7 +97,7 @@ $eccgui-size-codeeditor-toolbar-height: $button-height !default;
97
97
  }
98
98
 
99
99
  &.#{eccgui}-intent--accent {
100
- @include pt-input-intent($eccgui-color-primary);
100
+ @include pt-input-intent($eccgui-color-accent);
101
101
  }
102
102
 
103
103
  &.#{eccgui}-intent--neutral {
@@ -124,39 +124,29 @@ $eccgui-size-codeeditor-toolbar-height: $button-height !default;
124
124
  }
125
125
 
126
126
  &.cm-focused {
127
- outline: none;
128
- box-shadow: input-transition-shadow($input-shadow-color-focus, true), $input-box-shadow-focus;
127
+ --#{$eccgui}-a11y-outline-color: #{$eccgui-color-accent};
128
+ --#{$eccgui}-a11y-outline-offset: 0;
129
+
130
+ @extend .#{$eccgui}-a11y-focus-by-keyboard-static;
129
131
 
130
132
  &.#{eccgui}-intent--warning {
131
- box-shadow: input-transition-shadow($eccgui-color-warning-text, true), $input-box-shadow-focus;
133
+ --#{$eccgui}-a11y-outline-color: #{$eccgui-color-warning-text};
132
134
  }
133
135
 
134
136
  &.#{eccgui}-intent--success {
135
- box-shadow: input-transition-shadow($eccgui-color-success-text, true), $input-box-shadow-focus;
137
+ --#{$eccgui}-a11y-outline-color: #{$eccgui-color-success-text};
136
138
  }
137
139
 
138
140
  &.#{eccgui}-intent--danger {
139
- box-shadow: input-transition-shadow($eccgui-color-danger-text, true), $input-box-shadow-focus;
141
+ --#{$eccgui}-a11y-outline-color: #{$eccgui-color-danger-text};
140
142
  }
141
143
 
142
144
  &.#{eccgui}-intent--primary {
143
- box-shadow: input-transition-shadow($eccgui-color-info-text, true), $input-box-shadow-focus;
145
+ --#{$eccgui}-a11y-outline-color: #{$eccgui-color-primary};
144
146
  }
145
147
 
146
148
  &.#{eccgui}-intent--info {
147
- box-shadow: input-transition-shadow($eccgui-color-info-text, true), $input-box-shadow-focus;
148
- }
149
-
150
- &.#{eccgui}-intent--accent {
151
- box-shadow: input-transition-shadow($eccgui-color-warning-text, true), $input-box-shadow-focus;
152
- }
153
-
154
- &.#{eccgui}-intent--neutral {
155
- box-shadow: input-transition-shadow($eccgui-color-workspace-text, true), $input-box-shadow-focus;
156
- }
157
-
158
- &.#{eccgui}-intent--edited {
159
- box-shadow: input-transition-shadow($eccgui-color-info-text, true), $input-box-shadow-focus;
149
+ --#{$eccgui}-a11y-outline-color: #{$eccgui-color-info-text};
160
150
  }
161
151
 
162
152
  &.#{eccgui}-intent--removed {
@@ -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
+ });