@eccenca/gui-elements 23.6.0 → 23.7.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/cjs/cmem/markdown/Markdown.js +10 -2
  3. package/dist/cjs/cmem/markdown/Markdown.js.map +1 -1
  4. package/dist/cjs/cmem/react-flow/StickyNoteModal/StickyNoteModal.js +4 -4
  5. package/dist/cjs/cmem/react-flow/StickyNoteModal/StickyNoteModal.js.map +1 -1
  6. package/dist/cjs/common/index.js +2 -0
  7. package/dist/cjs/common/index.js.map +1 -1
  8. package/dist/cjs/common/utils/getScrollParent.js +24 -0
  9. package/dist/cjs/common/utils/getScrollParent.js.map +1 -0
  10. package/dist/cjs/components/AutocompleteField/AutoCompleteField.js +19 -2
  11. package/dist/cjs/components/AutocompleteField/AutoCompleteField.js.map +1 -1
  12. package/dist/cjs/components/MultiSelect/MultiSelect.js +66 -41
  13. package/dist/cjs/components/MultiSelect/MultiSelect.js.map +1 -1
  14. package/dist/cjs/components/Sticky/StickyTarget.js +85 -0
  15. package/dist/cjs/components/Sticky/StickyTarget.js.map +1 -0
  16. package/dist/cjs/components/Sticky/index.js +14 -0
  17. package/dist/cjs/components/Sticky/index.js.map +1 -0
  18. package/dist/cjs/components/index.js +1 -0
  19. package/dist/cjs/components/index.js.map +1 -1
  20. package/dist/cjs/extensions/codemirror/CodeMirror.js +1 -1
  21. package/dist/cjs/extensions/codemirror/CodeMirror.js.map +1 -1
  22. package/dist/cjs/extensions/react-flow/edges/EdgeDefault.js +7 -7
  23. package/dist/cjs/extensions/react-flow/edges/EdgeDefault.js.map +1 -1
  24. package/dist/cjs/extensions/react-flow/edges/EdgeLabel.js +3 -2
  25. package/dist/cjs/extensions/react-flow/edges/EdgeLabel.js.map +1 -1
  26. package/dist/cjs/extensions/react-flow/handles/HandleContent.js +14 -2
  27. package/dist/cjs/extensions/react-flow/handles/HandleContent.js.map +1 -1
  28. package/dist/cjs/extensions/react-flow/handles/HandleTools.js +1 -1
  29. package/dist/cjs/extensions/react-flow/handles/HandleTools.js.map +1 -1
  30. package/dist/cjs/extensions/react-flow/minimap/MiniMap.js +6 -8
  31. package/dist/cjs/extensions/react-flow/minimap/MiniMap.js.map +1 -1
  32. package/dist/esm/cmem/markdown/Markdown.js +10 -2
  33. package/dist/esm/cmem/markdown/Markdown.js.map +1 -1
  34. package/dist/esm/cmem/react-flow/StickyNoteModal/StickyNoteModal.js +5 -5
  35. package/dist/esm/cmem/react-flow/StickyNoteModal/StickyNoteModal.js.map +1 -1
  36. package/dist/esm/common/index.js +2 -0
  37. package/dist/esm/common/index.js.map +1 -1
  38. package/dist/esm/common/utils/getScrollParent.js +20 -0
  39. package/dist/esm/common/utils/getScrollParent.js.map +1 -0
  40. package/dist/esm/components/AutocompleteField/AutoCompleteField.js +27 -2
  41. package/dist/esm/components/AutocompleteField/AutoCompleteField.js.map +1 -1
  42. package/dist/esm/components/MultiSelect/MultiSelect.js +72 -52
  43. package/dist/esm/components/MultiSelect/MultiSelect.js.map +1 -1
  44. package/dist/esm/components/Sticky/StickyTarget.js +89 -0
  45. package/dist/esm/components/Sticky/StickyTarget.js.map +1 -0
  46. package/dist/esm/components/Sticky/index.js +2 -0
  47. package/dist/esm/components/Sticky/index.js.map +1 -0
  48. package/dist/esm/components/index.js +1 -0
  49. package/dist/esm/components/index.js.map +1 -1
  50. package/dist/esm/extensions/codemirror/CodeMirror.js +1 -1
  51. package/dist/esm/extensions/codemirror/CodeMirror.js.map +1 -1
  52. package/dist/esm/extensions/react-flow/edges/EdgeDefault.js +7 -7
  53. package/dist/esm/extensions/react-flow/edges/EdgeDefault.js.map +1 -1
  54. package/dist/esm/extensions/react-flow/edges/EdgeLabel.js +3 -2
  55. package/dist/esm/extensions/react-flow/edges/EdgeLabel.js.map +1 -1
  56. package/dist/esm/extensions/react-flow/handles/HandleContent.js +13 -2
  57. package/dist/esm/extensions/react-flow/handles/HandleContent.js.map +1 -1
  58. package/dist/esm/extensions/react-flow/handles/HandleTools.js +1 -1
  59. package/dist/esm/extensions/react-flow/handles/HandleTools.js.map +1 -1
  60. package/dist/esm/extensions/react-flow/minimap/MiniMap.js +6 -8
  61. package/dist/esm/extensions/react-flow/minimap/MiniMap.js.map +1 -1
  62. package/dist/types/cmem/react-flow/StickyNoteModal/StickyNoteModal.d.ts +5 -0
  63. package/dist/types/cmem/react-flow/configuration/graph.d.ts +9 -9
  64. package/dist/types/cmem/react-flow/configuration/unspecified.d.ts +2 -2
  65. package/dist/types/common/index.d.ts +2 -0
  66. package/dist/types/common/utils/getScrollParent.d.ts +9 -0
  67. package/dist/types/components/AutocompleteField/AutoCompleteField.d.ts +2 -0
  68. package/dist/types/components/MultiSelect/MultiSelect.d.ts +1 -1
  69. package/dist/types/components/Sticky/StickyTarget.d.ts +32 -0
  70. package/dist/types/components/Sticky/index.d.ts +1 -0
  71. package/dist/types/components/index.d.ts +1 -0
  72. package/dist/types/extensions/react-flow/edges/EdgeDefault.d.ts +6 -1
  73. package/dist/types/extensions/react-flow/edges/EdgeLabel.d.ts +1 -1
  74. package/dist/types/extensions/react-flow/edges/edgeTypes.d.ts +10 -10
  75. package/dist/types/extensions/react-flow/handles/HandleContent.d.ts +2 -3
  76. package/dist/types/extensions/react-flow/minimap/MiniMap.d.ts +12 -1
  77. package/package.json +3 -1
  78. package/src/cmem/markdown/Markdown.stories.tsx +8 -1
  79. package/src/cmem/markdown/Markdown.tsx +22 -1
  80. package/src/cmem/react-flow/ReactFlow/ReactFlow.stories.tsx +10 -4
  81. package/src/cmem/react-flow/StickyNoteModal/StickyNoteModal.tsx +8 -3
  82. package/src/common/index.ts +3 -0
  83. package/src/common/utils/getScrollParent.ts +20 -0
  84. package/src/components/AutocompleteField/AutoCompleteField.tsx +28 -0
  85. package/src/components/AutocompleteField/autocompletefield.scss +1 -1
  86. package/src/components/MultiSelect/MultiSelect.tsx +72 -47
  87. package/src/components/MultiSuggestField/MultiSuggestField.stories.tsx +146 -26
  88. package/src/components/MultiSuggestField/tests/MultiSuggestField.test.tsx +363 -61
  89. package/src/components/Sticky/StickyTarget.tsx +119 -0
  90. package/src/components/Sticky/index.ts +1 -0
  91. package/src/components/Sticky/sticky.scss +69 -0
  92. package/src/components/Sticky/stories/StickyTarget.stories.tsx +63 -0
  93. package/src/components/index.scss +1 -0
  94. package/src/components/index.ts +1 -0
  95. package/src/extensions/codemirror/CodeMirror.tsx +1 -1
  96. package/src/extensions/react-flow/edges/EdgeDefault.tsx +70 -62
  97. package/src/extensions/react-flow/edges/EdgeLabel.tsx +14 -2
  98. package/src/extensions/react-flow/edges/stories/EdgeDefault.stories.tsx +11 -5
  99. package/src/extensions/react-flow/edges/stories/EdgeLabel.stories.tsx +2 -0
  100. package/src/extensions/react-flow/handles/HandleContent.tsx +28 -25
  101. package/src/extensions/react-flow/handles/HandleTools.tsx +1 -0
  102. package/src/extensions/react-flow/handles/stories/HandleDefault.stories.tsx +5 -1
  103. package/src/extensions/react-flow/minimap/MiniMap.stories.tsx +62 -0
  104. package/src/extensions/react-flow/minimap/MiniMap.tsx +23 -7
  105. package/src/extensions/react-flow/nodes/stories/NodeContent.stories.tsx +2 -0
  106. package/src/extensions/react-flow/nodes/stories/NodeContentExtension.stories.tsx +2 -0
  107. package/src/extensions/react-flow/nodes/stories/NodeDefault.stories.tsx +0 -1
