@eccenca/gui-elements 25.1.0-rc.2 → 25.1.0-rc.4

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 (41) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/cjs/components/Button/Button.js +1 -1
  3. package/dist/cjs/components/Button/Button.js.map +1 -1
  4. package/dist/cjs/components/MultiSelect/MultiSelect.js +24 -6
  5. package/dist/cjs/components/MultiSelect/MultiSelect.js.map +1 -1
  6. package/dist/cjs/components/Tooltip/Tooltip.js +11 -7
  7. package/dist/cjs/components/Tooltip/Tooltip.js.map +1 -1
  8. package/dist/esm/components/Button/Button.js +1 -1
  9. package/dist/esm/components/Button/Button.js.map +1 -1
  10. package/dist/esm/components/MultiSelect/MultiSelect.js +25 -7
  11. package/dist/esm/components/MultiSelect/MultiSelect.js.map +1 -1
  12. package/dist/esm/components/Tooltip/Tooltip.js +11 -7
  13. package/dist/esm/components/Tooltip/Tooltip.js.map +1 -1
  14. package/dist/types/components/MultiSelect/MultiSelect.d.ts +1 -0
  15. package/package.json +1 -1
  16. package/src/_shame.scss +1 -35
  17. package/src/cmem/markdown/Markdown.stories.tsx +2 -2
  18. package/src/common/scss/_accessibility-defaults.scss +101 -0
  19. package/src/components/Application/_header.scss +21 -9
  20. package/src/components/Application/_sidebar.scss +6 -0
  21. package/src/components/Application/_toolbar.scss +3 -3
  22. package/src/components/AutoSuggestion/AutoSuggestion.scss +3 -1
  23. package/src/components/Badge/Badge.test.tsx +22 -0
  24. package/src/components/Button/Button.test.tsx +16 -2
  25. package/src/components/Button/Button.tsx +1 -1
  26. package/src/components/Checkbox/checkbox.scss +9 -1
  27. package/src/components/Dialog/dialog.scss +10 -2
  28. package/src/components/Link/link.scss +5 -6
  29. package/src/components/MultiSelect/MultiSelect.tsx +37 -9
  30. package/src/components/MultiSuggestField/MultiSuggestField.stories.tsx +40 -1
  31. package/src/components/MultiSuggestField/_multisuggestfield.scss +18 -0
  32. package/src/components/MultiSuggestField/tests/MultiSuggestField.test.tsx +88 -3
  33. package/src/components/RadioButton/radiobutton.scss +5 -1
  34. package/src/components/Tag/tag.scss +2 -2
  35. package/src/components/TextField/textfield.scss +20 -0
  36. package/src/components/Tooltip/Tooltip.test.tsx +40 -5
  37. package/src/components/Tooltip/Tooltip.tsx +14 -10
  38. package/src/components/Typography/typography.scss +10 -4
  39. package/src/configuration/stories/customproperties.stories.tsx +4 -0
  40. package/src/extensions/codemirror/_codemirror.scss +18 -28
  41. package/src/index.scss +1 -0
