@orangesk/orange-design-system 2.0.0-beta.47 → 2.0.0-beta.48

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 (91) hide show
  1. package/build/components/BlockAction/style.css +1 -1
  2. package/build/components/BlockAction/style.css.map +1 -1
  3. package/build/components/Breadcrumbs/style.css +1 -1
  4. package/build/components/Breadcrumbs/style.css.map +1 -1
  5. package/build/components/DocumentationSidebar/style.css +1 -1
  6. package/build/components/DocumentationSidebar/style.css.map +1 -1
  7. package/build/components/Icon/style.css +1 -1
  8. package/build/components/Icon/style.css.map +1 -1
  9. package/build/components/Loader/style.css +1 -1
  10. package/build/components/Loader/style.css.map +1 -1
  11. package/build/components/Megamenu/style.css +1 -1
  12. package/build/components/Megamenu/style.css.map +1 -1
  13. package/build/components/Pagination/style.css +1 -1
  14. package/build/components/Pagination/style.css.map +1 -1
  15. package/build/components/PromotionCard/style.css +1 -1
  16. package/build/components/PromotionCard/style.css.map +1 -1
  17. package/build/components/Section/style.css +1 -1
  18. package/build/components/Section/style.css.map +1 -1
  19. package/build/components/SocialButton/style.css +1 -1
  20. package/build/components/SocialButton/style.css.map +1 -1
  21. package/build/components/Stepbar/style.css +1 -1
  22. package/build/components/Stepbar/style.css.map +1 -1
  23. package/build/components/Table/style.css +1 -1
  24. package/build/components/Table/style.css.map +1 -1
  25. package/build/components/Tag/style.css +1 -1
  26. package/build/components/Tag/style.css.map +1 -1
  27. package/build/components/Tile/style.css +1 -1
  28. package/build/components/Tile/style.css.map +1 -1
  29. package/build/components/index.js +1 -1
  30. package/build/components/index.js.map +1 -1
  31. package/build/components/tsconfig.tsbuildinfo +1 -1
  32. package/build/components/types/index.d.ts +4 -11
  33. package/build/components/types/src/components/Carousel/Carousel.static.d.ts +4 -1
  34. package/build/components/types/src/components/Forms/Autocomplete/Autocomplete.static.d.ts +11 -0
  35. package/build/components/types/src/components/Forms/Group/Group.d.ts +1 -1
  36. package/build/components/types/src/components/Tile/Tile.d.ts +3 -11
  37. package/build/lib/base.css +1 -1
  38. package/build/lib/base.css.map +1 -1
  39. package/build/lib/components.css +1 -1
  40. package/build/lib/components.css.map +1 -1
  41. package/build/lib/megamenu.css +1 -1
  42. package/build/lib/megamenu.css.map +1 -1
  43. package/build/lib/scripts.js +1 -1
  44. package/build/lib/scripts.js.map +1 -1
  45. package/build/lib/style.css +1 -1
  46. package/build/lib/style.css.map +1 -1
  47. package/build/lib/tsconfig.tsbuildinfo +1 -1
  48. package/build/lib/utilities.css +1 -1
  49. package/build/lib/utilities.css.map +1 -1
  50. package/build/search-index.json +4 -4
  51. package/package.json +17 -17
  52. package/src/components/BlockAction/styles/style.scss +6 -4
  53. package/src/components/Breadcrumbs/styles/style.scss +2 -1
  54. package/src/components/Carousel/Carousel.static.ts +29 -1
  55. package/src/components/Carousel/tests/Carousel.static.test.jsx +50 -0
  56. package/src/components/DocumentationSidebar/DocumentationSidebar.tsx +21 -34
  57. package/src/components/DocumentationSidebar/styles/style.scss +0 -6
  58. package/src/components/Forms/Autocomplete/Autocomplete.static.ts +190 -14
  59. package/src/components/Forms/Autocomplete/styles/style.scss +61 -8
  60. package/src/components/Forms/Autocomplete/tests/Autocomplete.static.test.ts +187 -0
  61. package/src/components/Forms/DatePicker/styles/style.scss +1 -2
  62. package/src/components/Forms/Group/Group.tsx +4 -1
  63. package/src/components/Forms/Group/styles/config.scss +1 -1
  64. package/src/components/Forms/Group/styles/mixins.scss +17 -0
  65. package/src/components/Forms/Group/tests/Group.unit.test.jsx +9 -0
  66. package/src/components/Forms/TextArea/styles/config.scss +1 -0
  67. package/src/components/Forms/TextArea/styles/mixins.scss +7 -1
  68. package/src/components/Forms/TextInput/styles/config.scss +3 -1
  69. package/src/components/Forms/TextInput/styles/mixins.scss +7 -1
  70. package/src/components/Forms/TextInput/styles/style.scss +17 -12
  71. package/src/components/Forms/styles/config.scss +3 -2
  72. package/src/components/Icon/styles/style.scss +2 -1
  73. package/src/components/Loader/styles/style.scss +0 -2
  74. package/src/components/Pagination/styles/mixins.scss +8 -6
  75. package/src/components/Pagination/styles/style.scss +0 -4
  76. package/src/components/Preview/PreviewGenerator.tsx +15 -2
  77. package/src/components/PromotionCard/styles/mixins.scss +2 -1
  78. package/src/components/Section/styles/mixins.scss +27 -5
  79. package/src/components/SocialButton/styles/config.scss +2 -2
  80. package/src/components/Stepbar/styles/style.scss +4 -2
  81. package/src/components/Table/styles/mixins.scss +6 -3
  82. package/src/components/Tag/styles/config.scss +1 -1
  83. package/src/components/Tag/styles/style.scss +22 -5
  84. package/src/components/Tile/Tile.tsx +18 -42
  85. package/src/components/Tile/styles/mixins.scss +45 -47
  86. package/src/components/Tile/styles/style.scss +5 -17
  87. package/src/components/Tile/tests/Tile.unit.test.jsx +9 -78
  88. package/src/styles/tokens/color-vars.scss +0 -2
  89. package/src/styles/utilities/color.scss +0 -153
  90. package/src/components/Tile/CHANGELOG.md +0 -28
  91. package/src/components/Tile/styles/config.scss +0 -7