@@ -1,105 +1,407 @@
1
- import React from "react";
1
+ import React, { useCallback, useState } from "react";
2
2
  import { fireEvent, render, screen, waitFor } from "@testing-library/react";
3
3
 
4
4
  import "@testing-library/jest-dom";
5
5
 
6
6
  import { MultiSuggestField } from "../MultiSuggestField";
7
- import { Default, dropdownOnFocus, predefinedValues } from "../MultiSuggestField.stories";
7
+ import { Default, dropdownOnFocus, predefinedNotControlledValues } from "../MultiSuggestField.stories";
8
+
9
+ const testLabels = ["label1", "label2", "label3", "label4", "label5"];
10
+
11
+ const items = new Array(5).fill(undefined).map((_, id) => {
12
+ const testLabel = testLabels[id];
13
+ return { testLabel, testId: `${testLabel}-id` };
14
+ });
15
+
16
+ export const TestComponent = (): JSX.Element => {
17
+ const copy: Array<{ testLabel: string; testId: string }> = [items[2]];
18
+
19
+ const [selected, setSelected] = useState(copy);
20
+
21
+ const handleOnSelect = useCallback((params) => {
22
+ const items = params.selectedItems;
23
+ setSelected(items);
24
+ }, []);
25
+
26
+ const handleReset = (): void => {
27
+ setSelected(copy);
28
+ };
29
+
30
+ return (
31
+ <div>
32
+ <button data-testid="reset-button" onClick={handleReset}>
33
+ Reset
34
+ </button>
35
+ <br />
36
+ <br />
37
+ <MultiSuggestField<{ testLabel: string; testId: string }>
38
+ items={items}
39
+ createNewItemFromQuery={(query) => ({ testId: `${query}-id`, testLabel: query })}
40
+ onSelection={handleOnSelect}
41
+ itemId={({ testId }) => testId}
42
+ itemLabel={({ testLabel }) => testLabel}
43
+ selectedItems={selected}
44
+ />
45
+ </div>
46
+ );
47
+ };
8
48
 
9
49
  describe("MultiSuggestField", () => {
10
- it("should render default input", () => {
11
- const { container } = render(<MultiSuggestField {...Default.args} />);
12
- const [input] = container.getElementsByClassName("eccgui-multiselect");
50
+ describe("uncontrolled", () => {
51
+ it("should render default input", () => {
52
+ const { container } = render(<MultiSuggestField {...Default.args} />);
53
+ const [input] = container.getElementsByClassName("eccgui-multiselect");
13
54
 
14
- expect(input).toBeInTheDocument();
15
- });
55
+ expect(input).toBeInTheDocument();
56
+ });
16
57
 
17
- it("should render default selected items", async () => {
18
- const { getByTestId } = render(<MultiSuggestField {...predefinedValues.args} />);
58
+ it("should render default selected items", async () => {
59
+ const { getByText } = render(<MultiSuggestField {...predefinedNotControlledValues.args} />);
19
60
 
20
- const [firstSelected, secondSelected]: Array<string> = predefinedValues.args.selectedItems.map(
21
- ({ testLabel }) => testLabel.trim()
22
- );
61
+ const [firstSelected, secondSelected]: Array<string> = predefinedNotControlledValues.args.selectedItems.map(
62
+ ({ testLabel }) => testLabel
63
+ );
23
64
 
24
- await waitFor(() => {
25
- expect(getByTestId(firstSelected)).toBeInTheDocument();
26
- expect(getByTestId(secondSelected)).toBeInTheDocument();
65
+ await waitFor(() => {
66
+ expect(getByText(firstSelected)).toBeInTheDocument();
67
+ expect(getByText(secondSelected)).toBeInTheDocument();
68
+ });
27
69
  });
28
- });
29
70
 
30
- it("should clear all selected items on clear button click", async () => {
31
- const { queryByTestId, container } = render(<MultiSuggestField {...predefinedValues.args} />);
71
+ it("should clear all selected items on clear button click", async () => {
72
+ const { container } = render(
73
+ <MultiSuggestField
74
+ data-test-id="multi-suggest-field"
75
+ {...predefinedNotControlledValues.args}
76
+ onSelection={undefined}
77
+ />
78
+ );
32
79
 
33
- const [firstSelected, secondSelected]: Array<string> = predefinedValues.args.selectedItems.map(
34
- ({ testLabel }) => testLabel.trim()
35
- );
80
+ const selectedLength = predefinedNotControlledValues.args.selectedItems.length;
36
81
 
37
- const clearButton = container.querySelector('[data-test-id="clear-all-items"');
82
+ expect(container.querySelectorAll("[data-tag-index]").length).toBe(selectedLength);
38
83
 
39
- expect(clearButton).toBeInTheDocument();
84
+ const clearButton = container.querySelector('[data-test-id="clear-all-items"');
40
85
 
41
- fireEvent.click(clearButton!);
86
+ expect(clearButton).toBeInTheDocument();
42
87
 
43
- await waitFor(() => {
44
- expect(queryByTestId(firstSelected)).not.toBeInTheDocument();
45
- expect(queryByTestId(secondSelected)).not.toBeInTheDocument();
88
+ fireEvent.click(clearButton!);
89
+
90
+ await waitFor(() => {
91
+ expect(container.querySelectorAll("[data-tag-index]").length).toBe(0);
92
+ });
46
93
  });
47
- });
48
94
 
49
- it("should filter options and reset them if the query is empty", async () => {
50
- const { container } = render(<MultiSuggestField {...dropdownOnFocus.args} />);
95
+ it("should filter options and reset them if the query is empty", async () => {
96
+ const { container } = render(<MultiSuggestField {...dropdownOnFocus.args} />);
97
+
98
+ const [inputContainer] = container.getElementsByClassName("eccgui-multiselect");
99
+ const [input] = inputContainer.getElementsByTagName("input");
100
+
101
+ fireEvent.click(input);
102
+
103
+ await waitFor(() => {
104
+ const listbox = screen.getByRole("listbox");
105
+ expect(listbox).toBeInTheDocument();
106
+
107
+ const menuItems = listbox.getElementsByClassName("eccgui-menu__item");
108
+ expect(menuItems.length).toBe(dropdownOnFocus.args.items.length);
109
+ });
110
+
111
+ fireEvent.change(input, { target: { value: "ex" } });
112
+
113
+ await waitFor(() => {
114
+ const listbox = screen.getByRole("listbox");
115
+ const menuItems = listbox.getElementsByClassName("eccgui-menu__item");
116
+
117
+ expect(menuItems.length).toBe(1);
118
+
119
+ const noResult = screen.queryByText(dropdownOnFocus.args.noResultText);
120
+ expect(noResult).not.toBeInTheDocument();
121
+ });
122
+
123
+ fireEvent.change(input, { target: { value: "ttt" } });
51
124
 
52
- const [inputContainer] = container.getElementsByClassName("eccgui-multiselect");
53
- const [input] = inputContainer.getElementsByTagName("input");
125
+ await waitFor(() => {
126
+ const listbox = screen.getByRole("listbox");
127
+ const menuItems = listbox.getElementsByClassName("eccgui-menu__item");
54
128
 
55
- fireEvent.click(input);
129
+ expect(menuItems.length).toBe(1);
56
130
 
57
- await waitFor(() => {
58
- const listbox = screen.getByRole("listbox");
59
- expect(listbox).toBeInTheDocument();
131
+ const noResult = screen.queryByText(dropdownOnFocus.args.noResultText);
132
+ expect(noResult).toBeInTheDocument();
133
+ });
60
134
 
61
- const menuItems = listbox.getElementsByClassName("eccgui-menu__item");
62
- expect(menuItems.length).toBe(dropdownOnFocus.args.items.length);
135
+ fireEvent.change(input, { target: { value: "" } });
136
+
137
+ await waitFor(() => {
138
+ const listbox = screen.getByRole("listbox");
139
+ const menuItems = listbox.getElementsByClassName("eccgui-menu__item");
140
+ expect(menuItems.length).toBe(dropdownOnFocus.args.items.length);
141
+ });
63
142
  });
64
143
 
65
- fireEvent.change(input, { target: { value: "ex" } });
144
+ it("should render disable field with selected items", async () => {
145
+ const { container, getByText } = render(
146
+ <MultiSuggestField {...predefinedNotControlledValues.args} disabled />
147
+ );
148
+
149
+ const [inputTargetContainer] = container.getElementsByClassName("eccgui-multiselect__target");
66
150
 
67
- await waitFor(() => {
68
- const listbox = screen.getByRole("listbox");
69
- const menuItems = listbox.getElementsByClassName("eccgui-menu__item");
151
+ expect(inputTargetContainer.getAttribute("aria-disabled")).toBe("true");
70
152
 
71
- expect(menuItems.length).toBe(1);
153
+ const [firstSelected, secondSelected]: Array<string> = predefinedNotControlledValues.args.selectedItems.map(
154
+ ({ testLabel }) => testLabel
155
+ );
72
156
 
73
- const noResult = screen.queryByText(dropdownOnFocus.args.noResultText);
74
- expect(noResult).not.toBeInTheDocument();
157
+ await waitFor(() => {
158
+ expect(getByText(firstSelected)).toBeInTheDocument();
159
+ expect(getByText(secondSelected)).toBeInTheDocument();
160
+ });
75
161
  });
76
162
 
77
- fireEvent.change(input, { target: { value: "ttt" } });
163
+ it("should set deferred selection correctly when only selected items provided and remove selection", async () => {
164
+ const args = { ...predefinedNotControlledValues.args, selectedItems: [] };
165
+
166
+ const { rerender, container } = render(<MultiSuggestField {...args} data-test-id="multi-suggest-field" />);
167
+
168
+ const clearButtonBefore = container.querySelector("[data-test-id='clear-all-items'");
169
+
170
+ expect(clearButtonBefore).not.toBeInTheDocument();
171
+
172
+ const [firstSelected, secondSelected]: Array<string> = predefinedNotControlledValues.args.selectedItems.map(
173
+ ({ testLabel }) => testLabel
174
+ );
78
175
 
79
- await waitFor(() => {
80
- const listbox = screen.getByRole("listbox");
81
- const menuItems = listbox.getElementsByClassName("eccgui-menu__item");
176
+ await waitFor(() => {
177
+ expect(screen.queryByText(firstSelected)).toBeNull();
178
+ expect(screen.queryByText(secondSelected)).toBeNull();
179
+ });
82
180
 
83
- expect(menuItems.length).toBe(1);
181
+ rerender(<MultiSuggestField {...predefinedNotControlledValues.args} data-test-id="multi-suggest-field" />);
84
182
 
85
- const noResult = screen.queryByText(dropdownOnFocus.args.noResultText);
86
- expect(noResult).toBeInTheDocument();
183
+ await waitFor(() => {
184
+ expect(screen.getByText(firstSelected)).toBeInTheDocument();
185
+ expect(screen.getByText(secondSelected)).toBeInTheDocument();
186
+ });
187
+
188
+ await waitFor(() => {
189
+ const clearButtonAfter = container.querySelector("[data-test-id='clear-all-items'");
190
+
191
+ expect(clearButtonAfter).toBeInTheDocument();
192
+
193
+ fireEvent.click(clearButtonAfter!);
194
+ });
195
+
196
+ await waitFor(() => {
197
+ expect(container.querySelectorAll("[data-tag-index]").length).toBe(0);
198
+ });
87
199
  });
88
200
 
89
- fireEvent.change(input, { target: { value: "" } });
201
+ it("should render disable field with deferred selected items", async () => {
202
+ const { container, rerender } = render(
203
+ <MultiSuggestField {...predefinedNotControlledValues.args} selectedItems={[]} disabled />
204
+ );
205
+
206
+ const [inputTargetContainer] = container.getElementsByClassName("eccgui-multiselect__target");
90
207
 
91
- await waitFor(() => {
92
- const listbox = screen.getByRole("listbox");
93
- const menuItems = listbox.getElementsByClassName("eccgui-menu__item");
94
- expect(menuItems.length).toBe(dropdownOnFocus.args.items.length);
208
+ expect(inputTargetContainer.getAttribute("aria-disabled")).toBe("true");
209
+
210
+ const [firstSelected, secondSelected]: Array<string> = predefinedNotControlledValues.args.selectedItems.map(
211
+ ({ testLabel }) => testLabel
212
+ );
213
+
214
+ await waitFor(() => {
215
+ expect(screen.queryByText(firstSelected)).toBeNull();
216
+ expect(screen.queryByText(secondSelected)).toBeNull();
217
+ });
218
+
219
+ rerender(<MultiSuggestField {...predefinedNotControlledValues.args} disabled />);
220
+
221
+ const [updatedInputTargetContainer] = container.getElementsByClassName("eccgui-multiselect__target");
222
+
223
+ expect(updatedInputTargetContainer.getAttribute("aria-disabled")).toBe("true");
224
+
225
+ await waitFor(() => {
226
+ expect(screen.getByText(firstSelected)).toBeInTheDocument();
227
+ expect(screen.getByText(secondSelected)).toBeInTheDocument();
228
+ });
229
+ });
230
+
231
+ it("should call onSelection function with the selected items", async () => {
232
+ const onSelection = jest.fn();
233
+
234
+ const { container } = render(
235
+ <MultiSuggestField {...dropdownOnFocus.args} items={items} onSelection={onSelection} />
236
+ );
237
+
238
+ const [inputContainer] = container.getElementsByClassName("eccgui-multiselect");
239
+ const [input] = inputContainer.getElementsByTagName("input");
240
+
241
+ fireEvent.click(input);
242
+
243
+ await waitFor(() => {
244
+ const listbox = screen.getByRole("listbox");
245
+ expect(listbox).toBeInTheDocument();
246
+
247
+ const menuItems = listbox.getElementsByClassName("eccgui-menu__item");
248
+ expect(menuItems.length).toBe(dropdownOnFocus.args.items.length);
249
+
250
+ const item = menuItems[0];
251
+ fireEvent.click(item);
252
+ });
253
+
254
+ await waitFor(() => {
255
+ const expectedObject = {
256
+ createdItems: [],
257
+ newlySelected: items[0],
258
+ selectedItems: [items[0]],
259
+ };
260
+ expect(onSelection).toHaveBeenCalledWith(expectedObject);
261
+ });
95
262
  });
96
263
  });
97
264
 
98
- it("should render disable field with selected items", async () => {
99
- const { container } = render(<MultiSuggestField {...predefinedValues.args} disabled />);
265
+ describe("controlled", () => {
266
+ it("should render default selected items", async () => {
267
+ const onSelection = jest.fn();
268
+
269
+ const { getByText } = render(
270
+ <MultiSuggestField {...predefinedNotControlledValues.args} onSelection={onSelection} />
271
+ );
272
+
273
+ const [firstSelected, secondSelected]: Array<string> = predefinedNotControlledValues.args.selectedItems.map(
274
+ ({ testLabel }) => testLabel
275
+ );
276
+
277
+ await waitFor(() => {
278
+ expect(getByText(firstSelected)).toBeInTheDocument();
279
+ expect(getByText(secondSelected)).toBeInTheDocument();
280
+ });
281
+ });
282
+
283
+ it("should call onSelection function with the selected items", async () => {
284
+ const onSelection = jest.fn((values) => {
285
+ // eslint-disable-next-line no-console
286
+ console.log("Mocked onSelection function values: ", values);
287
+ });
288
+
289
+ const { container } = render(
290
+ <MultiSuggestField {...dropdownOnFocus.args} items={items} onSelection={onSelection} />
291
+ );
292
+
293
+ const [inputContainer] = container.getElementsByClassName("eccgui-multiselect");
294
+ const [input] = inputContainer.getElementsByTagName("input");
295
+
296
+ fireEvent.click(input);
297
+
298
+ await waitFor(() => {
299
+ const listbox = screen.getByRole("listbox");
300
+ expect(listbox).toBeInTheDocument();
301
+
302
+ const menuItems = listbox.getElementsByClassName("eccgui-menu__item");
303
+ expect(menuItems.length).toBe(dropdownOnFocus.args.items.length);
304
+
305
+ const item = menuItems[0];
306
+ fireEvent.click(item);
307
+ });
308
+
309
+ await waitFor(() => {
310
+ const expectedObject = {
311
+ createdItems: [],
312
+ newlySelected: items[0],
313
+ selectedItems: [items[0]],
314
+ };
315
+
316
+ expect(onSelection).toHaveBeenCalledWith(expectedObject);
317
+ });
318
+ });
319
+
320
+ it("should set deferred selection correctly and reset values", async () => {
321
+ const onSelection = jest.fn((values) => {
322
+ // eslint-disable-next-line no-console
323
+ console.log("Mocked onSelection function values: ", values);
324
+ });
325
+
326
+ const items = predefinedNotControlledValues.args.items;
100
327
 
101
- const [inputTargetContainer] = container.getElementsByClassName("eccgui-multiselect__target");
328
+ const args = { ...predefinedNotControlledValues.args, selectedItems: [], onSelection: onSelection };
102
329
 
103
- expect(inputTargetContainer.getAttribute("aria-disabled")).toBe("true");
330
+ const { container, rerender } = render(<MultiSuggestField {...args} data-test-id="multi-suggest-field" />);
331
+
332
+ const [inputContainer] = container.getElementsByClassName("eccgui-multiselect");
333
+ const [input] = inputContainer.getElementsByTagName("input");
334
+
335
+ const clearButtonBefore = container.querySelector("[data-test-id='clear-all-items'");
336
+
337
+ expect(clearButtonBefore).not.toBeInTheDocument();
338
+
339
+ fireEvent.click(input);
340
+
341
+ await waitFor(() => {
342
+ const listbox = screen.getByRole("listbox");
343
+ expect(listbox).toBeInTheDocument();
344
+
345
+ const menuItems = listbox.getElementsByClassName("eccgui-menu__item");
346
+ expect(menuItems.length).toBe(predefinedNotControlledValues.args.items.length);
347
+
348
+ const item = menuItems[0];
349
+ fireEvent.click(item);
350
+ });
351
+
352
+ await waitFor(() => {
353
+ const expectedObject = {
354
+ createdItems: [],
355
+ newlySelected: items[0],
356
+ selectedItems: [items[0]],
357
+ };
358
+ expect(onSelection).toHaveBeenCalledTimes(1);
359
+ expect(onSelection).toHaveBeenCalledWith(expectedObject);
360
+ });
361
+
362
+ const selectedItems = items.slice(2);
363
+
364
+ rerender(<MultiSuggestField {...args} selectedItems={selectedItems} data-test-id="multi-suggest-field" />);
365
+
366
+ await waitFor(() => {
367
+ const listbox = screen.getByRole("listbox");
368
+ expect(listbox).toBeInTheDocument();
369
+
370
+ const menuItems = listbox.getElementsByClassName("eccgui-menu__item");
371
+ expect(menuItems.length).toBe(predefinedNotControlledValues.args.items.length);
372
+
373
+ const item = menuItems[0];
374
+ fireEvent.click(item);
375
+ });
376
+
377
+ await waitFor(() => {
378
+ const expectedObject = {
379
+ createdItems: [],
380
+ newlySelected: items[0],
381
+ selectedItems: [...selectedItems, items[0]],
382
+ };
383
+
384
+ expect(onSelection).toHaveBeenCalledTimes(2);
385
+ expect(onSelection).toHaveBeenCalledWith(expectedObject);
386
+ });
387
+
388
+ await waitFor(() => {
389
+ const clearButtonAfter = container.querySelector("[data-test-id='clear-all-items'");
390
+
391
+ expect(clearButtonAfter).toBeInTheDocument();
392
+
393
+ fireEvent.click(clearButtonAfter!);
394
+ });
395
+
396
+ await waitFor(() => {
397
+ const expectedObject = {
398
+ createdItems: [],
399
+ selectedItems: [],
400
+ };
401
+
402
+ expect(onSelection).toHaveBeenCalledTimes(3);
403
+ expect(onSelection).toHaveBeenCalledWith(expectedObject);
404
+ });
405
+ });
104
406
  });
105
407
  });
@@ -0,0 +1,119 @@
1
+ import React, { CSSProperties } from "react";
2
+
3
+ import { utils } from "../../common/";
4
+ import { CLASSPREFIX as eccgui } from "../../configuration/constants";
5
+
6
+ export interface StickyTargetProps extends React.HTMLAttributes<HTMLDivElement> {
7
+ /**
8
+ * Set the side the element need to be sticky on.
9
+ */
10
+ to?: "top" | "bottom";
11
+ /**
12
+ * The sticky area is positioned relatively to a local scroll area.
13
+ * The application header is not taken into offset calculation
14
+ */
15
+ local?: boolean;
16
+ /**
17
+ * Set the background color used for the sticky area.
18
+ * As it can overlay other content readability could be harmed if the overlayed content is shining through.
19
+ */
20
+ background?: "card" | "application" | "transparent";
21
+ /**
22
+ * Set additional distance to original sticky position.
23
+ */
24
+ offset?: `${number}${string}`;
25
+ /**
26
+ * Callback that returns an DOM element.
27
+ * The position of `StickyTarget` is then calculated relative to that element.
28
+ */
29
+ getConnectedElement?: (ref: React.MutableRefObject<HTMLDivElement | null>) => Element | false;
30
+ }
31
+
32
+ /**
33
+ * Element wraps the content that need to be displayed sticky.
34
+ * The content then offset relative to its nearest scrolling ancestor and containing block (nearest block-level ancestor).
35
+ */
36
+ export const StickyTarget = ({
37
+ className,
38
+ to = "top",
39
+ local = false,
40
+ background = "transparent",
41
+ offset,
42
+ style,
43
+ getConnectedElement,
44
+ ...otherDivProps
45
+ }: StickyTargetProps) => {
46
+ const stickyTargetRef = React.useRef<HTMLDivElement | null>(null);
47
+
48
+ let offsetStyle = {};
49
+ if (typeof offset !== "undefined") {
50
+ offsetStyle = { ...style, "--eccgui-sticky-target-localoffset": offset } as CSSProperties;
51
+ }
52
+
53
+ let connectedOffset = 0;
54
+ React.useEffect(() => {
55
+ /**
56
+ * If the target should be sticky to a defined element then:
57
+ * * check for the element and its scroll parent
58
+ * * listen to scroll events and use the elements position as offset
59
+ */
60
+ if (getConnectedElement && stickyTargetRef) {
61
+ const stickyConnection = getConnectedElement(stickyTargetRef);
62
+ if (stickyConnection) {
63
+ const scrollParent = utils.getScrollParent(stickyConnection);
64
+ const scrollParentFallback = !scrollParent ? document.documentElement : false;
65
+ if (scrollParent || scrollParentFallback) {
66
+ const updateTargetOffset = () => {
67
+ const scrollParentPosition = (
68
+ (scrollParent || scrollParentFallback) as HTMLElement
69
+ ).getBoundingClientRect();
70
+ const stickyConnectionPosition = stickyConnection.getBoundingClientRect();
71
+ if (to === "top") {
72
+ connectedOffset =
73
+ stickyConnectionPosition.top -
74
+ Math.max(0, scrollParentPosition.top) +
75
+ stickyConnectionPosition.height;
76
+ }
77
+ if (to === "bottom") {
78
+ connectedOffset =
79
+ Math.max(scrollParentPosition.height, scrollParentPosition.bottom) -
80
+ stickyConnectionPosition.bottom +
81
+ stickyConnectionPosition.height;
82
+ }
83
+ stickyTargetRef.current?.style.setProperty(
84
+ "--eccgui-sticky-target-applicationoffset",
85
+ `${connectedOffset}px`
86
+ );
87
+ };
88
+ updateTargetOffset();
89
+ const eventListeningTarget = scrollParent || window;
90
+ const eventListeningMethod = (_event: Event) => {
91
+ updateTargetOffset();
92
+ };
93
+ eventListeningTarget.addEventListener("scroll", eventListeningMethod);
94
+ return () => {
95
+ eventListeningTarget.removeEventListener("scroll", eventListeningMethod);
96
+ };
97
+ }
98
+ }
99
+ }
100
+ return;
101
+ }, [getConnectedElement, stickyTargetRef, to]);
102
+
103
+ return (
104
+ <div
105
+ ref={stickyTargetRef}
106
+ className={
107
+ `${eccgui}-sticky__target` +
108
+ (to ? ` ${eccgui}-sticky__target--${to}` : "") +
109
+ (local ? ` ${eccgui}-sticky__target--localscrollarea` : "") +
110
+ (background ? ` ${eccgui}-sticky__target--bg-${background}` : "") +
111
+ (className ? ` ${className}` : "")
112
+ }
113
+ style={offset ? offsetStyle : style}
114
+ {...otherDivProps}
115
+ />
116
+ );
117
+ };
118
+
119
+ export default StickyTarget;
@@ -0,0 +1 @@
1
+ export * from "./StickyTarget";
@@ -0,0 +1,69 @@
1
+ @use "sass:color";
2
+
3
+ // Sticky target area
4
+
5
+ .#{$eccgui}-sticky__target {
6
+ position: sticky;
7
+ z-index: 1;
8
+
9
+ --eccgui-sticky-target-applicationoffset: 0px;
10
+
11
+ .#{$eccgui}-application__content &:not(.#{$eccgui}-sticky__target--localscrollarea) {
12
+ --eccgui-sticky-target-applicationoffset: $eccgui-size-block-whitespace;
13
+ }
14
+
15
+ .#{$eccgui}-application__header
16
+ + .#{$eccgui}-application__content
17
+ &:not(.#{$eccgui}-sticky__target--localscrollarea) {
18
+ --eccgui-sticky-target-applicationoffset: calc(#{mini-units(8)} + #{$eccgui-size-block-whitespace});
19
+ }
20
+ }
21
+
22
+ .#{$eccgui}-sticky__target--top {
23
+ top: calc(var(--eccgui-sticky-target-applicationoffset) + var(--eccgui-sticky-target-localoffset, 0px));
24
+ }
25
+
26
+ .#{$eccgui}-sticky__target--bottom {
27
+ --eccgui-sticky-target-applicationoffset: 0px;
28
+
29
+ bottom: calc(var(--eccgui-sticky-target-applicationoffset) + var(--eccgui-sticky-target-localoffset, 0px));
30
+ }
31
+
32
+ .#{$eccgui}-sticky__target--bg-card {
33
+ background-color: $card-background-color;
34
+
35
+ .#{$eccgui}-card.#{$eccgui}-intent--primary & {
36
+ background-color: color.mix($eccgui-color-primary, $card-background-color, 5%);
37
+ }
38
+ .#{$eccgui}-card.#{$eccgui}-intent--accent & {
39
+ background-color: color.mix($eccgui-color-accent, $card-background-color, 10%);
40
+ }
41
+ .#{$eccgui}-card.#{$eccgui}-intent--success & {
42
+ background-color: $eccgui-color-success-background;
43
+ }
44
+ .#{$eccgui}-card.#{$eccgui}-intent--info & {
45
+ background-color: $eccgui-color-info-background;
46
+ }
47
+ .#{$eccgui}-card.#{$eccgui}-intent--warning & {
48
+ background-color: $eccgui-color-warning-background;
49
+ }
50
+ .#{$eccgui}-card.#{$eccgui}-intent--danger & {
51
+ background-color: $eccgui-color-danger-background;
52
+ }
53
+
54
+ .#{$eccgui}-card.#{$ns}-interactive:hover & {
55
+ background-color: $button-background-color-hover;
56
+ }
57
+
58
+ .#{$eccgui}-card.#{$ns}-selected & {
59
+ background-color: $card-selected-background-color;
60
+ }
61
+
62
+ .#{$eccgui}-card--elevated & {
63
+ background-color: $button-background-color-active;
64
+ }
65
+ }
66
+
67
+ .#{$eccgui}-sticky__target--bg-application {
68
+ background-color: $eccgui-color-application-background;
69
+ }