@eccenca/gui-elements 24.3.1 → 24.4.0-fixpreventwronglydisplayedtooltipscmem6858.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +46 -1
- package/dist/cjs/cmem/ActivityControl/ActivityControlWidget.js +12 -9
- package/dist/cjs/cmem/ActivityControl/ActivityControlWidget.js.map +1 -1
- package/dist/cjs/components/AutoSuggestion/AutoSuggestion.js +10 -9
- package/dist/cjs/components/AutoSuggestion/AutoSuggestion.js.map +1 -1
- package/dist/cjs/components/AutoSuggestion/ExtendedCodeEditor.js +2 -2
- package/dist/cjs/components/AutoSuggestion/ExtendedCodeEditor.js.map +1 -1
- package/dist/cjs/components/Icon/IconButton.js +1 -0
- package/dist/cjs/components/Icon/IconButton.js.map +1 -1
- package/dist/cjs/components/Tooltip/Tooltip.js +15 -6
- package/dist/cjs/components/Tooltip/Tooltip.js.map +1 -1
- package/dist/cjs/extensions/codemirror/CodeMirror.js +83 -21
- package/dist/cjs/extensions/codemirror/CodeMirror.js.map +1 -1
- package/dist/cjs/extensions/codemirror/tests/codemirrorTestHelper.js +22 -1
- package/dist/cjs/extensions/codemirror/tests/codemirrorTestHelper.js.map +1 -1
- package/dist/cjs/extensions/react-flow/nodes/NodeContent.js +1 -1
- package/dist/cjs/extensions/react-flow/nodes/NodeContent.js.map +1 -1
- package/dist/esm/cmem/ActivityControl/ActivityControlWidget.js +12 -9
- package/dist/esm/cmem/ActivityControl/ActivityControlWidget.js.map +1 -1
- package/dist/esm/components/AutoSuggestion/AutoSuggestion.js +10 -9
- package/dist/esm/components/AutoSuggestion/AutoSuggestion.js.map +1 -1
- package/dist/esm/components/AutoSuggestion/ExtendedCodeEditor.js +2 -2
- package/dist/esm/components/AutoSuggestion/ExtendedCodeEditor.js.map +1 -1
- package/dist/esm/components/Icon/IconButton.js +1 -0
- package/dist/esm/components/Icon/IconButton.js.map +1 -1
- package/dist/esm/components/Tooltip/Tooltip.js +20 -11
- package/dist/esm/components/Tooltip/Tooltip.js.map +1 -1
- package/dist/esm/extensions/codemirror/CodeMirror.js +84 -22
- package/dist/esm/extensions/codemirror/CodeMirror.js.map +1 -1
- package/dist/esm/extensions/codemirror/tests/codemirrorTestHelper.js +20 -0
- package/dist/esm/extensions/codemirror/tests/codemirrorTestHelper.js.map +1 -1
- package/dist/esm/extensions/react-flow/nodes/NodeContent.js +1 -1
- package/dist/esm/extensions/react-flow/nodes/NodeContent.js.map +1 -1
- package/dist/types/cmem/ActivityControl/ActivityControlWidget.d.ts +4 -0
- package/dist/types/components/AutoSuggestion/AutoSuggestion.d.ts +7 -1
- package/dist/types/components/AutoSuggestion/ExtendedCodeEditor.d.ts +5 -1
- package/dist/types/components/Tooltip/Tooltip.d.ts +7 -1
- package/dist/types/extensions/codemirror/CodeMirror.d.ts +3 -2
- package/dist/types/extensions/codemirror/tests/codemirrorTestHelper.d.ts +3 -1
- package/package.json +3 -3
- package/src/cmem/ActivityControl/ActivityControlWidget.stories.tsx +45 -5
- package/src/cmem/ActivityControl/ActivityControlWidget.tsx +47 -9
- package/src/cmem/ActivityControl/tests/ActivityControlWidget.test.tsx +99 -0
- package/src/components/AutoSuggestion/AutoSuggestion.tsx +19 -5
- package/src/components/AutoSuggestion/ExtendedCodeEditor.tsx +8 -0
- package/src/components/Icon/IconButton.tsx +1 -0
- package/src/components/Tooltip/Tooltip.tsx +26 -6
- package/src/extensions/codemirror/CodeMirror.tsx +102 -26
- package/src/extensions/codemirror/tests/codemirrorTestHelper.ts +20 -1
- package/src/extensions/react-flow/nodes/NodeContent.tsx +1 -1
|
@@ -81,6 +81,10 @@ export interface ActivityControlWidgetProps extends TestableComponent {
|
|
|
81
81
|
* execution timer messages for waiting and running times.
|
|
82
82
|
*/
|
|
83
83
|
timerExecutionMsg?: JSX.Element | null;
|
|
84
|
+
/**
|
|
85
|
+
* additional actions that can serve as a complex component, positioned between the default actions and the context menu
|
|
86
|
+
*/
|
|
87
|
+
additionalActions?: React.ReactElement<unknown>[];
|
|
84
88
|
}
|
|
85
89
|
|
|
86
90
|
interface IActivityContextMenu extends TestableComponent {
|
|
@@ -110,11 +114,13 @@ interface IActivityMenuAction extends ActivityControlWidgetAction {
|
|
|
110
114
|
/** Shows the status of activities and supports actions on these activities. */
|
|
111
115
|
export function ActivityControlWidget(props: ActivityControlWidgetProps) {
|
|
112
116
|
const {
|
|
113
|
-
"data-test-id":
|
|
117
|
+
"data-test-id": dataTestIdLegacy,
|
|
118
|
+
"data-testid": dataTestId,
|
|
114
119
|
progressBar,
|
|
115
120
|
progressSpinner,
|
|
116
121
|
activityActions,
|
|
117
122
|
activityContextMenu,
|
|
123
|
+
additionalActions,
|
|
118
124
|
small,
|
|
119
125
|
border,
|
|
120
126
|
hasSpacing,
|
|
@@ -126,10 +132,19 @@ export function ActivityControlWidget(props: ActivityControlWidgetProps) {
|
|
|
126
132
|
} = props;
|
|
127
133
|
const spinnerClassNames = (progressSpinner?.className ?? "") + ` ${eccgui}-spinner--permanent`;
|
|
128
134
|
const widget = (
|
|
129
|
-
<OverviewItem
|
|
135
|
+
<OverviewItem
|
|
136
|
+
data-test-id={dataTestIdLegacy}
|
|
137
|
+
data-testid={dataTestId}
|
|
138
|
+
hasSpacing={border || hasSpacing}
|
|
139
|
+
densityHigh={small}
|
|
140
|
+
>
|
|
130
141
|
{progressBar && <ProgressBar {...progressBar} />}
|
|
131
142
|
{(progressSpinner || progressSpinnerFinishedIcon) && (
|
|
132
|
-
<OverviewItemDepiction
|
|
143
|
+
<OverviewItemDepiction
|
|
144
|
+
data-testid={dataTestId ? `${dataTestId}-progress-spinner` : undefined}
|
|
145
|
+
data-test-id={dataTestIdLegacy ? `${dataTestIdLegacy}-progress-spinner` : undefined}
|
|
146
|
+
keepColors
|
|
147
|
+
>
|
|
133
148
|
{progressSpinnerFinishedIcon ? (
|
|
134
149
|
React.cloneElement(progressSpinnerFinishedIcon as JSX.Element, { small, large: !small })
|
|
135
150
|
) : (
|
|
@@ -145,13 +160,21 @@ export function ActivityControlWidget(props: ActivityControlWidgetProps) {
|
|
|
145
160
|
)}
|
|
146
161
|
<OverviewItemDescription>
|
|
147
162
|
{props.label && (
|
|
148
|
-
<OverviewItemLine
|
|
163
|
+
<OverviewItemLine
|
|
164
|
+
data-testid={dataTestId ? `${dataTestId}-label` : undefined}
|
|
165
|
+
data-test-id={dataTestIdLegacy ? `${dataTestIdLegacy}-label` : undefined}
|
|
166
|
+
small={small}
|
|
167
|
+
>
|
|
149
168
|
{React.cloneElement(labelWrapper, {}, props.label)}
|
|
150
169
|
{timerExecutionMsg && (props.statusMessage || tags) && <> ({timerExecutionMsg})</>}
|
|
151
170
|
</OverviewItemLine>
|
|
152
171
|
)}
|
|
153
172
|
{(props.statusMessage || tags) && (
|
|
154
|
-
<OverviewItemLine
|
|
173
|
+
<OverviewItemLine
|
|
174
|
+
data-testid={dataTestId ? `${dataTestId}-status-message` : undefined}
|
|
175
|
+
data-test-id={dataTestIdLegacy ? `${dataTestIdLegacy}-status-message` : undefined}
|
|
176
|
+
small
|
|
177
|
+
>
|
|
155
178
|
{tags}
|
|
156
179
|
{props.statusMessage && (
|
|
157
180
|
<OverflowText passDown>
|
|
@@ -172,21 +195,35 @@ export function ActivityControlWidget(props: ActivityControlWidgetProps) {
|
|
|
172
195
|
</OverviewItemLine>
|
|
173
196
|
)}
|
|
174
197
|
{timerExecutionMsg && !(props.statusMessage || tags) && (
|
|
175
|
-
<OverviewItemLine
|
|
198
|
+
<OverviewItemLine
|
|
199
|
+
data-testid={dataTestId ? `${dataTestId}-status-message` : undefined}
|
|
200
|
+
data-test-id={dataTestIdLegacy ? `${dataTestIdLegacy}-status-message` : undefined}
|
|
201
|
+
small
|
|
202
|
+
>
|
|
203
|
+
{timerExecutionMsg}
|
|
204
|
+
</OverviewItemLine>
|
|
176
205
|
)}
|
|
177
206
|
</OverviewItemDescription>
|
|
178
|
-
<OverviewItemActions
|
|
207
|
+
<OverviewItemActions
|
|
208
|
+
data-testid={dataTestId ? `${dataTestId}-actions` : undefined}
|
|
209
|
+
data-test-id={dataTestIdLegacy ? `${dataTestIdLegacy}-actions` : undefined}
|
|
210
|
+
>
|
|
179
211
|
{activityActions &&
|
|
180
212
|
activityActions.map((action, idx) => {
|
|
181
213
|
return (
|
|
182
214
|
<IconButton
|
|
183
|
-
key={
|
|
215
|
+
key={
|
|
216
|
+
typeof action.icon === "string"
|
|
217
|
+
? action.icon
|
|
218
|
+
: action["data-test-id"] ?? action["data-testid"] ?? idx
|
|
219
|
+
}
|
|
184
220
|
data-test-id={action["data-test-id"]}
|
|
221
|
+
data-testid={action["data-testid"]}
|
|
185
222
|
name={action.icon}
|
|
186
223
|
text={action.tooltip}
|
|
187
224
|
onClick={action.action}
|
|
188
225
|
disabled={action.disabled}
|
|
189
|
-
|
|
226
|
+
intent={action.hasStateWarning ? "warning" : undefined}
|
|
190
227
|
tooltipProps={{
|
|
191
228
|
hoverOpenDelay: 200,
|
|
192
229
|
placement: "bottom",
|
|
@@ -194,6 +231,7 @@ export function ActivityControlWidget(props: ActivityControlWidgetProps) {
|
|
|
194
231
|
/>
|
|
195
232
|
);
|
|
196
233
|
})}
|
|
234
|
+
{additionalActions}
|
|
197
235
|
{activityContextMenu && activityContextMenu.menuItems.length > 0 && (
|
|
198
236
|
<ContextMenu
|
|
199
237
|
data-test-id={activityContextMenu["data-test-id"]}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { fireEvent, render, screen } from "@testing-library/react";
|
|
3
|
+
|
|
4
|
+
import "@testing-library/jest-dom";
|
|
5
|
+
|
|
6
|
+
import { IconButton, Tag, TagList } from "../../../index";
|
|
7
|
+
import { ActivityControlWidget, ActivityControlWidgetAction } from "../ActivityControlWidget";
|
|
8
|
+
|
|
9
|
+
describe("ActivityControlWidget", () => {
|
|
10
|
+
it("Renders basic widget with actions and handles clicks", () => {
|
|
11
|
+
const mockAction1 = jest.fn();
|
|
12
|
+
const mockAction2 = jest.fn();
|
|
13
|
+
const actions: ActivityControlWidgetAction[] = [
|
|
14
|
+
{
|
|
15
|
+
"data-testid": "action-1",
|
|
16
|
+
icon: "item-reload",
|
|
17
|
+
action: mockAction1,
|
|
18
|
+
tooltip: "Action 1",
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"data-testid": "action-2",
|
|
22
|
+
icon: "item-start",
|
|
23
|
+
action: mockAction2,
|
|
24
|
+
tooltip: "Action 2",
|
|
25
|
+
},
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
render(
|
|
29
|
+
<ActivityControlWidget
|
|
30
|
+
label="Basic widget"
|
|
31
|
+
data-testid="basic-widget"
|
|
32
|
+
activityActions={actions}
|
|
33
|
+
statusMessage="Status message"
|
|
34
|
+
/>
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const button1 = screen.getByTestId("action-1");
|
|
38
|
+
const button2 = screen.getByTestId("action-2");
|
|
39
|
+
|
|
40
|
+
const label = screen.getByTestId("basic-widget-label");
|
|
41
|
+
const statusMessage = screen.getByTestId("basic-widget-status-message");
|
|
42
|
+
const actionsContainer = screen.getByTestId("basic-widget-actions");
|
|
43
|
+
|
|
44
|
+
expect(label).toBeInTheDocument();
|
|
45
|
+
expect(statusMessage).toBeInTheDocument();
|
|
46
|
+
expect(actionsContainer).toBeInTheDocument();
|
|
47
|
+
|
|
48
|
+
expect(label).toHaveTextContent("Basic widget");
|
|
49
|
+
expect(statusMessage).toHaveTextContent("Status message");
|
|
50
|
+
|
|
51
|
+
expect(button1).toBeInTheDocument();
|
|
52
|
+
expect(button2).toBeInTheDocument();
|
|
53
|
+
|
|
54
|
+
fireEvent.click(button1);
|
|
55
|
+
expect(mockAction1).toHaveBeenCalledTimes(1);
|
|
56
|
+
|
|
57
|
+
fireEvent.click(button2);
|
|
58
|
+
expect(mockAction2).toHaveBeenCalledTimes(1);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("Renders widget with tags", () => {
|
|
62
|
+
const tags = (
|
|
63
|
+
<TagList>
|
|
64
|
+
<Tag>Tag one</Tag>
|
|
65
|
+
<Tag>Other tag</Tag>
|
|
66
|
+
</TagList>
|
|
67
|
+
);
|
|
68
|
+
render(<ActivityControlWidget label="Widget with tags" tags={tags} data-testid="widget-with-tags" />);
|
|
69
|
+
|
|
70
|
+
const label = screen.getByTestId("widget-with-tags-label");
|
|
71
|
+
const statusMessage = screen.getByTestId("widget-with-tags-status-message");
|
|
72
|
+
|
|
73
|
+
expect(label).toBeInTheDocument();
|
|
74
|
+
expect(statusMessage).toBeInTheDocument();
|
|
75
|
+
|
|
76
|
+
expect(label).toHaveTextContent("Widget with tags");
|
|
77
|
+
expect(statusMessage).toHaveTextContent("Tag one");
|
|
78
|
+
expect(statusMessage).toHaveTextContent("Other tag");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("Renders widget with additional actions and handles click", () => {
|
|
82
|
+
const mockAction = jest.fn();
|
|
83
|
+
const additionalActions = [
|
|
84
|
+
<IconButton
|
|
85
|
+
key="add-btn"
|
|
86
|
+
name="application-explore"
|
|
87
|
+
onClick={mockAction}
|
|
88
|
+
data-testid="additional-action"
|
|
89
|
+
/>,
|
|
90
|
+
];
|
|
91
|
+
render(<ActivityControlWidget additionalActions={additionalActions} />);
|
|
92
|
+
|
|
93
|
+
const customButton = screen.getByTestId("additional-action");
|
|
94
|
+
expect(customButton).toBeInTheDocument();
|
|
95
|
+
|
|
96
|
+
fireEvent.click(customButton);
|
|
97
|
+
expect(mockAction).toHaveBeenCalledTimes(1);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
@@ -166,6 +166,12 @@ export interface AutoSuggestionProps {
|
|
|
166
166
|
/** If this is enabled the value of the editor is replaced with the initialValue if it changes.
|
|
167
167
|
* FIXME: This property is a workaround for some "controlled" usages of the component via the initialValue property. */
|
|
168
168
|
reInitOnInitialValueChange?: boolean;
|
|
169
|
+
/** Optional height of the component */
|
|
170
|
+
height?: number | string;
|
|
171
|
+
/** Set read-only mode. Default: false */
|
|
172
|
+
readOnly?: boolean;
|
|
173
|
+
/** Properties that should be added to the outer div container. */
|
|
174
|
+
outerDivAttributes?: Omit<React.HTMLAttributes<HTMLDivElement>, "id" | "data-test-id">;
|
|
169
175
|
}
|
|
170
176
|
|
|
171
177
|
// Meta data regarding a request
|
|
@@ -198,6 +204,9 @@ const AutoSuggestion = ({
|
|
|
198
204
|
mode,
|
|
199
205
|
multiline = false,
|
|
200
206
|
reInitOnInitialValueChange = false,
|
|
207
|
+
height,
|
|
208
|
+
readOnly,
|
|
209
|
+
outerDivAttributes
|
|
201
210
|
}: AutoSuggestionProps) => {
|
|
202
211
|
const value = React.useRef<string>(initialValue);
|
|
203
212
|
const cursorPosition = React.useRef(0);
|
|
@@ -354,7 +363,7 @@ const AutoSuggestion = ({
|
|
|
354
363
|
editorState.suggestions = [];
|
|
355
364
|
setSuggestions([]);
|
|
356
365
|
}
|
|
357
|
-
|
|
366
|
+
setCurrentIndex(0);
|
|
358
367
|
}, [suggestionResponse, editorState]);
|
|
359
368
|
|
|
360
369
|
const getOffsetRange = (cm: EditorView, from: number, to: number) => {
|
|
@@ -368,7 +377,7 @@ const AutoSuggestion = ({
|
|
|
368
377
|
return { fromOffset, toOffset };
|
|
369
378
|
};
|
|
370
379
|
|
|
371
|
-
const
|
|
380
|
+
const inputActionsDisplayed = React.useCallback((node) => {
|
|
372
381
|
if (!node) return;
|
|
373
382
|
const width = node.offsetWidth;
|
|
374
383
|
const slCodeEditor = node.parentElement.getElementsByClassName(`${eccgui}-singlelinecodeeditor`);
|
|
@@ -482,8 +491,7 @@ const AutoSuggestion = ({
|
|
|
482
491
|
}, 1);
|
|
483
492
|
};
|
|
484
493
|
|
|
485
|
-
|
|
486
|
-
const handleInputEditorKeyPress = (event: any) => {
|
|
494
|
+
const handleInputEditorKeyPress = (event: KeyboardEvent) => {
|
|
487
495
|
const overWrittenKeys: Array<string> = Object.values(OVERWRITTEN_KEYS);
|
|
488
496
|
if (overWrittenKeys.includes(event.key) && (useTabForCompletions || event.key !== OVERWRITTEN_KEYS.Tab)) {
|
|
489
497
|
//don't prevent when enter should create new line (multiline config) and dropdown isn't shown
|
|
@@ -619,6 +627,7 @@ const AutoSuggestion = ({
|
|
|
619
627
|
break;
|
|
620
628
|
default:
|
|
621
629
|
//do nothing
|
|
630
|
+
closeDropDown();
|
|
622
631
|
}
|
|
623
632
|
}
|
|
624
633
|
};
|
|
@@ -653,6 +662,8 @@ const AutoSuggestion = ({
|
|
|
653
662
|
showScrollBar={showScrollBar}
|
|
654
663
|
multiline={multiline}
|
|
655
664
|
onMouseDown={handleInputMouseDown}
|
|
665
|
+
height={height}
|
|
666
|
+
readOnly={readOnly}
|
|
656
667
|
/>
|
|
657
668
|
);
|
|
658
669
|
}, [
|
|
@@ -665,6 +676,7 @@ const AutoSuggestion = ({
|
|
|
665
676
|
showScrollBar,
|
|
666
677
|
multiline,
|
|
667
678
|
handleInputMouseDown,
|
|
679
|
+
readOnly
|
|
668
680
|
]);
|
|
669
681
|
|
|
670
682
|
const hasError = !!value.current && !pathIsValid && !pathValidationPending;
|
|
@@ -673,6 +685,7 @@ const AutoSuggestion = ({
|
|
|
673
685
|
id={id}
|
|
674
686
|
ref={autoSuggestionDivRef}
|
|
675
687
|
className={`${eccgui}-autosuggestion` + (className ? ` ${className}` : "")}
|
|
688
|
+
{...outerDivAttributes}
|
|
676
689
|
>
|
|
677
690
|
<div
|
|
678
691
|
className={` ${eccgui}-autosuggestion__inputfield ${BlueprintClassNames.INPUT_GROUP} ${
|
|
@@ -703,11 +716,12 @@ const AutoSuggestion = ({
|
|
|
703
716
|
{codeEditor}
|
|
704
717
|
</ContextOverlay>
|
|
705
718
|
{!!value.current && (
|
|
706
|
-
<span className={BlueprintClassNames.INPUT_ACTION} ref={
|
|
719
|
+
<span className={BlueprintClassNames.INPUT_ACTION} ref={inputActionsDisplayed}>
|
|
707
720
|
<IconButton
|
|
708
721
|
data-test-id={"value-path-clear-btn"}
|
|
709
722
|
name="operation-clear"
|
|
710
723
|
text={clearIconText}
|
|
724
|
+
disabled={readOnly}
|
|
711
725
|
onClick={handleInputEditorClear}
|
|
712
726
|
/>
|
|
713
727
|
</span>
|
|
@@ -61,6 +61,10 @@ export interface ExtendedCodeEditorProps {
|
|
|
61
61
|
| "additionalExtensions"
|
|
62
62
|
| "outerDivAttributes"
|
|
63
63
|
>;
|
|
64
|
+
/** Optional height of the component */
|
|
65
|
+
height?: number | string;
|
|
66
|
+
/** Set read-only mode. Default: false */
|
|
67
|
+
readOnly?: boolean;
|
|
64
68
|
}
|
|
65
69
|
|
|
66
70
|
export type IEditorProps = ExtendedCodeEditorProps;
|
|
@@ -80,6 +84,8 @@ export const ExtendedCodeEditor = ({
|
|
|
80
84
|
onCursorChange,
|
|
81
85
|
onSelection,
|
|
82
86
|
codeEditorProps,
|
|
87
|
+
height,
|
|
88
|
+
readOnly
|
|
83
89
|
}: ExtendedCodeEditorProps) => {
|
|
84
90
|
const initialContent = React.useRef(multiline ? initialValue : initialValue.replace(/[\r\n]/g, " "));
|
|
85
91
|
const multilineExtensions = multiline
|
|
@@ -102,6 +108,8 @@ export const ExtendedCodeEditor = ({
|
|
|
102
108
|
shouldHaveMinimalSetup={false}
|
|
103
109
|
preventLineNumbers={!multiline}
|
|
104
110
|
mode={mode}
|
|
111
|
+
height={height}
|
|
112
|
+
readOnly={readOnly}
|
|
105
113
|
name=""
|
|
106
114
|
enableTab={enableTab}
|
|
107
115
|
additionalExtensions={[...multilineExtensions]}
|
|
@@ -50,6 +50,7 @@ export const IconButton = ({
|
|
|
50
50
|
const defaultIconTooltipProps = {
|
|
51
51
|
hoverOpenDelay: 1000,
|
|
52
52
|
openOnTargetFocus: restProps.disabled || (restProps.tabIndex ?? 0) < 0 ? false : undefined,
|
|
53
|
+
swapPlaceholderDelay: 10,
|
|
53
54
|
};
|
|
54
55
|
const iconProps = {
|
|
55
56
|
small: restProps.small,
|
|
@@ -42,6 +42,12 @@ export interface TooltipProps extends Omit<BlueprintTooltipProps, "position"> {
|
|
|
42
42
|
* You can prevent it in any case by setting it to `false`.
|
|
43
43
|
*/
|
|
44
44
|
usePlaceholder?: boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Time after the placeholder element is replaced by the actual tooltip component.
|
|
47
|
+
* Must be greater than 0.
|
|
48
|
+
* For the first display of the tooltip this time adds up to `hoverOpenDelay`.
|
|
49
|
+
*/
|
|
50
|
+
swapPlaceholderDelay?: number;
|
|
45
51
|
}
|
|
46
52
|
|
|
47
53
|
export const Tooltip = ({
|
|
@@ -53,18 +59,21 @@ export const Tooltip = ({
|
|
|
53
59
|
markdownEnabler = "\n\n",
|
|
54
60
|
markdownProps,
|
|
55
61
|
usePlaceholder,
|
|
56
|
-
|
|
62
|
+
swapPlaceholderDelay = 100,
|
|
63
|
+
hoverOpenDelay = 450,
|
|
57
64
|
...otherTooltipProps
|
|
58
65
|
}: TooltipProps) => {
|
|
59
66
|
const placeholderRef = React.useRef(null);
|
|
60
67
|
const eventMemory = React.useRef<null | "afterhover" | "afterfocus">(null);
|
|
61
68
|
const searchId = React.useRef<null | string>(null);
|
|
62
|
-
const
|
|
69
|
+
const swapDelay = React.useRef<null | NodeJS.Timeout>(null);
|
|
70
|
+
const swapDelayTime = swapPlaceholderDelay;
|
|
63
71
|
const [placeholder, setPlaceholder] = React.useState<boolean>(
|
|
64
72
|
!otherTooltipProps.disabled &&
|
|
65
73
|
!otherTooltipProps.defaultIsOpen &&
|
|
66
74
|
!otherTooltipProps.isOpen &&
|
|
67
75
|
otherTooltipProps.renderTarget === undefined &&
|
|
76
|
+
swapDelayTime > 0 &&
|
|
68
77
|
hoverOpenDelay > swapDelayTime &&
|
|
69
78
|
(usePlaceholder === true || (typeof content === "string" && usePlaceholder !== false))
|
|
70
79
|
);
|
|
@@ -77,7 +86,10 @@ export const Tooltip = ({
|
|
|
77
86
|
React.useEffect(() => {
|
|
78
87
|
if (placeholderRef.current !== null) {
|
|
79
88
|
const swap = (ev: MouseEvent | globalThis.FocusEvent) => {
|
|
80
|
-
|
|
89
|
+
if (swapDelay.current) {
|
|
90
|
+
clearTimeout(swapDelay.current);
|
|
91
|
+
}
|
|
92
|
+
swapDelay.current = setTimeout(() => {
|
|
81
93
|
// we delay the swap to prevent unwanted effects
|
|
82
94
|
// (e.g. forced mouseover after the swap but the cursor is already somewhere else)
|
|
83
95
|
eventMemory.current = ev.type === "focusin" ? "afterfocus" : "afterhover";
|
|
@@ -93,7 +105,7 @@ export const Tooltip = ({
|
|
|
93
105
|
) {
|
|
94
106
|
eventMemory.current = null;
|
|
95
107
|
}
|
|
96
|
-
clearTimeout(swapDelay);
|
|
108
|
+
clearTimeout(swapDelay.current as NodeJS.Timeout);
|
|
97
109
|
});
|
|
98
110
|
}
|
|
99
111
|
};
|
|
@@ -122,7 +134,15 @@ export const Tooltip = ({
|
|
|
122
134
|
(target as HTMLElement).focus();
|
|
123
135
|
break;
|
|
124
136
|
case "afterhover":
|
|
125
|
-
|
|
137
|
+
// re-check if the cursor is still over the element after swapping the placeholder before triggering the event to bubble up
|
|
138
|
+
(target as HTMLElement).addEventListener(
|
|
139
|
+
"mouseover",
|
|
140
|
+
() => (target as HTMLElement).dispatchEvent(new MouseEvent("mouseover", { bubbles: true })),
|
|
141
|
+
{
|
|
142
|
+
capture: true,
|
|
143
|
+
once: true,
|
|
144
|
+
}
|
|
145
|
+
);
|
|
126
146
|
break;
|
|
127
147
|
}
|
|
128
148
|
}
|
|
@@ -166,7 +186,7 @@ export const Tooltip = ({
|
|
|
166
186
|
) : (
|
|
167
187
|
<BlueprintTooltip
|
|
168
188
|
lazy={true}
|
|
169
|
-
hoverOpenDelay={hoverOpenDelay
|
|
189
|
+
hoverOpenDelay={hoverOpenDelay}
|
|
170
190
|
{...otherTooltipProps}
|
|
171
191
|
content={tooltipContent}
|
|
172
192
|
className={targetClassName}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React, { useMemo, useRef } from "react";
|
|
2
2
|
import { defaultKeymap, indentWithTab } from "@codemirror/commands";
|
|
3
3
|
import { foldKeymap } from "@codemirror/language";
|
|
4
|
-
import { EditorState, Extension } from "@codemirror/state";
|
|
4
|
+
import { EditorState, Extension, Compartment } from "@codemirror/state";
|
|
5
5
|
import { DOMEventHandlers, EditorView, KeyBinding, keymap, Rect, ViewUpdate } from "@codemirror/view";
|
|
6
6
|
import { minimalSetup } from "codemirror";
|
|
7
7
|
|
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
adaptedHighlightSpecialChars,
|
|
31
31
|
adaptedLineNumbers,
|
|
32
32
|
adaptedLintGutter,
|
|
33
|
-
adaptedPlaceholder,
|
|
33
|
+
adaptedPlaceholder, compartment,
|
|
34
34
|
} from "./tests/codemirrorTestHelper";
|
|
35
35
|
import { ExtensionCreator } from "./types";
|
|
36
36
|
|
|
@@ -53,6 +53,7 @@ export interface CodeEditorProps extends TestableComponent {
|
|
|
53
53
|
/**
|
|
54
54
|
* Handler method to receive onChange events.
|
|
55
55
|
* As input the new value is given.
|
|
56
|
+
* @deprecated (v25) use `(v: string) => void` in future
|
|
56
57
|
*/
|
|
57
58
|
onChange?: (v: any) => void;
|
|
58
59
|
/**
|
|
@@ -74,7 +75,7 @@ export interface CodeEditorProps extends TestableComponent {
|
|
|
74
75
|
/**
|
|
75
76
|
* Called when the cursor position changes
|
|
76
77
|
*/
|
|
77
|
-
onCursorChange?: (pos: number, coords: Rect, scrollinfo: HTMLElement, cm: EditorView) =>
|
|
78
|
+
onCursorChange?: (pos: number, coords: Rect, scrollinfo: HTMLElement, cm: EditorView) => void;
|
|
78
79
|
|
|
79
80
|
/**
|
|
80
81
|
* Syntax mode of the code editor.
|
|
@@ -83,7 +84,7 @@ export interface CodeEditorProps extends TestableComponent {
|
|
|
83
84
|
/**
|
|
84
85
|
* Default value used first when the editor is instanciated.
|
|
85
86
|
*/
|
|
86
|
-
defaultValue?:
|
|
87
|
+
defaultValue?: string;
|
|
87
88
|
/**
|
|
88
89
|
* If enabled the code editor won't show numbers before each line.
|
|
89
90
|
*/
|
|
@@ -169,7 +170,7 @@ export interface CodeEditorProps extends TestableComponent {
|
|
|
169
170
|
}
|
|
170
171
|
|
|
171
172
|
const addExtensionsFor = (flag: boolean, ...extensions: Extension[]) => (flag ? [...extensions] : []);
|
|
172
|
-
const addToKeyMapConfigFor = (flag: boolean, ...keys:
|
|
173
|
+
const addToKeyMapConfigFor = (flag: boolean, ...keys: KeyBinding[]) => (flag ? [...keys] : []);
|
|
173
174
|
const addHandlersFor = (flag: boolean, handlerName: string, handler: any) =>
|
|
174
175
|
flag ? ({ [handlerName]: handler } as DOMEventHandlers<any>) : {};
|
|
175
176
|
|
|
@@ -221,7 +222,24 @@ export const CodeEditor = ({
|
|
|
221
222
|
}: CodeEditorProps) => {
|
|
222
223
|
const parent = useRef<any>(undefined);
|
|
223
224
|
const [view, setView] = React.useState<EditorView | undefined>();
|
|
225
|
+
const currentView = React.useRef<EditorView>()
|
|
226
|
+
currentView.current = view
|
|
227
|
+
const currentReadOnly = React.useRef(readOnly)
|
|
228
|
+
currentReadOnly.current = readOnly
|
|
224
229
|
const [showPreview, setShowPreview] = React.useState<boolean>(false);
|
|
230
|
+
// CodeMirror Compartments in order to allow for re-configuration after initialization
|
|
231
|
+
const readOnlyCompartment = React.useRef<Compartment>(compartment())
|
|
232
|
+
const wrapLinesCompartment = React.useRef<Compartment>(compartment())
|
|
233
|
+
const preventLineNumbersCompartment = React.useRef<Compartment>(compartment())
|
|
234
|
+
const shouldHaveMinimalSetupCompartment = React.useRef<Compartment>(compartment())
|
|
235
|
+
const placeholderCompartment = React.useRef<Compartment>(compartment())
|
|
236
|
+
const modeCompartment = React.useRef<Compartment>(compartment())
|
|
237
|
+
const keyMapConfigsCompartment = React.useRef<Compartment>(compartment())
|
|
238
|
+
const tabIntentSizeCompartment = React.useRef<Compartment>(compartment())
|
|
239
|
+
const disabledCompartment = React.useRef<Compartment>(compartment())
|
|
240
|
+
const supportCodeFoldingCompartment = React.useRef<Compartment>(compartment())
|
|
241
|
+
const useLintingCompartment = React.useRef<Compartment>(compartment())
|
|
242
|
+
const shouldHighlightActiveLineCompartment = React.useRef<Compartment>(compartment())
|
|
225
243
|
|
|
226
244
|
const linters = useMemo(() => {
|
|
227
245
|
if (!mode) {
|
|
@@ -240,17 +258,15 @@ export const CodeEditor = ({
|
|
|
240
258
|
|
|
241
259
|
const onKeyDownHandler = (event: KeyboardEvent, view: EditorView) => {
|
|
242
260
|
if (onKeyDown && !onKeyDown(event)) {
|
|
243
|
-
if (event.key === "Enter") {
|
|
261
|
+
if (event.key === "Enter" && !currentReadOnly.current) {
|
|
244
262
|
const cursor = view.state.selection.main.head;
|
|
245
|
-
const cursorLine = view.state.doc.lineAt(cursor).number;
|
|
246
|
-
const offsetFromFirstLine = view.state.doc.line(cursorLine).to;
|
|
247
263
|
view.dispatch({
|
|
248
264
|
changes: {
|
|
249
|
-
from:
|
|
265
|
+
from: cursor,
|
|
250
266
|
insert: "\n",
|
|
251
267
|
},
|
|
252
268
|
selection: {
|
|
253
|
-
anchor:
|
|
269
|
+
anchor: cursor + 1,
|
|
254
270
|
},
|
|
255
271
|
});
|
|
256
272
|
}
|
|
@@ -265,14 +281,17 @@ export const CodeEditor = ({
|
|
|
265
281
|
return false;
|
|
266
282
|
};
|
|
267
283
|
|
|
268
|
-
|
|
284
|
+
const createKeyMapConfigs = () => {
|
|
269
285
|
const tabIndent =
|
|
270
286
|
!!(tabIntentStyle === "tab" && mode && !(tabForceSpaceForModes ?? []).includes(mode)) || enableTab;
|
|
271
|
-
|
|
287
|
+
return [
|
|
272
288
|
defaultKeymap as KeyBinding,
|
|
273
|
-
...addToKeyMapConfigFor(supportCodeFolding, foldKeymap),
|
|
289
|
+
...addToKeyMapConfigFor(supportCodeFolding, ...foldKeymap),
|
|
274
290
|
...addToKeyMapConfigFor(tabIndent, indentWithTab),
|
|
275
291
|
];
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
React.useEffect(() => {
|
|
276
295
|
const domEventHandlers = {
|
|
277
296
|
...addHandlersFor(!!onScroll, "scroll", onScroll),
|
|
278
297
|
...addHandlersFor(
|
|
@@ -286,13 +305,13 @@ export const CodeEditor = ({
|
|
|
286
305
|
} as DOMEventHandlers<any>;
|
|
287
306
|
const extensions = [
|
|
288
307
|
markField,
|
|
289
|
-
adaptedPlaceholder(placeholder),
|
|
308
|
+
placeholderCompartment.current.of(adaptedPlaceholder(placeholder)),
|
|
290
309
|
adaptedHighlightSpecialChars(),
|
|
291
|
-
useCodeMirrorModeExtension(mode),
|
|
292
|
-
keymap?.of(
|
|
293
|
-
EditorState?.tabSize.of(tabIntentSize),
|
|
294
|
-
EditorState?.readOnly.of(readOnly),
|
|
295
|
-
EditorView?.editable.of(!disabled),
|
|
310
|
+
modeCompartment.current.of(useCodeMirrorModeExtension(mode)),
|
|
311
|
+
keyMapConfigsCompartment.current.of(keymap?.of(createKeyMapConfigs())),
|
|
312
|
+
tabIntentSizeCompartment.current.of(EditorState?.tabSize.of(tabIntentSize)),
|
|
313
|
+
readOnlyCompartment.current.of(EditorState?.readOnly.of(readOnly)),
|
|
314
|
+
disabledCompartment.current.of(EditorView?.editable.of(!disabled)),
|
|
296
315
|
AdaptedEditorViewDomEventHandlers(domEventHandlers) as Extension,
|
|
297
316
|
EditorView?.updateListener.of((v: ViewUpdate) => {
|
|
298
317
|
if (disabled) return;
|
|
@@ -328,12 +347,12 @@ export const CodeEditor = ({
|
|
|
328
347
|
}
|
|
329
348
|
}
|
|
330
349
|
}),
|
|
331
|
-
addExtensionsFor(shouldHaveMinimalSetup, minimalSetup),
|
|
332
|
-
addExtensionsFor(!preventLineNumbers, adaptedLineNumbers()),
|
|
333
|
-
addExtensionsFor(shouldHighlightActiveLine, adaptedHighlightActiveLine()),
|
|
334
|
-
addExtensionsFor(wrapLines, EditorView?.lineWrapping),
|
|
335
|
-
addExtensionsFor(supportCodeFolding, adaptedFoldGutter(), adaptedCodeFolding()),
|
|
336
|
-
addExtensionsFor(useLinting, ...linters),
|
|
350
|
+
shouldHaveMinimalSetupCompartment.current.of(addExtensionsFor(shouldHaveMinimalSetup, minimalSetup)),
|
|
351
|
+
preventLineNumbersCompartment.current.of(addExtensionsFor(!preventLineNumbers, adaptedLineNumbers())),
|
|
352
|
+
shouldHighlightActiveLineCompartment.current.of(addExtensionsFor(shouldHighlightActiveLine, adaptedHighlightActiveLine())),
|
|
353
|
+
wrapLinesCompartment.current.of(addExtensionsFor(wrapLines, EditorView?.lineWrapping)),
|
|
354
|
+
supportCodeFoldingCompartment.current.of(addExtensionsFor(supportCodeFolding, adaptedFoldGutter(), adaptedCodeFolding())),
|
|
355
|
+
useLintingCompartment.current.of(addExtensionsFor(useLinting, ...linters)),
|
|
337
356
|
additionalExtensions,
|
|
338
357
|
];
|
|
339
358
|
|
|
@@ -375,7 +394,64 @@ export const CodeEditor = ({
|
|
|
375
394
|
setView(undefined);
|
|
376
395
|
}
|
|
377
396
|
};
|
|
378
|
-
}, [parent.current
|
|
397
|
+
}, [parent.current]);
|
|
398
|
+
|
|
399
|
+
// Updates an extension for a specific parameter that has changed after the initialization
|
|
400
|
+
const updateExtension = (extension: Extension | undefined, parameterCompartment: Compartment): void => {
|
|
401
|
+
if(extension) {
|
|
402
|
+
currentView.current?.dispatch({
|
|
403
|
+
effects: parameterCompartment.reconfigure(extension)
|
|
404
|
+
})
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
React.useEffect(() => {
|
|
409
|
+
updateExtension(EditorState?.readOnly.of(readOnly!), readOnlyCompartment.current)
|
|
410
|
+
}, [readOnly])
|
|
411
|
+
|
|
412
|
+
React.useEffect(() => {
|
|
413
|
+
updateExtension(adaptedPlaceholder(placeholder), placeholderCompartment.current)
|
|
414
|
+
}, [placeholder])
|
|
415
|
+
|
|
416
|
+
React.useEffect(() => {
|
|
417
|
+
updateExtension(useCodeMirrorModeExtension(mode), modeCompartment.current)
|
|
418
|
+
}, [mode])
|
|
419
|
+
|
|
420
|
+
React.useEffect(() => {
|
|
421
|
+
updateExtension(keymap?.of(createKeyMapConfigs()), keyMapConfigsCompartment.current)
|
|
422
|
+
}, [supportCodeFolding, mode, tabIntentStyle, (tabForceSpaceForModes ?? []).join(", "), enableTab])
|
|
423
|
+
|
|
424
|
+
React.useEffect(() => {
|
|
425
|
+
updateExtension(EditorState?.tabSize.of(tabIntentSize ?? 2), tabIntentSizeCompartment.current)
|
|
426
|
+
}, [tabIntentSize])
|
|
427
|
+
|
|
428
|
+
React.useEffect(() => {
|
|
429
|
+
updateExtension(EditorView?.editable.of(!disabled), disabledCompartment.current)
|
|
430
|
+
}, [disabled])
|
|
431
|
+
|
|
432
|
+
React.useEffect(() => {
|
|
433
|
+
updateExtension(addExtensionsFor(shouldHaveMinimalSetup ?? true, minimalSetup), shouldHaveMinimalSetupCompartment.current)
|
|
434
|
+
}, [shouldHaveMinimalSetup])
|
|
435
|
+
|
|
436
|
+
React.useEffect(() => {
|
|
437
|
+
updateExtension(addExtensionsFor(!preventLineNumbers, adaptedLineNumbers()), preventLineNumbersCompartment.current)
|
|
438
|
+
}, [preventLineNumbers])
|
|
439
|
+
|
|
440
|
+
React.useEffect(() => {
|
|
441
|
+
updateExtension(addExtensionsFor(shouldHighlightActiveLine ?? false, adaptedHighlightActiveLine()), shouldHighlightActiveLineCompartment.current)
|
|
442
|
+
}, [shouldHighlightActiveLine])
|
|
443
|
+
|
|
444
|
+
React.useEffect(() => {
|
|
445
|
+
updateExtension(addExtensionsFor(wrapLines ?? false, EditorView?.lineWrapping), wrapLinesCompartment.current)
|
|
446
|
+
}, [wrapLines])
|
|
447
|
+
|
|
448
|
+
React.useEffect(() => {
|
|
449
|
+
updateExtension(addExtensionsFor(supportCodeFolding ?? false, adaptedFoldGutter(), adaptedCodeFolding()), supportCodeFoldingCompartment.current)
|
|
450
|
+
}, [supportCodeFolding])
|
|
451
|
+
|
|
452
|
+
React.useEffect(() => {
|
|
453
|
+
updateExtension(addExtensionsFor(useLinting ?? false, ...linters), useLintingCompartment.current)
|
|
454
|
+
}, [mode, useLinting])
|
|
379
455
|
|
|
380
456
|
const hasToolbarSupport = mode && ModeToolbarSupport.indexOf(mode) > -1 && useToolbar;
|
|
381
457
|
|