@@ -6,6 +6,8 @@ export interface AutocompleteConfig {
6
6
  optionClassName: string;
7
7
  customInputClassName: string;
8
8
  displayMenu: string;
9
+ onConfirm?: (value: unknown) => void;
10
+ tClearButton: () => string;
9
11
  tAssistiveHint: () => string;
10
12
  }
11
13
 
@@ -16,25 +18,39 @@ export const defaultConfig: AutocompleteConfig = {
16
18
  optionClassName: "",
17
19
  customInputClassName: "",
18
20
  displayMenu: "overlay",
21
+ tClearButton: () => "Vymazať text",
19
22
  tAssistiveHint: () =>
20
23
  "Pre prezeranie výsledkov automatického dopĺňania použite šípky nahor a nadol a pre výber konkrétneho výsledku použite tlačidlo Enter. Pri dotykových zariadeniach použite dotyk alebo potiahnutie prstom do strany.",
21
24
  };
22
25
 
26
+ const addClassToken = (className: string, token: string) => {
27
+ const classTokens = className.split(/\s+/).filter(Boolean);
28
+
29
+ if (classTokens.includes(token)) {
30
+ return className;
31
+ }
32
+
33
+ return [...classTokens, token].join(" ");
34
+ };
35
+
23
36
  export default class Autocomplete {
24
37
  private element: HTMLElement;
25
38
  private config: AutocompleteConfig;
26
39
  private instance: any;
40
+ private teardownClearButton: (() => void) | null;
27
41
 
28
42
  constructor(element: HTMLElement, config?: Partial<AutocompleteConfig>) {
29
43
  this.element = element;
30
44
  this.config = { ...defaultConfig, ...config };
31
45
  this.onPageshow = this.onPageshow.bind(this);
46
+ this.onClearButtonClick = this.onClearButtonClick.bind(this);
32
47
  this.instance = null;
48
+ this.teardownClearButton = null;
33
49
 
34
50
  (this.element as any).ODS_Autocomplete = this;
35
51
 
36
52
  // Initialize asynchronously to handle dynamic imports
37
- this.init().catch(console.error);
53
+ this.init().catch(() => undefined);
38
54
 
39
55
  return this;
40
56
  }
@@ -55,11 +71,19 @@ export default class Autocomplete {
55
71
  const customInputClassTokens = customInputClassName
56
72
  .split(/\s+/)
57
73
  .filter(Boolean);
74
+ const wrapperClassTokens = this.config.wrapperClassName
75
+ .split(/\s+/)
76
+ .filter(Boolean);
58
77
 
59
- const hasSearchIcon = customInputClassTokens.includes("input--search-icon");
60
- const hasSearchIconWithPlaceholder = customInputClassTokens.includes(
61
- "input--search-icon-with-placeholder",
62
- );
78
+ const hasSearchIcon =
79
+ customInputClassTokens.includes("input--search-icon") ||
80
+ wrapperClassTokens.includes("autocomplete__wrapper--search-icon");
81
+ const hasSearchIconWithPlaceholder =
82
+ customInputClassTokens.includes("input--search-icon-with-placeholder") ||
83
+ wrapperClassTokens.includes(
84
+ "autocomplete__wrapper--search-icon-with-placeholder",
85
+ );
86
+ const isInvalid = this.element.classList.contains("is-invalid");
63
87
 
64
88
  this.config.customInputClassName = customInputClassTokens
65
89
  .filter(
@@ -69,22 +93,25 @@ export default class Autocomplete {
69
93
  )
70
94
  .join(" ");
71
95
 
96
+ if (isInvalid) {
97
+ this.config.customInputClassName = addClassToken(
98
+ this.config.customInputClassName,
99
+ "is-invalid",
100
+ );
101
+ }
102
+
72
103
  if (hasSearchIcon) {
73
- this.config.wrapperClassName = [
104
+ this.config.wrapperClassName = addClassToken(
74
105
  this.config.wrapperClassName,
75
106
  "autocomplete__wrapper--search-icon",
76
- ]
77
- .filter(Boolean)
78
- .join(" ");
107
+ );
79
108
  }
80
109
 
81
110
  if (hasSearchIconWithPlaceholder) {
82
- this.config.wrapperClassName = [
111
+ this.config.wrapperClassName = addClassToken(
83
112
  this.config.wrapperClassName,
84
113
  "autocomplete__wrapper--search-icon-with-placeholder",
85
- ]
86
- .filter(Boolean)
87
- .join(" ");
114
+ );
88
115
  }
89
116
 
90
117
  // Dynamic import to avoid SSR issues
@@ -104,9 +131,156 @@ export default class Autocomplete {
104
131
  });
105
132
  }
106
133
 
134
+ this.initEnhancedControls(hasSearchIconWithPlaceholder);
135
+
107
136
  window.addEventListener("pageshow", this.onPageshow);
108
137
  }
