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

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 (98) 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/Card/style.css +1 -1
  6. package/build/components/Card/style.css.map +1 -1
  7. package/build/components/DocumentationSidebar/style.css +1 -1
  8. package/build/components/DocumentationSidebar/style.css.map +1 -1
  9. package/build/components/Icon/style.css +1 -1
  10. package/build/components/Icon/style.css.map +1 -1
  11. package/build/components/Loader/style.css +1 -1
  12. package/build/components/Loader/style.css.map +1 -1
  13. package/build/components/Megamenu/style.css +1 -1
  14. package/build/components/Megamenu/style.css.map +1 -1
  15. package/build/components/Pagination/style.css +1 -1
  16. package/build/components/Pagination/style.css.map +1 -1
  17. package/build/components/PromotionCard/style.css +1 -1
  18. package/build/components/PromotionCard/style.css.map +1 -1
  19. package/build/components/Section/style.css +1 -1
  20. package/build/components/Section/style.css.map +1 -1
  21. package/build/components/SocialButton/style.css +1 -1
  22. package/build/components/SocialButton/style.css.map +1 -1
  23. package/build/components/Stepbar/style.css +1 -1
  24. package/build/components/Stepbar/style.css.map +1 -1
  25. package/build/components/Table/style.css +1 -1
  26. package/build/components/Table/style.css.map +1 -1
  27. package/build/components/Tag/style.css +1 -1
  28. package/build/components/Tag/style.css.map +1 -1
  29. package/build/components/Tile/style.css +1 -1
  30. package/build/components/Tile/style.css.map +1 -1
  31. package/build/components/index.js +1 -1
  32. package/build/components/index.js.map +1 -1
  33. package/build/components/tsconfig.tsbuildinfo +1 -1
  34. package/build/components/types/index.d.ts +5 -11
  35. package/build/components/types/src/components/Carousel/Carousel.static.d.ts +4 -1
  36. package/build/components/types/src/components/CarouselHero/CarouselHero.d.ts +1 -0
  37. package/build/components/types/src/components/Forms/Autocomplete/Autocomplete.static.d.ts +11 -0
  38. package/build/components/types/src/components/Forms/Group/Group.d.ts +1 -1
  39. package/build/components/types/src/components/Tile/Tile.d.ts +3 -11
  40. package/build/lib/base.css +1 -1
  41. package/build/lib/base.css.map +1 -1
  42. package/build/lib/components.css +1 -1
  43. package/build/lib/components.css.map +1 -1
  44. package/build/lib/megamenu.css +1 -1
  45. package/build/lib/megamenu.css.map +1 -1
  46. package/build/lib/scripts.js +1 -1
  47. package/build/lib/scripts.js.map +1 -1
  48. package/build/lib/style.css +1 -1
  49. package/build/lib/style.css.map +1 -1
  50. package/build/lib/tsconfig.tsbuildinfo +1 -1
  51. package/build/lib/utilities.css +1 -1
  52. package/build/lib/utilities.css.map +1 -1
  53. package/build/search-index.json +5 -5
  54. package/package.json +21 -21
  55. package/src/components/BlockAction/styles/style.scss +6 -4
  56. package/src/components/Breadcrumbs/styles/style.scss +2 -1
  57. package/src/components/Card/styles/config.scss +1 -1
  58. package/src/components/Carousel/Carousel.static.ts +29 -1
  59. package/src/components/Carousel/tests/Carousel.static.test.jsx +50 -0
  60. package/src/components/CarouselHero/CarouselHero.static.ts +7 -0
  61. package/src/components/CarouselHero/CarouselHero.tsx +61 -3
  62. package/src/components/CarouselHero/tests/CarouselHero.unit.test.jsx +31 -1
  63. package/src/components/DocumentationSidebar/DocumentationSidebar.tsx +21 -34
  64. package/src/components/DocumentationSidebar/styles/style.scss +0 -6
  65. package/src/components/Forms/Autocomplete/Autocomplete.static.ts +190 -14
  66. package/src/components/Forms/Autocomplete/styles/style.scss +61 -8
  67. package/src/components/Forms/Autocomplete/tests/Autocomplete.static.test.ts +187 -0
  68. package/src/components/Forms/DatePicker/styles/style.scss +1 -2
  69. package/src/components/Forms/Group/Group.tsx +4 -1
  70. package/src/components/Forms/Group/styles/config.scss +1 -1
  71. package/src/components/Forms/Group/styles/mixins.scss +17 -0
  72. package/src/components/Forms/Group/tests/Group.unit.test.jsx +9 -0
  73. package/src/components/Forms/TextArea/styles/config.scss +1 -0
  74. package/src/components/Forms/TextArea/styles/mixins.scss +7 -1
  75. package/src/components/Forms/TextInput/styles/config.scss +3 -1
  76. package/src/components/Forms/TextInput/styles/mixins.scss +7 -1
  77. package/src/components/Forms/TextInput/styles/style.scss +17 -12
  78. package/src/components/Forms/styles/config.scss +3 -2
  79. package/src/components/Icon/styles/style.scss +2 -1
  80. package/src/components/Loader/styles/style.scss +0 -2
  81. package/src/components/Pagination/styles/mixins.scss +8 -6
  82. package/src/components/Pagination/styles/style.scss +0 -4
  83. package/src/components/Preview/PreviewGenerator.tsx +15 -2
  84. package/src/components/PromotionCard/styles/mixins.scss +2 -1
  85. package/src/components/Section/styles/mixins.scss +27 -5
  86. package/src/components/SocialButton/styles/config.scss +2 -2
  87. package/src/components/Stepbar/styles/style.scss +4 -2
  88. package/src/components/Table/styles/mixins.scss +6 -3
  89. package/src/components/Tag/styles/config.scss +1 -1
  90. package/src/components/Tag/styles/style.scss +22 -5
  91. package/src/components/Tile/Tile.tsx +18 -42
  92. package/src/components/Tile/styles/mixins.scss +55 -45
  93. package/src/components/Tile/styles/style.scss +5 -17
  94. package/src/components/Tile/tests/Tile.unit.test.jsx +9 -78
  95. package/src/styles/tokens/color-vars.scss +0 -2
  96. package/src/styles/utilities/color.scss +0 -153
  97. package/src/components/Tile/CHANGELOG.md +0 -28
  98. package/src/components/Tile/styles/config.scss +0 -7