@@ -0,0 +1,101 @@
1
+ .#{$prefix}--assistive-text,
2
+ .#{$prefix}--visually-hidden {
3
+ /*
4
+ originally from ~@carbon/styles/scss/css--helpers
5
+ but we cannot use it directly because of other included rules there.
6
+ */
7
+ position: absolute;
8
+ visibility: inherit;
9
+ width: 1px;
10
+ height: 1px;
11
+ padding: 0;
12
+ margin: -1px;
13
+ overflow: hidden;
14
+ white-space: nowrap;
15
+ border: 0;
16
+ clip-path: rect(0, 0, 0, 0);
17
+ }
18
+
19
+ /*
20
+ default focus indicator
21
+ */
22
+
23
+ :root {
24
+ --#{$eccgui}-a11y-outline-color: #{eccgui-color-var("layout", "magenta", "900")};
25
+ --#{$eccgui}-a11y-outline-style: solid;
26
+ --#{$eccgui}-a11y-outline-width: #{0.25 * $eccgui-size-block-whitespace};
27
+
28
+ // shift outline min 2px inside element to have a minimum 2px outline even with hidden overflow
29
+ --#{$eccgui}-a11y-outline-offset: min(calc(var(--#{$eccgui}-a11y-outline-width) * -0.5), -2px);
30
+ }
31
+
32
+ @keyframes outline-bounce {
33
+ 0% {
34
+ outline-width: calc(var(--#{$eccgui}-a11y-outline-width) * 0.5);
35
+ outline-color: var(--#{$eccgui}-a11y-outline-color);
36
+ outline-offset: var(--#{$eccgui}-a11y-outline-offset);
37
+ }
38
+
39
+ 33.3% {
40
+ outline-color: var(--#{$eccgui}-a11y-outline-color);
41
+ outline-offset: calc(var(--#{$eccgui}-a11y-outline-width));
42
+ }
43
+
44
+ 66.6% {
45
+ outline-color: currentcolor;
46
+ outline-offset: calc(var(--#{$eccgui}-a11y-outline-width) * -2);
47
+ }
48
+
49
+ 100% {
50
+ outline-width: var(--#{$eccgui}-a11y-outline-width);
51
+ outline-color: var(--#{$eccgui}-a11y-outline-color);
52
+ outline-offset: var(--#{$eccgui}-a11y-outline-offset);
53
+ }
54
+ }
55
+
56
+ @mixin focus-by-keyboard-static {
57
+ // strong visual focus indication for keyboard devices
58
+
59
+ outline: var(--#{$eccgui}-a11y-outline-color) var(--#{$eccgui}-a11y-outline-style)
60
+ var(--#{$eccgui}-a11y-outline-width);
61
+ outline-offset: var(--#{$eccgui}-a11y-outline-offset);
62
+ }
63
+
64
+ @mixin focus-by-keyboard-animation {
65
+ @media (prefers-reduced-motion: no-preference) {
66
+ animation: outline-bounce 0.5s;
67
+ }
68
+ }
69
+
70
+ .#{$eccgui}-a11y-focus-by-keyboard-animated {
71
+ @include focus-by-keyboard-static;
72
+ @include focus-by-keyboard-animation;
73
+ }
74
+
75
+ .#{$eccgui}-a11y-focus-by-keyboard-static {
76
+ @include focus-by-keyboard-static;
77
+
78
+ animation: none;
79
+ }
80
+
81
+ @mixin focus-by-pointer {
82
+ // limited visual focus indication for pointer devices
83
+
84
+ outline: transparent none 0;
85
+ outline-offset: 0;
86
+ }
87
+
88
+ .#{$eccgui}-a11y-focus-by-pointer {
89
+ @include focus-by-pointer;
90
+ }
91
+
92
+ *[tabindex]:not([tabindex^="-"]):focus-visible,
93
+ :focus-visible {
94
+ @extend .#{$eccgui}-a11y-focus-by-keyboard-animated;
95
+ }
96
+
97
+ input:focus:not(:focus-visible),
98
+ textarea:focus:not(:focus-visible),
99
+ :focus:not(:focus-visible) {
100
+ @extend .#{$eccgui}-a11y-focus-by-pointer;
101
+ }
@@ -190,15 +190,27 @@ a.#{$prefix}--header__menu-item:active {
190
190
  }
191
191
 
192
192
  // $shell-header-focus
193
- .#{$prefix}--header__action:focus,
194
- .#{$prefix}--header__action.#{$prefix}--btn--icon-only:focus,
195
- .#{$prefix}--header__action.#{$prefix}--btn--primary:focus,
196
- a.#{$prefix}--header__name:focus,
197
- a.#{$prefix}--header__menu-item:focus {
198
- outline: 1px dotted $shell-header-focus;
199
- outline-offset: -1px;
200
- border: none;
201
- box-shadow: none;
193
+ .#{$prefix}--header__action,
194
+ .#{$prefix}--header__action.#{$prefix}--btn--icon-only,
195
+ .#{$prefix}--header__action.#{$prefix}--btn--primary,
196
+ a.#{$prefix}--header__name,
197
+ a.#{$prefix}--header__menu-item {
198
+ &:focus {
199
+ @extend .#{$eccgui}-a11y-focus-by-pointer;
200
+
201
+ border: none;
202
+ box-shadow: none;
203
+ }
204
+
205
+ &,
206
+ .#{$prefix}--popover--open & {
207
+ &:focus-visible {
208
+ @extend .#{$eccgui}-a11y-focus-by-keyboard-animated;
209
+
210
+ border: none;
211
+ box-shadow: none;
212
+ }
213
+ }
202
214
  }
203
215
  .#{$prefix}--header__menu-title[aria-expanded="true"] {
204
216
  color: $shell-header-focus;
@@ -28,3 +28,9 @@ $ui-02: $eccgui-color-workspace-background !default;
28
28
  padding: $eccgui-size-block-whitespace calc(0.5 * (#{mini-units(8)} - 30px));
29
29
  transition: none;
30
30
  }
31
+
32
+ .#{$eccgui}-application__menu__toggler.cds--header__action {
33
+ &:focus-visible {
34
+ @extend .#{$eccgui}-a11y-focus-by-keyboard-animated;
35
+ }
36
+ }
@@ -51,18 +51,18 @@ $shell-panel-focus: $shell-header-focus !default;
51
51
  .#{$eccgui}-application__toolbar__panel-backdrop--onleave,
52
52
  .#{$eccgui}-application__toolbar__panel-backdrop--onoutsideclick {
53
53
  position: fixed;
54
- inset: mini-units(8) 0 0 0;
54
+ inset: mini-units(8) 0 0;
55
55
  }
56
56
 
57
57
  .#{$eccgui}-application__toolbar__panel-backdrop--onoutsideclick {
58
58
  top: 0;
59
59
  }
60
60
 
61
- // add rules for own class identiiers
61
+ // add rules for own class identifiers
62
62
 
63
63
  .#{$eccgui}-application__toolbar {
64
- flex-basis: auto;
65
64
  flex-grow: 0;
65
+ flex-basis: auto;
66
66
 
67
67
  .#{$prefix}--popover--bottom-right .#{$prefix}--popover-content {
68
68
  // for some reason the original calculation still moves out the tooltip
@@ -39,7 +39,9 @@
39
39
  }
40
40
 
41
41
  &:has(.cm-editor.cm-focused) {
42
- box-shadow: input-transition-shadow($input-shadow-color-focus, true), $input-box-shadow-focus;
42
+ --#{$eccgui}-a11y-outline-color: #{$eccgui-color-accent};
43
+
44
+ @extend .#{$eccgui}-a11y-focus-by-keyboard-static;
43
45
  }
44
46
 
45
47
  .cm-editor {
@@ -0,0 +1,22 @@
1
+ import React from "react";
2
+ import { render } from "@testing-library/react";
3
+
4
+ import "@testing-library/jest-dom";
5
+
6
+ import { Badge } from "../../../index";
7
+ import { CLASSPREFIX as eccgui } from "../../configuration/constants";
8
+
9
+ describe("Badge", () => {
10
+ it("should shorten a number badge exceeding maxLength to a 9+ notation", () => {
11
+ const { container } = render(<Badge maxLength={2}>{42}</Badge>);
12
+ const badge = container.querySelector(`.${eccgui}-badge__tag`);
13
+ expect(badge).not.toBeNull();
14
+ expect(badge).toHaveTextContent("9+");
15
+ });
16
+ it("should apply maxWidth style to a string badge when maxLength is set", () => {
17
+ const { container } = render(<Badge maxLength={4}>forty two</Badge>);
18
+ const tag = container.querySelector(`.${eccgui}-badge__tag`);
19
+ expect(tag).not.toBeNull();
20
+ expect((tag as HTMLElement).style.maxWidth).toBe("calc((3em + 3ch)/2)");
21
+ });
22
+ });
@@ -3,6 +3,7 @@ import { render, screen } from "@testing-library/react";
3
3
 
4
4
  import "@testing-library/jest-dom";
5
5
 
6
+ import { CLASSPREFIX as eccgui } from "../../configuration/constants";
6
7
  import Icon from "../Icon/Icon";
7
8
 
8
9
  import Button from "./Button";
@@ -21,7 +22,7 @@ describe("Button", () => {
21
22
  </Button>
22
23
  );
23
24
  expect(screen.getByRole("button").lastChild).toEqual(screen.getByText(/left icon/i));
24
- expect(container.getElementsByClassName("eccgui-icon").length).toBe(1);
25
+ expect(container.getElementsByClassName(`${eccgui}-icon`).length).toBe(1);
25
26
  });
26
27
 
27
28
  it("should have icon at the right after the text", () => {
@@ -31,6 +32,19 @@ describe("Button", () => {
31
32
  </Button>
32
33
  );
33
34
  expect(screen.getByRole("button").firstChild).toEqual(screen.getByText(/right icon/i));
34
- expect(container.getElementsByClassName("eccgui-icon").length).toBe(1);
35
+ expect(container.getElementsByClassName(`${eccgui}-icon`).length).toBe(1);
36
+ });
37
+
38
+ it("should render badge markup with correct content when used on an icon button", () => {
39
+ const { container } = render(<Button name="item-info" badge={"badge content"} text={"Cation label"} />);
40
+ const badge = container.querySelector(`.${eccgui}-badge`);
41
+ expect(badge).not.toBeNull();
42
+ expect(badge).toHaveTextContent("badge content");
43
+ });
44
+ it("should render badge markup with correct content when batch displays a 0 (zero) number on an icon button", () => {
45
+ const { container } = render(<Button name="item-info" badge={0} text={"Cation label"} />);
46
+ const badge = container.querySelector(`.${eccgui}-badge`);
47
+ expect(badge).not.toBeNull();
48
+ expect(badge).toHaveTextContent("0");
35
49
  });
36
50
  });
@@ -113,7 +113,7 @@ export const Button = ({
113
113
  rightIcon={typeof rightIcon === "string" ? <Icon name={rightIcon} /> : rightIcon}
114
114
  >
115
115
  {children}
116
- {badge && (
116
+ {typeof badge !== "undefined" && (
117
117
  <Badge
118
118
  children={badge}
119
119
  {...constructBadgeProperties({
@@ -47,7 +47,7 @@ $switch-checked-background-color-disabled: eccgui-color-rgba(
47
47
  }
48
48
 
49
49
  input:focus ~ .#{$ns}-control-indicator {
50
- outline-offset: 0;
50
+ @extend .#{$eccgui}-a11y-focus-by-keyboard-static;
51
51
  }
52
52
 
53
53
  input:disabled ~ .#{$ns}-control-indicator,
@@ -72,6 +72,14 @@ $switch-checked-background-color-disabled: eccgui-color-rgba(
72
72
  background-image: url("~@carbon/icons/svg/32/checkbox--indeterminate.svg");
73
73
  }
74
74
  }
75
+
76
+ &.#{$ns}-switch {
77
+ --#{$eccgui}-a11y-outline-offset: 1px;
78
+
79
+ input:focus ~ .#{$ns}-control-indicator {
80
+ @extend .#{$eccgui}-a11y-focus-by-keyboard-static;
81
+ }
82
+ }
75
83
  }
76
84
 
77
85
  .#{$ns}-control-indicator + *,
@@ -22,6 +22,14 @@ $eccgui-color-modal-backdrop: eccgui-color-rgba(
22
22
  z-index: $eccgui-zindex-modals;
23
23
  }
24
24
 
25
+ .#{$ns}-overlay-start-focus-trap,
26
+ .#{$ns}-overlay-end-focus-trap {
27
+ // do not show focus on trap elements, we need to fix keeping focus on modal elements differently later
28
+ &[tabindex]:not([tabindex^="-"]):focus-visible {
29
+ @extend .#{$eccgui}-a11y-focus-by-pointer;
30
+ }
31
+ }
32
+
25
33
  .#{$eccgui}-dialog__backdrop {
26
34
  background-color: $eccgui-color-modal-backdrop;
27
35
  }
@@ -33,7 +41,7 @@ $eccgui-color-modal-backdrop: eccgui-color-rgba(
33
41
  max-width: calc(100vw - 4rem);
34
42
  max-height: calc(100vh - 4rem);
35
43
  margin: 2rem;
36
- pointer-events: all;
44
+ pointer-events: auto;
37
45
  user-select: text;
38
46
 
39
47
  & > * {
@@ -42,7 +50,7 @@ $eccgui-color-modal-backdrop: eccgui-color-rgba(
42
50
  max-width: 100%;
43
51
  }
44
52
 
45
- * {
53
+ *:has(> svg) {
46
54
  pointer-events: all;
47
55
  }
48
56
 
@@ -31,13 +31,12 @@
31
31
  }
32
32
  }
33
33
 
34
- &:focus {
35
- outline: none;
36
- outline-offset: 0;
34
+ &:focus:not(.#{$prefix}--link--disabled) {
35
+ @include focus-by-pointer;
37
36
  }
38
37
 
39
- &:focus-visible {
40
- outline: eccgui-color-rgba($eccgui-color-accent, $eccgui-opacity-muted) solid 2px;
41
- outline-offset: 1px;
38
+ &:focus-visible:not(.#{$prefix}--link--disabled) {
39
+ @include focus-by-keyboard-static;
40
+ @include focus-by-keyboard-animation;
42
41
  }
43
42
  }
@@ -17,11 +17,12 @@ import {
17
17
  IconButton,
18
18
  MenuItem,
19
19
  OverflowText,
20
- Spinner
20
+ Spinner,
21
21
  } from "./../../index";
22
22
 
23
23
  export interface MultiSuggestFieldSelectionProps<T> {
24
24
  newlySelected?: T;
25
+ newlyRemoved?: T;
25
26
  selectedItems: T[];
26
27
  createdItems: Partial<T>[];
27
28
  }
@@ -178,6 +179,8 @@ export function MultiSuggestField<T>({
178
179
  intent,
179
180
  ...otherMultiSelectProps
180
181
  }: MultiSuggestFieldProps<T>) {
182
+ type SelectionChange = { type: "selected"; item: T } | { type: "removed"; item: T } | { type: "none" };
183
+
181
184
  // Options created by a user
182
185
  const createdItems = useRef<T[]>([]);
183
186
  // Options passed ouside (f.e. from the backend)
@@ -199,6 +202,7 @@ export function MultiSuggestField<T>({
199
202
  query?: string;
200
203
  timeoutId?: number;
201
204
  }>({});
205
+ const selectionChange = useRef<SelectionChange>({ type: "none" });
202
206
 
203
207
  /** Update external items when they change
204
208
  * e.g for auto-complete when query change
@@ -209,11 +213,21 @@ export function MultiSuggestField<T>({
209
213
  }, [items.map((item) => itemId(item)).join("|")]);
210
214
 
211
215
  React.useEffect(() => {
212
- onSelection?.({
213
- newlySelected: selectedItems.slice(-1)[0],
216
+ const selectionParams: MultiSuggestFieldSelectionProps<T> = {
214
217
  createdItems: createdItems.current,
215
218
  selectedItems,
216
- });
219
+ };
220
+
221
+ if (selectionChange.current.type === "selected") {
222
+ selectionParams.newlySelected = selectionChange.current.item;
223
+ }
224
+
225
+ if (selectionChange.current.type === "removed") {
226
+ selectionParams.newlyRemoved = selectionChange.current.item;
227
+ }
228
+
229
+ onSelection?.(selectionParams);
230
+ selectionChange.current = { type: "none" };
217
231
  }, [
218
232
  onSelection,
219
233
  selectedItems.map((item) => itemId(item)).join("|"),
@@ -228,6 +242,7 @@ export function MultiSuggestField<T>({
228
242
  return;
229
243
  }
230
244
 
245
+ selectionChange.current = { type: "none" };
231
246
  setSelectedItems(externalSelectedItems);
232
247
  }, [externalSelectedItems?.map((item) => itemId(item)).join("|")]);
233
248
 
@@ -268,13 +283,18 @@ export function MultiSuggestField<T>({
268
283
  * @param matcher
269
284
  */
270
285
  const removeItemSelection = (matcher: string) => {
271
- const filteredItems = selectedItems.filter((item) => itemId(item) !== matcher);
272
- setSelectedItems(filteredItems);
286
+ setSelectedItems((items) => {
287
+ const removedItem = items.find((item) => itemId(item) === matcher);
288
+
289
+ selectionChange.current = removedItem ? { type: "removed", item: removedItem } : { type: "none" };
290
+
291
+ return items.filter((item) => itemId(item) !== matcher);
292
+ });
273
293
  };
274
294
 
275
295
  const defaultFilterPredicate = (item: T, query: string) => {
276
- const searchWords = highlighterUtils.extractSearchWords(query, true)
277
- return highlighterUtils.matchesAllWords(itemLabel(item).toLowerCase(), searchWords)
296
+ const searchWords = highlighterUtils.extractSearchWords(query, true);
297
+ return highlighterUtils.matchesAllWords(itemLabel(item).toLowerCase(), searchWords);
278
298
  };
279
299
 
280
300
  /**
@@ -286,6 +306,7 @@ export function MultiSuggestField<T>({
286
306
  if (itemHasBeenSelectedAlready(itemId(item))) {
287
307
  removeItemSelection(itemId(item));
288
308
  } else {
309
+ selectionChange.current = { type: "selected", item };
289
310
  setSelectedItems((items) => [...items, item]);
290
311
  }
291
312
 
@@ -365,6 +386,7 @@ export function MultiSuggestField<T>({
365
386
  const handleClear = () => {
366
387
  requestState.current.query = "";
367
388
 
389
+ selectionChange.current = { type: "none" };
368
390
  setSelectedItems([]);
369
391
  setFilteredItems([...externalItems, ...createdItems.current]);
370
392
  };
@@ -375,7 +397,13 @@ export function MultiSuggestField<T>({
375
397
  * @param index
376
398
  */
377
399
  const removeTagFromSelectionViaIndex = (_label: React.ReactNode, index: number) => {
378
- setSelectedItems([...selectedItems.slice(0, index), ...selectedItems.slice(index + 1)]);
400
+ setSelectedItems((items) => {
401
+ const removedItem = items[index];
402
+
403
+ selectionChange.current = removedItem ? { type: "removed", item: removedItem } : { type: "none" };
404
+
405
+ return [...items.slice(0, index), ...items.slice(index + 1)];
406
+ });
379
407
  };
380
408
 
381
409
  /**
@@ -5,6 +5,8 @@ import { Meta, StoryFn } from "@storybook/react";
5
5
  import { fn } from "@storybook/test";
6
6
 
7
7
  import { helpersArgTypes } from "../../../.storybook/helpers";
8
+ import { Notification } from "../Notification/Notification";
9
+ import Spacing from "../Separation/Spacing";
8
10
 
9
11
  import { MultiSuggestField, MultiSuggestFieldSelectionProps, SimpleDialog } from "./../../../index";
10
12
 
@@ -61,7 +63,7 @@ Default.args = {
61
63
 
62
64
  /**
63
65
  * Display always the dropdown after the element was clicked on.
64
- * Do not wait until the query input was startet.
66
+ * Do not wait until the query input was started.
65
67
  */
66
68
  export const dropdownOnFocus = Template.bind({});
67
69
  dropdownOnFocus.args = {
@@ -259,3 +261,40 @@ CustomSearch.args = {
259
261
  return item.testId.toLowerCase().includes(query) || item.testLabel.toLowerCase().includes(query);
260
262
  },
261
263
  };
264
+
265
+ const SelectionNotificationComponent = (): React.JSX.Element => {
266
+ const [notification, setNotification] = useState<string | null>(null);
267
+
268
+ const availableItems = useMemo<string[]>(() => ["existing item"], []);
269
+
270
+ const identity = useCallback((item: string): string => item, []);
271
+
272
+ const handleOnSelect = useCallback((params: MultiSuggestFieldSelectionProps<string>) => {
273
+ if (params.newlySelected) {
274
+ setNotification(`Element added: ${params.newlySelected}`);
275
+ } else if (params.newlyRemoved) {
276
+ setNotification(`Element removed: ${params.newlyRemoved}`);
277
+ }
278
+ }, []);
279
+
280
+ return (
281
+ <OverlaysProvider>
282
+ {notification && <Notification intent={"info"}>{notification}</Notification>}
283
+ <Spacing size={"medium"} />
284
+ <MultiSuggestField<string>
285
+ items={availableItems}
286
+ prePopulateWithItems={true}
287
+ onSelection={handleOnSelect}
288
+ itemId={identity}
289
+ itemLabel={identity}
290
+ createNewItemFromQuery={identity}
291
+ />
292
+ </OverlaysProvider>
293
+ );
294
+ };
295
+
296
+ /**
297
+ * Demonstrates the `newlySelected` and `newlyRemoved` properties of the `onSelection` callback.
298
+ * A notification appears when an element is added or removed from the selection.
299
+ */
300
+ export const selectionNotification = SelectionNotificationComponent.bind({});
@@ -6,3 +6,21 @@
6
6
  max-height: var(--eccgui-multisuggestfield-max-height, 45vh);
7
7
  }
8
8
  }
9
+
10
+ .#{$eccgui}-multisuggestfield {
11
+ --#{$eccgui}-a11y-outline-color: #{$eccgui-color-accent};
12
+
13
+ &.#{$ns}-intent-success {
14
+ --#{$eccgui}-a11y-outline-color: #{$eccgui-color-success-text};
15
+ }
16
+ &.#{$ns}-intent-warning {
17
+ --#{$eccgui}-a11y-outline-color: #{$eccgui-color-warning-text};
18
+ }
19
+ &.#{$ns}-intent-danger {
20
+ --#{$eccgui}-a11y-outline-color: #{$eccgui-color-danger-text};
21
+ }
22
+
23
+ &:focus-within {
24
+ @extend .#{$eccgui}-a11y-focus-by-keyboard-static;
25
+ }
26
+ }
@@ -261,6 +261,86 @@ describe("MultiSuggestField", () => {
261
261
  });
262
262
  });
263
263
 
264
+ it("should set newlySelected only when an item is added", async () => {
265
+ const onSelection = jest.fn();
266
+ const initiallySelected = predefinedNotControlledValues.args.selectedItems;
267
+
268
+ const { container } = render(
269
+ <MultiSuggestField
270
+ {...dropdownOnFocus.args}
271
+ items={items}
272
+ selectedItems={initiallySelected}
273
+ onSelection={onSelection}
274
+ />
275
+ );
276
+
277
+ await waitFor(() => {
278
+ expect(onSelection).toHaveBeenCalledWith({
279
+ createdItems: [],
280
+ selectedItems: initiallySelected,
281
+ });
282
+ });
283
+
284
+ onSelection.mockClear();
285
+
286
+ const [inputContainer] = container.getElementsByClassName("eccgui-multiselect");
287
+ const [input] = inputContainer.getElementsByTagName("input");
288
+
289
+ fireEvent.click(input);
290
+
291
+ await waitFor(() => {
292
+ const listbox = screen.getByRole("listbox");
293
+ const menuItems = listbox.getElementsByClassName("eccgui-menu__item");
294
+
295
+ expect(menuItems.length).toBe(dropdownOnFocus.args.items.length);
296
+
297
+ fireEvent.click(menuItems[2]);
298
+ });
299
+
300
+ await waitFor(() => {
301
+ expect(onSelection).toHaveBeenLastCalledWith({
302
+ createdItems: [],
303
+ newlySelected: items[2],
304
+ selectedItems: [...initiallySelected, items[2]],
305
+ });
306
+ });
307
+ });
308
+
309
+ it("should set newlyRemoved only when an item is removed", async () => {
310
+ const onSelection = jest.fn();
311
+ const initiallySelected = predefinedNotControlledValues.args.selectedItems;
312
+
313
+ const { container } = render(
314
+ <MultiSuggestField
315
+ {...predefinedNotControlledValues.args}
316
+ selectedItems={initiallySelected}
317
+ onSelection={onSelection}
318
+ />
319
+ );
320
+
321
+ await waitFor(() => {
322
+ expect(onSelection).toHaveBeenCalledWith({
323
+ createdItems: [],
324
+ selectedItems: initiallySelected,
325
+ });
326
+ });
327
+
328
+ onSelection.mockClear();
329
+
330
+ const [firstTag] = Array.from(container.querySelectorAll("span[data-tag-index]"));
331
+ const removeTagButton = firstTag.querySelector("button");
332
+
333
+ fireEvent.click(removeTagButton!);
334
+
335
+ await waitFor(() => {
336
+ expect(onSelection).toHaveBeenLastCalledWith({
337
+ createdItems: [],
338
+ newlyRemoved: initiallySelected[0],
339
+ selectedItems: initiallySelected.slice(1),
340
+ });
341
+ });
342
+ });
343
+
264
344
  it("should filter items by custom search function", async () => {
265
345
  const { container } = render(<MultiSuggestField {...CustomSearch.args} items={items} />);
266
346
 
@@ -340,6 +420,13 @@ describe("MultiSuggestField", () => {
340
420
  expect(getByText(firstSelected)).toBeInTheDocument();
341
421
  expect(getByText(secondSelected)).toBeInTheDocument();
342
422
  });
423
+
424
+ await waitFor(() => {
425
+ expect(onSelection).toHaveBeenCalledWith({
426
+ createdItems: [],
427
+ selectedItems: predefinedNotControlledValues.args.selectedItems,
428
+ });
429
+ });
343
430
  });
344
431
 
345
432
  it("should call onSelection function with the selected items", async () => {
@@ -485,7 +572,6 @@ describe("MultiSuggestField", () => {
485
572
  await waitFor(() => {
486
573
  const expectedObject = {
487
574
  createdItems: [],
488
- newlySelected: items.at(-1),
489
575
  selectedItems: items,
490
576
  };
491
577
  expect(onSelection).toHaveBeenCalledWith(expectedObject);
@@ -513,7 +599,6 @@ describe("MultiSuggestField", () => {
513
599
  await waitFor(() => {
514
600
  const expectedObject = {
515
601
  createdItems: [],
516
- newlySelected: items.at(-1),
517
602
  selectedItems: items,
518
603
  };
519
604
  expect(onSelection).toHaveBeenCalledWith(expectedObject);
@@ -536,7 +621,7 @@ describe("MultiSuggestField", () => {
536
621
 
537
622
  const expectedObject = {
538
623
  createdItems: [],
539
- newlySelected: selected.at(-1),
624
+ newlyRemoved: items[i],
540
625
  selectedItems: selected,
541
626
  };
542
627
 
@@ -1,6 +1,10 @@
1
1
  // Checkbox need to be imported before, we won't double it here currently
2
2
 
3
3
  .#{$ns}-control {
4
+ --#{$eccgui}-a11y-outline-color: #{$eccgui-color-accent};
5
+ --#{$eccgui}-a11y-outline-width: 2px;
6
+ --#{$eccgui}-a11y-outline-offset: 0;
7
+
4
8
  &.#{$ns}-radio {
5
9
  input ~ .#{$ns}-control-indicator,
6
10
  input:checked ~ .#{$ns}-control-indicator {
@@ -8,7 +12,7 @@
8
12
  }
9
13
 
10
14
  input:focus ~ .#{$ns}-control-indicator {
11
- outline-offset: 1px;
15
+ @extend .#{$eccgui}-a11y-focus-by-keyboard-static;
12
16
  }
13
17
 
14
18
  input:disabled ~ .#{$ns}-control-indicator,