109
138
 
139
+ private getWrapper(): HTMLElement | null {
140
+ if (this.element.tagName === "SELECT") {
141
+ const rootElement = this.element.previousElementSibling;
142
+ if (!(rootElement instanceof HTMLElement)) {
143
+ return null;
144
+ }
145
+
146
+ if (rootElement.classList.contains("autocomplete__wrapper")) {
147
+ return rootElement;
148
+ }
149
+
150
+ return rootElement.querySelector(".autocomplete__wrapper");
151
+ }
152
+
153
+ return this.element.querySelector(".autocomplete__wrapper");
154
+ }
155
+
156
+ private getInput(): HTMLInputElement | null {
157
+ return this.getWrapper()?.querySelector(".autocomplete__input") ?? null;
158
+ }
159
+
160
+ private initEnhancedControls(
161
+ hasSearchIconWithPlaceholder: boolean,
162
+ retries = 10,
163
+ ): void {
164
+ if (!this.getInput()) {
165
+ if (retries > 0) {
166
+ window.setTimeout(
167
+ () =>
168
+ this.initEnhancedControls(
169
+ hasSearchIconWithPlaceholder,
170
+ retries - 1,
171
+ ),
172
+ 0,
173
+ );
174
+ }
175
+
176
+ return;
177
+ }
178
+
179
+ this.syncInputState();
180
+
181
+ if (hasSearchIconWithPlaceholder) {
182
+ this.initClearButton();
183
+ }
184
+ }
185
+
186
+ private syncInputState(): void {
187
+ const input = this.getInput();
188
+ if (!input) {
189
+ return;
190
+ }
191
+
192
+ if (this.element.classList.contains("is-invalid")) {
193
+ input.classList.add("is-invalid");
194
+ input.setAttribute("aria-invalid", "true");
195
+ }
196
+
197
+ if (this.element instanceof HTMLSelectElement && this.element.disabled) {
198
+ input.disabled = true;
199
+ }
200
+ }
201
+
202
+ private initClearButton(): void {
203
+ this.teardownClearButton?.();
204
+
205
+ const wrapper = this.getWrapper();
206
+ const input = this.getInput();
207
+
208
+ if (!wrapper || !input) {
209
+ return;
210
+ }
211
+
212
+ const clearButton = document.createElement("button");
213
+ clearButton.type = "button";
214
+ clearButton.className = "autocomplete__clear";
215
+ clearButton.setAttribute("aria-label", this.config.tClearButton());
216
+
217
+ const updateClearButton = () => {
218
+ clearButton.hidden = input.disabled || input.value.length === 0;
219
+ };
220
+ const scheduleUpdateClearButton = () => {
221
+ window.setTimeout(updateClearButton, 0);
222
+ };
223
+
224
+ clearButton.addEventListener("mousedown", this.preventDefault);
225
+ clearButton.addEventListener("keydown", this.stopPropagation);
226
+ clearButton.addEventListener("click", this.onClearButtonClick);
227
+ input.addEventListener("input", updateClearButton);
228
+ input.addEventListener("change", updateClearButton);
229
+ wrapper.addEventListener("click", scheduleUpdateClearButton);
230
+ wrapper.addEventListener("keyup", scheduleUpdateClearButton);
231
+
232
+ updateClearButton();
233
+ wrapper.appendChild(clearButton);
234
+
235
+ this.teardownClearButton = () => {
236
+ clearButton.removeEventListener("mousedown", this.preventDefault);
237
+ clearButton.removeEventListener("keydown", this.stopPropagation);
238
+ clearButton.removeEventListener("click", this.onClearButtonClick);
239
+ input.removeEventListener("input", updateClearButton);
240
+ input.removeEventListener("change", updateClearButton);
241
+ wrapper.removeEventListener("click", scheduleUpdateClearButton);
242
+ wrapper.removeEventListener("keyup", scheduleUpdateClearButton);
243
+ clearButton.remove();
244
+ };
245
+ }
246
+
247
+ private preventDefault(event: Event): void {
248
+ event.preventDefault();
249
+ }
250
+
251
+ private stopPropagation(event: Event): void {
252
+ event.stopPropagation();
253
+ }
254
+
255
+ private onClearButtonClick(): void {
256
+ const input = this.getInput();
257
+
258
+ if (!input) {
259
+ return;
260
+ }
261
+
262
+ input.value = "";
263
+ input.dispatchEvent(new Event("input", { bubbles: true }));
264
+ input.dispatchEvent(new Event("change", { bubbles: true }));
265
+
266
+ if (this.element instanceof HTMLSelectElement) {
267
+ const emptyOption = Array.from(this.element.options).find(
268
+ (option) => option.value === "",
269
+ );
270
+
271
+ if (emptyOption) {
272
+ emptyOption.selected = true;
273
+ } else {
274
+ this.element.selectedIndex = -1;
275
+ }
276
+
277
+ this.element.dispatchEvent(new Event("change", { bubbles: true }));
278
+ }
279
+
280
+ this.config.onConfirm?.("");
281
+ input.focus();
282
+ }
283
+
110
284
  private onPageshow(e: PageTransitionEvent): void {
111
285
  if (e.persisted && window) {
112
286
  window.location.reload();
@@ -114,13 +288,15 @@ export default class Autocomplete {
114
288
  }
115
289
 
116
290
  public destroy(): void {
291
+ this.teardownClearButton?.();
292
+ this.teardownClearButton = null;
117
293
  window.removeEventListener("pageshow", this.onPageshow);
118
294
  (this.element as any).ODS_Autocomplete = null;
119
295
  }
120
296
 
121
297
  public update(): void {
122
298
  this.destroy();
123
- this.init().catch(console.error);
299
+ this.init().catch(() => undefined);
124
300
  }
125
301
 
126
302
  static getInstance(el: HTMLElement): Autocomplete | null {
@@ -26,16 +26,32 @@
26
26
  content: "";
27
27
  position: absolute;
28
28
  top: 50%;
29
- left: convert.to-rem(8px);
29
+ left: convert.to-rem(10px);
30
30
  width: convert.to-rem(20px);
31
31
  height: convert.to-rem(20px);
32
32
  transform: translateY(-50%);
33
- background-image: textConfig.$background-image-search-icon;
34
- background-repeat: no-repeat;
33
+ background-color: var(--color-icon-brand);
34
+ mask-image: textConfig.$mask-image-search-icon;
35
+ mask-position: center;
36
+ mask-repeat: no-repeat;
37
+ mask-size: contain;
35
38
  pointer-events: none;
36
39
  z-index: 3;
37
40
  }
38
41
 
42
+ &:has(.autocomplete__input:not(:placeholder-shown))::before {
43
+ background-color: var(--color-icon-default);
44
+ }
45
+
46
+ &:has(.autocomplete__input:disabled)::before {
47
+ background-color: var(--color-icon-disabled);
48
+ }
49
+
50
+ &:has(.autocomplete__input.is-invalid)::before,
51
+ &:has(.autocomplete__input[aria-invalid="true"])::before {
52
+ background-color: var(--color-icon-negative);
53
+ }
54
+
39
55
  .autocomplete__input {
40
56
  background-image: none !important;
41
57
  padding-left: convert.to-rem(40px);
@@ -48,17 +64,54 @@
48
64
  }
49
65
  }
50
66
 
51
- &--search-icon {
52
- &::before {
53
- opacity: 0;
67
+ &--search-icon-with-placeholder {
68
+ .autocomplete__input {
69
+ padding-right: convert.to-rem(44px);
54
70
  }
55
71
 
56
- &:has(.autocomplete__input:not(:focus):placeholder-shown)::before {
57
- opacity: 1;
72
+ .autocomplete__hint {
73
+ padding-right: convert.to-rem(44px);
58
74
  }
59
75
  }
60
76
  }
61
77
 
78
+ &__clear {
79
+ position: absolute;
80
+ top: 50%;
81
+ right: 0;
82
+ z-index: 4;
83
+ display: inline-flex;
84
+ align-items: center;
85
+ justify-content: center;
86
+ width: convert.to-rem(44px);
87
+ height: convert.to-rem(36px);
88
+ padding: 0;
89
+ background: transparent;
90
+ border: 0;
91
+ transform: translateY(-50%);
92
+ cursor: pointer;
93
+
94
+ &::before {
95
+ content: "";
96
+ width: convert.to-rem(24px);
97
+ height: convert.to-rem(24px);
98
+ background-color: var(--color-icon-default);
99
+ mask-image: url('data:image/svg+xml,<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M18 20L10 12L18 4L20 6L14 12L20 18L18 20Z" fill="%23000"/><path d="M6 4L14 12L6 20L4 18L10 12L4 6L6 4Z" fill="%23000"/></svg>');
100
+ mask-position: center;
101
+ mask-repeat: no-repeat;
102
+ mask-size: contain;
103
+ }
104
+
105
+ &:focus-visible {
106
+ outline: 2px solid var(--color-border-contrast);
107
+ outline-offset: -2px;
108
+ }
109
+
110
+ &[hidden] {
111
+ display: none;
112
+ }
113
+ }
114
+
62
115
  &__input {
63
116
  @include textMixins.base();
64
117
  @include textMixins.states();
@@ -0,0 +1,187 @@
1
+ import { fireEvent, waitFor } from "@testing-library/dom";
2
+ import { beforeEach, describe, expect, it, vi } from "vitest";
3
+
4
+ import AutocompleteStatic from "../Autocomplete.static";
5
+
6
+ const renderAutocomplete = (options: any) => {
7
+ const wrapper = document.createElement("div");
8
+ wrapper.className = ["autocomplete__wrapper", options.wrapperClassName]
9
+ .filter(Boolean)
10
+ .join(" ");
11
+
12
+ const input = document.createElement("input");
13
+ input.className = ["autocomplete__input", options.customInputClassName]
14
+ .filter(Boolean)
15
+ .join(" ");
16
+ input.value = options.defaultValue ?? "";
17
+
18
+ wrapper.appendChild(input);
19
+ options.element.appendChild(wrapper);
20
+ };
21
+
22
+ const renderAutocompleteAsync = (options: any) => {
23
+ window.setTimeout(() => renderAutocomplete(options), 0);
24
+ };
25
+
26
+ const accessibleAutocompleteMock: any = vi.fn(renderAutocompleteAsync);
27
+
28
+ accessibleAutocompleteMock.enhanceSelectElement = vi.fn((options: any) => {
29
+ const element = document.createElement("div");
30
+ const selectedOption =
31
+ options.selectElement.options[options.selectElement.selectedIndex];
32
+
33
+ options.selectElement.parentNode.insertBefore(element, options.selectElement);
34
+
35
+ renderAutocompleteAsync({
36
+ ...options,
37
+ element,
38
+ defaultValue: selectedOption?.textContent ?? "",
39
+ });
40
+ });
41
+
42
+ vi.mock("@orangesk/accessible-autocomplete", () => ({
43
+ default: accessibleAutocompleteMock,
44
+ }));
45
+
46
+ describe("Autocomplete.static", () => {
47
+ beforeEach(() => {
48
+ document.body.innerHTML = "";
49
+ vi.clearAllMocks();
50
+ });
51
+
52
+ it("moves search icon classes from input config to wrapper config", async () => {
53
+ const select = document.createElement("select");
54
+ document.body.appendChild(select);
55
+
56
+ new AutocompleteStatic(select, {
57
+ customInputClassName: "input--search-icon-with-placeholder custom-class",
58
+ });
59
+
60
+ await waitFor(() => {
61
+ expect(
62
+ accessibleAutocompleteMock.enhanceSelectElement,
63
+ ).toHaveBeenCalled();
64
+ });
65
+
66
+ const config =
67
+ accessibleAutocompleteMock.enhanceSelectElement.mock.calls[0][0];
68
+
69
+ expect(config.wrapperClassName).toContain(
70
+ "autocomplete__wrapper--search-icon-with-placeholder",
71
+ );
72
+ expect(config.customInputClassName).toBe("custom-class");
73
+ });
74
+
75
+ it("passes invalid state to generated input", async () => {
76
+ const select = document.createElement("select");
77
+ select.className = "autocomplete is-invalid";
78
+ document.body.appendChild(select);
79
+
80
+ new AutocompleteStatic(select, {
81
+ customInputClassName: "input--search-icon-with-placeholder",
82
+ });
83
+
84
+ await waitFor(() => {
85
+ expect(document.querySelector(".autocomplete__input")).toHaveClass(
86
+ "is-invalid",
87
+ );
88
+ expect(document.querySelector(".autocomplete__input")).toHaveAttribute(
89
+ "aria-invalid",
90
+ "true",
91
+ );
92
+ });
93
+ });
94
+
95
+ it("passes disabled state to generated input and hides clear button", async () => {
96
+ const select = document.createElement("select");
97
+ const appleOption = new Option("apple", "apple", true, true);
98
+
99
+ select.append(appleOption);
100
+ select.disabled = true;
101
+ document.body.appendChild(select);
102
+
103
+ new AutocompleteStatic(select, {
104
+ customInputClassName: "input--search-icon-with-placeholder",
105
+ });
106
+
107
+ await waitFor(() => {
108
+ expect(document.querySelector(".autocomplete__input")).toBeDisabled();
109
+ expect(document.querySelector(".autocomplete__clear")).not.toBeVisible();
110
+ });
111
+ });
112
+
113
+ it("clears input and select value from search icon autocomplete", async () => {
114
+ const select = document.createElement("select");
115
+ const emptyOption = new Option("", "");
116
+ const appleOption = new Option("apple", "apple", true, true);
117
+
118
+ select.append(emptyOption, appleOption);
119
+ document.body.appendChild(select);
120
+
121
+ const onConfirm = vi.fn();
122
+
123
+ new AutocompleteStatic(select, {
124
+ customInputClassName: "input--search-icon-with-placeholder",
125
+ onConfirm,
126
+ });
127
+
128
+ await waitFor(() => {
129
+ expect(document.querySelector(".autocomplete__clear")).toBeVisible();
130
+ });
131
+
132
+ expect(
133
+ document
134
+ .querySelector(".autocomplete__wrapper")
135
+ ?.contains(document.querySelector(".autocomplete__clear")),
136
+ ).toBe(true);
137
+
138
+ const input = document.querySelector<HTMLInputElement>(
139
+ ".autocomplete__input",
140
+ );
141
+ const clearButton = document.querySelector<HTMLButtonElement>(
142
+ ".autocomplete__clear",
143
+ );
144
+
145
+ expect(input?.value).toBe("apple");
146
+
147
+ fireEvent.click(clearButton as HTMLButtonElement);
148
+
149
+ expect(input?.value).toBe("");
150
+ expect(select.value).toBe("");
151
+ expect(clearButton).not.toBeVisible();
152
+ expect(onConfirm).toHaveBeenCalledWith("");
153
+ });
154
+
155
+ it("keeps clear button keyboard events from reaching autocomplete wrapper", async () => {
156
+ const select = document.createElement("select");
157
+ const emptyOption = new Option("", "");
158
+ const appleOption = new Option("apple", "apple", true, true);
159
+
160
+ select.append(emptyOption, appleOption);
161
+ document.body.appendChild(select);
162
+
163
+ new AutocompleteStatic(select, {
164
+ customInputClassName: "input--search-icon-with-placeholder",
165
+ });
166
+
167
+ await waitFor(() => {
168
+ expect(document.querySelector(".autocomplete__clear")).toBeVisible();
169
+ });
170
+
171
+ const wrapper = document.querySelector(".autocomplete__wrapper");
172
+ const clearButton = document.querySelector<HTMLButtonElement>(
173
+ ".autocomplete__clear",
174
+ );
175
+ const onWrapperKeydown = vi.fn();
176
+
177
+ wrapper?.addEventListener("keydown", onWrapperKeydown);
178
+
179
+ fireEvent.keyDown(clearButton as HTMLButtonElement, {
180
+ key: " ",
181
+ code: "Space",
182
+ keyCode: 32,
183
+ });
184
+
185
+ expect(onWrapperKeydown).not.toHaveBeenCalled();
186
+ });
187
+ });
@@ -70,7 +70,7 @@
70
70
  border-color: var(--color-surface-moderate);
71
71
  }
72
72
  }
73
- .bg-black .daypickr__select,
73
+
74
74
  .is-dark .daypickr__select {
75
75
  background-image: url("data:image/svg+xml,%3Csvg width='16' height='16' fill='white' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M20 10L12 18L4 10L6 8L12 14L18 8L20 10Z'/%3E%3C/svg%3E%0A");
76
76
  }
@@ -109,7 +109,6 @@
109
109
  }
110
110
  }