@@ -381,7 +381,7 @@ export const DocumentationSidebar: React.FC<DocumentationSidebarProps> = (
381
381
  >
382
382
  <Image src="/logo.svg" alt="logo" />
383
383
 
384
- <div className={`${CLASS_ROOT}__search-container`}>
384
+ <div className="mb">
385
385
  <TextInput
386
386
  id="search-documentation"
387
387
  htmlType="search"
@@ -393,42 +393,29 @@ export const DocumentationSidebar: React.FC<DocumentationSidebarProps> = (
393
393
  aria-label="Search documentation"
394
394
  ref={searchInputRef}
395
395
  />
396
- {searchQuery && (
397
- <button
398
- type="button"
399
- className={`${CLASS_ROOT}__search-clear`}
400
- onClick={() => {
401
- setSearchQuery("");
402
- setSearchResults([]);
403
- }}
404
- aria-label="Clear search"
405
- >
406
-
407
- </button>
396
+
397
+ {searchResults.length > 0 && (
398
+ <div className={`${CLASS_ROOT}__search-results`}>
399
+ <ul className={`${CLASS_ROOT}__search-list`}>
400
+ {searchResults.map((result, index) => (
401
+ <li key={result.id}>
402
+ <Link
403
+ href={result.href}
404
+ className={cx(`${CLASS_ROOT}__search-result-link`, {
405
+ [`${CLASS_ROOT}__search-result-link--active`]:
406
+ index === activeSearchIndex,
407
+ })}
408
+ onClick={() => handleSelect(result.href)}
409
+ >
410
+ {result.label}
411
+ </Link>
412
+ </li>
413
+ ))}
414
+ </ul>
415
+ </div>
408
416
  )}
409
417
  </div>
410
418
 
411
- {searchResults.length > 0 && (
412
- <div className={`${CLASS_ROOT}__search-results`}>
413
- <ul className={`${CLASS_ROOT}__search-list`}>
414
- {searchResults.map((result, index) => (
415
- <li key={result.id}>
416
- <Link
417
- href={result.href}
418
- className={cx(`${CLASS_ROOT}__search-result-link`, {
419
- [`${CLASS_ROOT}__search-result-link--active`]:
420
- index === activeSearchIndex,
421
- })}
422
- onClick={() => handleSelect(result.href)}
423
- >
424
- {result.label}
425
- </Link>
426
- </li>
427
- ))}
428
- </ul>
429
- </div>
430
- )}
431
-
432
419
  <nav
433
420
  className={`${CLASS_ROOT}__nav`}
434
421
  style={{ display: searchResults.length > 0 ? "none" : "block" }}
@@ -201,12 +201,6 @@
201
201
  padding-left: convert.to-rem(15px);
202
202
  }
203
203
 
204
- // Search styles
205
- &__search-container {
206
- position: relative;
207
- margin-bottom: 16px;
208
- }
209
-
210
204
  &__search-clear {
211
205
  position: absolute;
212
206
  right: 8px;
@@ -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: