@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.
- package/CHANGELOG.md +14 -0
- package/dist/cjs/components/Button/Button.js +1 -1
- package/dist/cjs/components/Button/Button.js.map +1 -1
- package/dist/cjs/components/MultiSelect/MultiSelect.js +24 -6
- package/dist/cjs/components/MultiSelect/MultiSelect.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/esm/components/Button/Button.js +1 -1
- package/dist/esm/components/Button/Button.js.map +1 -1
- package/dist/esm/components/MultiSelect/MultiSelect.js +25 -7
- package/dist/esm/components/MultiSelect/MultiSelect.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/types/components/MultiSelect/MultiSelect.d.ts +1 -0
- package/package.json +1 -1
- package/src/_shame.scss +1 -35
- package/src/cmem/markdown/Markdown.stories.tsx +2 -2
- package/src/common/scss/_accessibility-defaults.scss +101 -0
- 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/Badge/Badge.test.tsx +22 -0
- package/src/components/Button/Button.test.tsx +16 -2
- package/src/components/Button/Button.tsx +1 -1
- package/src/components/Checkbox/checkbox.scss +9 -1
- package/src/components/Dialog/dialog.scss +10 -2
- package/src/components/Link/link.scss +5 -6
- package/src/components/MultiSelect/MultiSelect.tsx +37 -9
- package/src/components/MultiSuggestField/MultiSuggestField.stories.tsx +40 -1
- package/src/components/MultiSuggestField/_multisuggestfield.scss +18 -0
- package/src/components/MultiSuggestField/tests/MultiSuggestField.test.tsx +88 -3
- package/src/components/RadioButton/radiobutton.scss +5 -1
- package/src/components/Tag/tag.scss +2 -2
- package/src/components/TextField/textfield.scss +20 -0
- package/src/components/Tooltip/Tooltip.test.tsx +40 -5
- package/src/components/Tooltip/Tooltip.tsx +14 -10
- package/src/components/Typography/typography.scss +10 -4
- package/src/configuration/stories/customproperties.stories.tsx +4 -0
- package/src/extensions/codemirror/_codemirror.scss +18 -28
- 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
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
36
|
-
outline-offset: 0;
|
|
34
|
+
&:focus:not(.#{$prefix}--link--disabled) {
|
|
35
|
+
@include focus-by-pointer;
|
|
37
36
|
}
|
|
38
37
|
|
|
39
|
-
&:focus-visible {
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
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
|
-
|
|
272
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
15
|
+
@extend .#{$eccgui}-a11y-focus-by-keyboard-static;
|
|
12
16
|
}
|
|
13
17
|
|
|
14
18
|
input:disabled ~ .#{$ns}-control-indicator,
|