@eccenca/gui-elements 25.1.0-rc.1 → 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.
- package/CHANGELOG.md +13 -1
- package/dist/cjs/cmem/react-flow/StickyNoteModal/StickyNoteModal.js +1 -1
- package/dist/cjs/cmem/react-flow/StickyNoteModal/StickyNoteModal.js.map +1 -1
- package/dist/cjs/common/index.js +1 -0
- package/dist/cjs/common/index.js.map +1 -1
- package/dist/cjs/common/utils/CssCustomProperties.js.map +1 -1
- package/dist/cjs/common/utils/colorHash.js +26 -12
- package/dist/cjs/common/utils/colorHash.js.map +1 -1
- package/dist/cjs/components/ColorField/ColorField.js +114 -0
- package/dist/cjs/components/ColorField/ColorField.js.map +1 -0
- package/dist/cjs/components/RadioButton/RadioButton.js +5 -2
- package/dist/cjs/components/RadioButton/RadioButton.js.map +1 -1
- package/dist/cjs/components/TextField/useTextValidation.js +17 -8
- package/dist/cjs/components/TextField/useTextValidation.js.map +1 -1
- package/dist/cjs/components/index.js +1 -0
- package/dist/cjs/components/index.js.map +1 -1
- package/dist/cjs/extensions/codemirror/CodeMirror.js +40 -14
- package/dist/cjs/extensions/codemirror/CodeMirror.js.map +1 -1
- package/dist/cjs/extensions/codemirror/toolbars/EditorAppearanceConfigMenu.js +23 -0
- package/dist/cjs/extensions/codemirror/toolbars/EditorAppearanceConfigMenu.js.map +1 -0
- package/dist/cjs/extensions/codemirror/toolbars/markdown.toolbar.js +5 -2
- package/dist/cjs/extensions/codemirror/toolbars/markdown.toolbar.js.map +1 -1
- package/dist/esm/cmem/react-flow/StickyNoteModal/StickyNoteModal.js +1 -1
- package/dist/esm/cmem/react-flow/StickyNoteModal/StickyNoteModal.js.map +1 -1
- package/dist/esm/common/index.js +2 -1
- package/dist/esm/common/index.js.map +1 -1
- package/dist/esm/common/utils/CssCustomProperties.js.map +1 -1
- package/dist/esm/common/utils/colorHash.js +26 -13
- package/dist/esm/common/utils/colorHash.js.map +1 -1
- package/dist/esm/components/ColorField/ColorField.js +140 -0
- package/dist/esm/components/ColorField/ColorField.js.map +1 -0
- package/dist/esm/components/RadioButton/RadioButton.js +6 -2
- package/dist/esm/components/RadioButton/RadioButton.js.map +1 -1
- package/dist/esm/components/TextField/useTextValidation.js +39 -8
- package/dist/esm/components/TextField/useTextValidation.js.map +1 -1
- package/dist/esm/components/index.js +1 -0
- package/dist/esm/components/index.js.map +1 -1
- package/dist/esm/extensions/codemirror/CodeMirror.js +42 -16
- package/dist/esm/extensions/codemirror/CodeMirror.js.map +1 -1
- package/dist/esm/extensions/codemirror/toolbars/EditorAppearanceConfigMenu.js +47 -0
- package/dist/esm/extensions/codemirror/toolbars/EditorAppearanceConfigMenu.js.map +1 -0
- package/dist/esm/extensions/codemirror/toolbars/markdown.toolbar.js +16 -2
- package/dist/esm/extensions/codemirror/toolbars/markdown.toolbar.js.map +1 -1
- package/dist/types/common/index.d.ts +2 -1
- package/dist/types/common/utils/CssCustomProperties.d.ts +2 -2
- package/dist/types/common/utils/colorHash.d.ts +5 -4
- package/dist/types/components/ColorField/ColorField.d.ts +30 -0
- package/dist/types/components/RadioButton/RadioButton.d.ts +8 -2
- package/dist/types/components/index.d.ts +1 -0
- package/dist/types/extensions/codemirror/CodeMirror.d.ts +12 -9
- package/dist/types/extensions/codemirror/toolbars/EditorAppearanceConfigMenu.d.ts +24 -0
- package/dist/types/extensions/codemirror/toolbars/markdown.toolbar.d.ts +2 -0
- package/package.json +1 -1
- package/src/cmem/react-flow/StickyNoteModal/StickyNoteModal.tsx +1 -1
- package/src/common/index.ts +2 -1
- package/src/common/utils/CssCustomProperties.ts +5 -3
- package/src/common/utils/colorHash.ts +38 -20
- package/src/components/ColorField/ColorField.stories.tsx +72 -0
- package/src/components/ColorField/ColorField.test.tsx +101 -0
- package/src/components/ColorField/ColorField.tsx +200 -0
- package/src/components/ColorField/_colorfield.scss +67 -0
- package/src/components/RadioButton/RadioButton.tsx +15 -3
- package/src/components/RadioButton/radiobutton.scss +13 -0
- package/src/components/TextField/stories/TextField.stories.tsx +23 -0
- package/src/components/TextField/tests/useTextValidation.test.tsx +83 -0
- package/src/components/TextField/useTextValidation.ts +17 -8
- package/src/components/index.scss +1 -0
- package/src/components/index.ts +1 -0
- package/src/extensions/codemirror/CodeMirror.stories.tsx +9 -4
- package/src/extensions/codemirror/CodeMirror.tsx +71 -26
- package/src/extensions/codemirror/tests/CodeEditor.test.tsx +138 -0
- package/src/extensions/codemirror/tests/EditorAppearanceConfigMenu.test.tsx +131 -0
- package/src/extensions/codemirror/toolbars/EditorAppearanceConfigMenu.tsx +59 -0
- package/src/extensions/codemirror/toolbars/markdown.toolbar.tsx +17 -3
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { act, render } from "@testing-library/react";
|
|
3
|
+
|
|
4
|
+
import { useTextValidation } from "../useTextValidation";
|
|
5
|
+
|
|
6
|
+
const HookWrapper: React.FC<{ value: string; callback: jest.Mock; callbackDelay?: number }> = ({
|
|
7
|
+
value,
|
|
8
|
+
callback,
|
|
9
|
+
callbackDelay = 0,
|
|
10
|
+
}) => {
|
|
11
|
+
useTextValidation({
|
|
12
|
+
value,
|
|
13
|
+
onChange: jest.fn(),
|
|
14
|
+
invisibleCharacterWarning: { callback, callbackDelay },
|
|
15
|
+
});
|
|
16
|
+
return null;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
describe("useTextValidation", () => {
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
jest.useFakeTimers();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
jest.useRealTimers();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
/** Render the hook with a controlled value and flush the debounce timer. */
|
|
29
|
+
const runWithValue = (value: string, callbackDelay = 0) => {
|
|
30
|
+
const callback = jest.fn();
|
|
31
|
+
render(<HookWrapper value={value} callback={callback} callbackDelay={callbackDelay} />);
|
|
32
|
+
act(() => {
|
|
33
|
+
jest.runAllTimers();
|
|
34
|
+
});
|
|
35
|
+
return callback;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
describe("invisible character detection", () => {
|
|
39
|
+
it("reports empty set for plain text", () => {
|
|
40
|
+
const callback = runWithValue("hello world");
|
|
41
|
+
expect(callback).toHaveBeenCalledWith(new Set());
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("detects zero-width space (U+200B)", () => {
|
|
45
|
+
const callback = runWithValue("hello\u200Bworld");
|
|
46
|
+
expect(callback).toHaveBeenCalledWith(new Set([0x200b]));
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("detects zero-width non-joiner (U+200C)", () => {
|
|
50
|
+
const callback = runWithValue("hello\u200Cworld");
|
|
51
|
+
expect(callback).toHaveBeenCalledWith(new Set([0x200c]));
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe("emoji false-positive prevention", () => {
|
|
56
|
+
it("does not flag ✔️ (base char + variation selector U+FE0F)", () => {
|
|
57
|
+
const callback = runWithValue("✔️");
|
|
58
|
+
expect(callback).toHaveBeenCalledWith(new Set());
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("does not flag ZWJ sequence emoji 👨👩👧👦", () => {
|
|
62
|
+
const callback = runWithValue("👨👩👧👦");
|
|
63
|
+
expect(callback).toHaveBeenCalledWith(new Set());
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("does not flag keycap emoji #️⃣", () => {
|
|
67
|
+
const callback = runWithValue("#️⃣");
|
|
68
|
+
expect(callback).toHaveBeenCalledWith(new Set());
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe("mixed content", () => {
|
|
73
|
+
it("detects ZWS while ignoring surrounding emoji", () => {
|
|
74
|
+
const callback = runWithValue("Check\u200B ✔️👨👩👧#️⃣");
|
|
75
|
+
expect(callback).toHaveBeenCalledWith(new Set([0x200b]));
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("reports empty set for text with only emoji", () => {
|
|
79
|
+
const callback = runWithValue("✔️ 👨👩👧👦#️⃣");
|
|
80
|
+
expect(callback).toHaveBeenCalledWith(new Set());
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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(
|
package/src/components/index.ts
CHANGED
|
@@ -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";
|
|
@@ -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: "
|
|
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: "
|
|
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
|
-
|
|
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
|
|
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
|
|
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 &&
|
|
337
|
-
v.view.dom.classList.add(`${eccgui}-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 (
|
|
388
|
-
view.dom.className += ` ${eccgui}-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
|
-
|
|
451
|
-
|
|
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
|
-
|
|
455
|
-
|
|
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(
|
|
459
|
-
}, [
|
|
492
|
+
updateExtension(addExtensionsFor(shouldHaveMinimalSetup ?? true, minimalSetup), shouldHaveMinimalSetupCompartment.current)
|
|
493
|
+
}, [shouldHaveMinimalSetup])
|
|
460
494
|
|
|
461
495
|
React.useEffect(() => {
|
|
462
|
-
updateExtension(addExtensionsFor(
|
|
463
|
-
}, [
|
|
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 && (
|
|
@@ -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
|
+
});
|