@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.
- package/CHANGELOG.md +16 -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/Tooltip/Tooltip.js +11 -7
- package/dist/cjs/components/Tooltip/Tooltip.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/Tooltip/Tooltip.js +11 -7
- package/dist/esm/components/Tooltip/Tooltip.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/_shame.scss +1 -35
- package/src/cmem/react-flow/StickyNoteModal/StickyNoteModal.tsx +1 -1
- package/src/common/index.ts +2 -1
- package/src/common/scss/_accessibility-defaults.scss +101 -0
- package/src/common/utils/CssCustomProperties.ts +5 -3
- package/src/common/utils/colorHash.ts +38 -20
- package/src/components/Application/_header.scss +21 -9
- package/src/components/Application/_sidebar.scss +6 -0
- package/src/components/Application/_toolbar.scss +3 -3
- package/src/components/AutoSuggestion/AutoSuggestion.scss +3 -1
- package/src/components/Checkbox/checkbox.scss +9 -1
- 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/Dialog/dialog.scss +8 -0
- package/src/components/Link/link.scss +5 -6
- package/src/components/MultiSuggestField/_multisuggestfield.scss +18 -0
- package/src/components/RadioButton/RadioButton.tsx +15 -3
- package/src/components/RadioButton/radiobutton.scss +18 -1
- package/src/components/TextField/stories/TextField.stories.tsx +23 -0
- package/src/components/TextField/tests/useTextValidation.test.tsx +83 -0
- package/src/components/TextField/textfield.scss +20 -0
- package/src/components/TextField/useTextValidation.ts +17 -8
- package/src/components/Tooltip/Tooltip.test.tsx +40 -5
- package/src/components/Tooltip/Tooltip.tsx +14 -10
- package/src/components/index.scss +1 -0
- package/src/components/index.ts +1 -0
- package/src/configuration/stories/customproperties.stories.tsx +4 -0
- package/src/extensions/codemirror/CodeMirror.stories.tsx +9 -4
- package/src/extensions/codemirror/CodeMirror.tsx +71 -26
- package/src/extensions/codemirror/_codemirror.scss +18 -28
- 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
- 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
|
-
|
|
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(
|
|
@@ -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(
|
|
57
|
-
|
|
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).
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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);
|
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";
|
|
@@ -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: "
|
|
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 && (
|
|
@@ -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
|
-
|
|
27
|
-
|
|
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-
|
|
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-
|
|
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:
|
|
128
|
-
|
|
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
|
-
|
|
133
|
+
--#{$eccgui}-a11y-outline-color: #{$eccgui-color-warning-text};
|
|
132
134
|
}
|
|
133
135
|
|
|
134
136
|
&.#{eccgui}-intent--success {
|
|
135
|
-
|
|
137
|
+
--#{$eccgui}-a11y-outline-color: #{$eccgui-color-success-text};
|
|
136
138
|
}
|
|
137
139
|
|
|
138
140
|
&.#{eccgui}-intent--danger {
|
|
139
|
-
|
|
141
|
+
--#{$eccgui}-a11y-outline-color: #{$eccgui-color-danger-text};
|
|
140
142
|
}
|
|
141
143
|
|
|
142
144
|
&.#{eccgui}-intent--primary {
|
|
143
|
-
|
|
145
|
+
--#{$eccgui}-a11y-outline-color: #{$eccgui-color-primary};
|
|
144
146
|
}
|
|
145
147
|
|
|
146
148
|
&.#{eccgui}-intent--info {
|
|
147
|
-
|
|
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
|
+
});
|