111
111
 
112
- .bg-black .daypickr__button--pagination,
113
112
  .is-dark .daypickr__button--pagination {
114
113
  &-prev::before {
115
114
  background-image: url("data:image/svg+xml,%3Csvg width='20' height='20' fill='white' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M14 20L6 12L14 4L16 6L10 12L16 18L14 20Z'/%3E%3C/svg%3E%0A");
@@ -1,5 +1,6 @@
1
1
  import cx from "classnames";
2
2
  import React, { HTMLAttributes, ReactNode } from "react";
3
+ import { Autocomplete } from "../Autocomplete/Autocomplete";
3
4
  import { Select } from "../Select/Select";
4
5
  import { TextInput } from "../TextInput/TextInput";
5
6
 
@@ -8,7 +9,7 @@ import { Item } from "./Item";
8
9
  const CLASS_ROOT = "control-group";
9
10
 
10
11
  interface ControlShape {
11
- type: "text" | "select";
12
+ type: "autocomplete" | "text" | "select";
12
13
  [key: string]: any;
13
14
  }
14
15
 
@@ -35,6 +36,8 @@ const Group: React.FC<GroupProps> = ({
35
36
 
36
37
  const ControlElement = (props: any) => {
37
38
  switch (type) {
39
+ case "autocomplete":
40
+ return <Autocomplete {...props} />;
38
41
  case "select":
39
42
  return <Select {...props} />;
40
43
  default:
@@ -2,7 +2,7 @@
2
2
  @use "../../TextInput/styles/config" as TextInputConfig;
3
3
 
4
4
  $item: (
5
- border: 2px solid var(--color-surface-moderate),
5
+ border: 2px solid var(--color-border-strong),
6
6
  background: var(--color-surface-subtle),
7
7
  );
8
8
 
@@ -39,15 +39,32 @@
39
39
  border-radius: 0 !important;
40
40
  }
41
41
 
42
+ > *:not(:first-child):not(:last-child) .autocomplete__input {
43
+ border-radius: 0 !important;
44
+ }
45
+
42
46
  > *:first-child:not(:last-child) {
43
47
  border-top-right-radius: 0 !important;
44
48
  border-bottom-right-radius: 0 !important;
45
49
  }
46
50
 
51
+ > *:first-child:not(:last-child) .autocomplete__input,
52
+ > *:has(+ .autocomplete[style*="display: none"] + *) .autocomplete__input {
53
+ border-top-right-radius: 0 !important;
54
+ border-bottom-right-radius: 0 !important;
55
+ }
56
+
47
57
  > *:last-child:not(:first-child) {
48
58
  border-top-left-radius: 0 !important;
49
59
  border-bottom-left-radius: 0 !important;
50
60
  }
61
+
62
+ > *:last-child:not(:first-child) .autocomplete__input,
63
+ > *:not(:first-child):has(+ .autocomplete[style*="display: none"])
64
+ .autocomplete__input {
65
+ border-top-left-radius: 0 !important;
66
+ border-bottom-left-radius: 0 !important;
67
+ }
51
68
  }
52
69
 
53
70
  @mixin item($config: config.$item) {
@@ -109,6 +109,15 @@ describe("rendering Group", () => {
109
109
  );
110
110
  expect(container.getElementsByClassName("select").length).toBe(1);
111
111
  });
112
+ it("renders Autocomplete component (autocomplete class) when control type is set to autocomplete", () => {
113
+ const { container } = render(
114
+ <Group
115
+ data-testid="test-id"
116
+ control={{ type: "autocomplete", options: ["", "apple"] }}
117
+ />,
118
+ );
119
+ expect(container.getElementsByClassName("autocomplete").length).toBe(1);
120
+ });
112
121
  it("addonPrefix Item has class control-group__item--large when size is set to large", () => {
113
122
  const { container } = render(
114
123
  <Group
@@ -13,6 +13,7 @@ $states: (
13
13
  disabled: commonConfig.$disabled,
14
14
  readonly: commonConfig.$readonly,
15
15
  invalid: commonConfig.$invalid,
16
+ placeholder: commonConfig.$placeholder,
16
17
  );
17
18
 
18
19
  $sizes: (
@@ -22,7 +22,7 @@
22
22
  @include generate.css-map($config, "disabled");
23
23
  }
24
24
 
25
- &:read-only {
25
+ &:read-only:not(:disabled) {
26
26
  @include generate.css-map($config, "readonly");
27
27
  }
28
28
 
@@ -30,6 +30,12 @@
30
30
  @include generate.css-map($config, "invalid");
31
31
  }
32
32
 
33
+ &::placeholder {
34
+ @include generate.css-map($config, "placeholder");
35
+
36
+ opacity: 1;
37
+ }
38
+
33
39
  &.is-valid {
34
40
  padding-right: 36px;
35
41
  background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='15' height='15' viewBox='0 0 15 15'%3E%3Cpath d='M7.5,15 C3.35786438,15 0,11.6421356 0,7.5 C0,3.35786438 3.35786438,0 7.5,0 C11.6421356,0 15,3.35786438 15,7.5 C15,11.6421356 11.6421356,15 7.5,15 Z M3.40550597,6.68901229 C2.99100286,7.0539857 2.95085113,7.6858763 3.31582458,8.1003794 L5.77477826,10.8930318 C6.1397517,11.3075349 6.7716423,11.3476866 7.18614541,10.9827132 C7.20478962,10.9662968 7.22281563,10.9491914 7.24018594,10.9314326 L11.8941565,6.17336831 C12.2803381,5.7785491 12.2733359,5.14542284 11.8785167,4.75924123 C11.4836975,4.37305961 10.8505712,4.38006176 10.4643896,4.77488097 L7.31720124,7.9924615 C6.93101963,8.3872807 6.29789337,8.3942828 5.90307416,8.0081012 C5.88531533,7.9907309 5.86820993,7.9727049 5.85179355,7.9540607 L4.81687313,6.77869369 C4.45189969,6.36419058 3.82000909,6.32403885 3.40550597,6.68901229 Z' fill='%2332c832' /%3E%3C/svg%3E");