@sapudex/web-components 2.39.0 → 2.41.0

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 (161) hide show
  1. package/cypress/specs/Dropdown.cy.tsx +90 -0
  2. package/cypress/specs/PasswordField.cy.tsx +37 -0
  3. package/cypress/specs/Search.cy.tsx +244 -138
  4. package/cypress/specs/SelectBox.cy.tsx +457 -0
  5. package/cypress/specs/Tile.cy.tsx +96 -0
  6. package/cypress/support/component-imports.instrumented.ts +4 -0
  7. package/cypress/support/component-imports.src.ts +4 -0
  8. package/dist/Dropdown.d.ts +5 -0
  9. package/dist/Dropdown.js +56 -0
  10. package/dist/Dropdown.js.map +1 -1
  11. package/dist/List.d.ts +1 -6
  12. package/dist/List.js +2 -10
  13. package/dist/List.js.map +1 -1
  14. package/dist/MastheadCountrySelector.d.ts +7 -0
  15. package/dist/MastheadCountrySelector.js +3 -0
  16. package/dist/MastheadCountrySelector.js.map +1 -1
  17. package/dist/PasswordField.d.ts +10 -0
  18. package/dist/PasswordField.js +17 -2
  19. package/dist/PasswordField.js.map +1 -1
  20. package/dist/PasswordFieldTemplate.js +1 -1
  21. package/dist/PasswordFieldTemplate.js.map +1 -1
  22. package/dist/Search.d.ts +1 -1
  23. package/dist/Search.js +13 -9
  24. package/dist/Search.js.map +1 -1
  25. package/dist/SearchTemplate.js +3 -3
  26. package/dist/SearchTemplate.js.map +1 -1
  27. package/dist/SearchableListBase.d.ts +2 -6
  28. package/dist/SearchableListBase.js +6 -3
  29. package/dist/SearchableListBase.js.map +1 -1
  30. package/dist/SearchableListBaseTemplate.js +1 -1
  31. package/dist/SearchableListBaseTemplate.js.map +1 -1
  32. package/dist/SelectBase.d.ts +20 -2
  33. package/dist/SelectBase.js +189 -13
  34. package/dist/SelectBase.js.map +1 -1
  35. package/dist/SelectBox.js +2 -1
  36. package/dist/SelectBox.js.map +1 -1
  37. package/dist/SelectBoxTemplate.js +4 -4
  38. package/dist/SelectBoxTemplate.js.map +1 -1
  39. package/dist/TextField.d.ts +1 -1
  40. package/dist/TextField.js +2 -2
  41. package/dist/TextField.js.map +1 -1
  42. package/dist/TextFieldTemplate.js +1 -1
  43. package/dist/TextFieldTemplate.js.map +1 -1
  44. package/dist/assets/{InputSuggestions-jTW8u1vD.js → InputSuggestions-BSg7bjcx.js} +1 -1
  45. package/dist/assets/{ListItemStandardExpandableTextTemplate-ShRY1eSJ.js → ListItemStandardExpandableTextTemplate-CCEQur_J.js} +1 -1
  46. package/dist/assets/{bundle.esm-VI-guBTG.js → bundle.esm-I7iOjLgn.js} +232 -232
  47. package/dist/assets/messagebundle_en-BZqDcZHN.js +1 -0
  48. package/dist/assets/messagebundle_et-yYsmBPXN.js +1 -0
  49. package/dist/assets/messagebundle_fi-DeMNrHC9.js +1 -0
  50. package/dist/assets/messagebundle_fr-qEm5Eykz.js +1 -0
  51. package/dist/assets/messagebundle_hr-NBdiYo-T.js +1 -0
  52. package/dist/assets/messagebundle_hu-CBV3ISbF.js +1 -0
  53. package/dist/css/themes/MastheadCountrySelector.css +1 -1
  54. package/dist/css/themes/MastheadProfileMenu.css +1 -1
  55. package/dist/css/themes/SelectBase.css +1 -1
  56. package/dist/css/themes/SelectBox.css +1 -1
  57. package/dist/css/themes/StatusMessage.css +1 -1
  58. package/dist/css/themes/TextField.css +1 -1
  59. package/dist/css/themes/Tile.css +1 -1
  60. package/dist/css/themes/Toast.css +1 -1
  61. package/dist/custom-elements-internal.json +85 -161
  62. package/dist/custom-elements.json +56 -120
  63. package/dist/features/SearchSuggestionsPopover.d.ts +6 -9
  64. package/dist/features/SearchSuggestionsPopover.js +47 -163
  65. package/dist/features/SearchSuggestionsPopover.js.map +1 -1
  66. package/dist/generated/assets/i18n/messagebundle_en.json +1 -1
  67. package/dist/generated/assets/i18n/messagebundle_et.json +1 -1
  68. package/dist/generated/assets/i18n/messagebundle_fi.json +1 -1
  69. package/dist/generated/assets/i18n/messagebundle_fr.json +1 -1
  70. package/dist/generated/assets/i18n/messagebundle_hr.json +1 -1
  71. package/dist/generated/assets/i18n/messagebundle_hu.json +1 -1
  72. package/dist/generated/i18n/i18n-defaults.d.ts +3 -2
  73. package/dist/generated/i18n/i18n-defaults.js +3 -2
  74. package/dist/generated/i18n/i18n-defaults.js.map +1 -1
  75. package/dist/generated/templates/MastheadCountrySelectorTemplate.lit.js +6 -5
  76. package/dist/generated/templates/MastheadCountrySelectorTemplate.lit.js.map +1 -1
  77. package/dist/generated/templates/MastheadMobileMenuTemplate.lit.js +1 -1
  78. package/dist/generated/templates/MastheadMobileMenuTemplate.lit.js.map +1 -1
  79. package/dist/generated/themes/MastheadCountrySelector.css.d.ts +1 -1
  80. package/dist/generated/themes/MastheadCountrySelector.css.js +1 -1
  81. package/dist/generated/themes/MastheadCountrySelector.css.js.map +1 -1
  82. package/dist/generated/themes/MastheadProfileMenu.css.d.ts +1 -1
  83. package/dist/generated/themes/MastheadProfileMenu.css.js +1 -1
  84. package/dist/generated/themes/MastheadProfileMenu.css.js.map +1 -1
  85. package/dist/generated/themes/SelectBase.css.d.ts +1 -1
  86. package/dist/generated/themes/SelectBase.css.js +1 -1
  87. package/dist/generated/themes/SelectBase.css.js.map +1 -1
  88. package/dist/generated/themes/SelectBox.css.d.ts +1 -1
  89. package/dist/generated/themes/SelectBox.css.js +1 -1
  90. package/dist/generated/themes/SelectBox.css.js.map +1 -1
  91. package/dist/generated/themes/StatusMessage.css.d.ts +1 -1
  92. package/dist/generated/themes/StatusMessage.css.js +1 -1
  93. package/dist/generated/themes/StatusMessage.css.js.map +1 -1
  94. package/dist/generated/themes/TextField.css.d.ts +1 -1
  95. package/dist/generated/themes/TextField.css.js +1 -1
  96. package/dist/generated/themes/TextField.css.js.map +1 -1
  97. package/dist/generated/themes/Tile.css.d.ts +1 -1
  98. package/dist/generated/themes/Tile.css.js +1 -1
  99. package/dist/generated/themes/Tile.css.js.map +1 -1
  100. package/dist/generated/themes/Toast.css.d.ts +1 -1
  101. package/dist/generated/themes/Toast.css.js +1 -1
  102. package/dist/generated/themes/Toast.css.js.map +1 -1
  103. package/dist/messagebundle.properties +2 -1
  104. package/dist/messagebundle_en.properties +2 -1
  105. package/dist/messagebundle_et.properties +126 -0
  106. package/dist/messagebundle_fi.properties +127 -0
  107. package/dist/messagebundle_fr.properties +120 -0
  108. package/dist/messagebundle_hr.properties +125 -0
  109. package/dist/messagebundle_hu.properties +125 -0
  110. package/dist/vscode.html-custom-data.json +12 -27
  111. package/dist/web-types.json +36 -44
  112. package/package.json +1 -1
  113. package/src/Dropdown.ts +62 -0
  114. package/src/List.ts +3 -18
  115. package/src/MastheadCountrySelector.hbs +2 -0
  116. package/src/MastheadCountrySelector.ts +9 -0
  117. package/src/MastheadMobileMenu.hbs +1 -0
  118. package/src/PasswordField.ts +24 -0
  119. package/src/PasswordFieldTemplate.tsx +2 -2
  120. package/src/Search.ts +13 -9
  121. package/src/SearchTemplate.tsx +0 -3
  122. package/src/SearchableListBase.ts +8 -9
  123. package/src/SearchableListBaseTemplate.tsx +0 -1
  124. package/src/SelectBase.ts +203 -16
  125. package/src/SelectBox.ts +2 -1
  126. package/src/SelectBoxTemplate.tsx +1 -0
  127. package/src/TextField.ts +1 -1
  128. package/src/TextFieldTemplate.tsx +1 -1
  129. package/src/features/SearchSuggestionsPopover.ts +55 -177
  130. package/src/generated/i18n/i18n-defaults.ts +2 -2
  131. package/src/generated/templates/MastheadCountrySelectorTemplate.lit.ts +6 -5
  132. package/src/generated/templates/MastheadMobileMenuTemplate.lit.ts +1 -1
  133. package/src/generated/themes/MastheadCountrySelector.css.ts +1 -1
  134. package/src/generated/themes/MastheadProfileMenu.css.ts +1 -1
  135. package/src/generated/themes/SelectBase.css.ts +1 -1
  136. package/src/generated/themes/SelectBox.css.ts +1 -1
  137. package/src/generated/themes/StatusMessage.css.ts +1 -1
  138. package/src/generated/themes/TextField.css.ts +1 -1
  139. package/src/generated/themes/Tile.css.ts +1 -1
  140. package/src/generated/themes/Toast.css.ts +1 -1
  141. package/src/i18n/messagebundle.properties +2 -1
  142. package/src/i18n/messagebundle_en.properties +2 -1
  143. package/src/i18n/messagebundle_et.properties +126 -0
  144. package/src/i18n/messagebundle_fi.properties +127 -0
  145. package/src/i18n/messagebundle_fr.properties +120 -0
  146. package/src/i18n/messagebundle_hr.properties +125 -0
  147. package/src/i18n/messagebundle_hu.properties +125 -0
  148. package/src/themes/MastheadCountrySelector.css +5 -2
  149. package/src/themes/MastheadProfileMenu.css +0 -5
  150. package/src/themes/SelectBase.css +7 -0
  151. package/src/themes/SelectBox.css +4 -0
  152. package/src/themes/StatusMessage.css +4 -4
  153. package/src/themes/TextField.css +3 -0
  154. package/src/themes/Tile.css +11 -3
  155. package/src/themes/Toast.css +14 -2
  156. package/dist/assets/messagebundle_en-BxjFAY2S.js +0 -1
  157. package/dist/assets/messagebundle_et-BCYOcg2t.js +0 -1
  158. package/dist/assets/messagebundle_fi-CssCIjmp.js +0 -1
  159. package/dist/assets/messagebundle_fr-CvOyuAVw.js +0 -1
  160. package/dist/assets/messagebundle_hr-CashhWbN.js +0 -1
  161. package/dist/assets/messagebundle_hu-BEMbu-EE.js +0 -1
package/src/SelectBase.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import property from "@ui5/webcomponents-base/dist/decorators/property.js";
2
+ import slot from "@ui5/webcomponents-base/dist/decorators/slot-strict.js";
2
3
  import executeTemplate from "@ui5/webcomponents-base/dist/renderer/executeTemplate.js";
3
- import type { ChangeInfo } from "@ui5/webcomponents-base/dist/UI5Element.js";
4
+ import type { ChangeInfo, Slot } from "@ui5/webcomponents-base/dist/UI5Element.js";
4
5
  import {
5
- isEscape, isDown, isUp, isHome, isEnd, isTabNext, isTabPrevious,
6
+ isEscape, isDown, isUp, isHome, isEnd, isTabNext, isTabPrevious, isLeft, isRight,
6
7
  } from "@ui5/webcomponents-base/dist/Keys.js";
7
8
  import { getAllAccessibleNameRefTexts } from "@ui5/webcomponents-base/dist/util/AccessibilityTextsHelper.js";
8
9
  import type { UDExListItemBase } from "./List.js";
@@ -163,6 +164,16 @@ class UDExSelectBase extends UDExSearchableListBase {
163
164
  @property({ type: Boolean, noAttribute: true })
164
165
  _requestRender = false;
165
166
 
167
+ /**
168
+ * Defines the footer content for the component dropdown.
169
+ * Place any content (e.g. an action button) here. Clicking footer content does not close the dropdown —
170
+ * use the <code>close</code> method if needed.
171
+ *
172
+ * @public
173
+ */
174
+ @slot({ type: HTMLElement })
175
+ footer!: Slot<HTMLElement>;
176
+
166
177
  _paneWidthVariableName = "--udex-dropdown-pane-width";
167
178
  _labelBaseClass = "udex-text-field__label";
168
179
  _dropdownClass = "udex-select-box__dropdown";
@@ -177,10 +188,13 @@ class UDExSelectBase extends UDExSearchableListBase {
177
188
  _inputValue: string = "";
178
189
  _lastSelectedItemId: string = "";
179
190
  _handleResizeBound: ResizeObserverCallback;
191
+ _handleListKeyDownBound: (e: KeyboardEvent) => void;
192
+ _listKeyDownListenerAdded: boolean = false;
180
193
 
181
194
  constructor() {
182
195
  super();
183
196
  this._handleResizeBound = this._handleResize.bind(this);
197
+ this._handleListKeyDownBound = this._handleListKeyDownCapture.bind(this);
184
198
  }
185
199
 
186
200
  onEnterDOM() {
@@ -192,6 +206,10 @@ class UDExSelectBase extends UDExSearchableListBase {
192
206
 
193
207
  onExitDOM() {
194
208
  ResizeHandler.deregister(this, this._handleResizeBound);
209
+ if (this._list && this._listKeyDownListenerAdded) {
210
+ this._list.removeEventListener("keydown", this._handleListKeyDownBound, { capture: true });
211
+ this._listKeyDownListenerAdded = false;
212
+ }
195
213
  }
196
214
 
197
215
  onInvalidation({ name }: ChangeInfo): void {
@@ -210,6 +228,11 @@ class UDExSelectBase extends UDExSearchableListBase {
210
228
  if (this._requestRender) {
211
229
  this._requestRender = false;
212
230
  }
231
+
232
+ if (!this._listKeyDownListenerAdded && this._list) {
233
+ this._list.addEventListener("keydown", this._handleListKeyDownBound, { capture: true });
234
+ this._listKeyDownListenerAdded = true;
235
+ }
213
236
  }
214
237
 
215
238
  _handleResize() {
@@ -224,14 +247,44 @@ class UDExSelectBase extends UDExSearchableListBase {
224
247
  return;
225
248
  }
226
249
 
227
- if (isTabPrevious(e) && !this.tabNavigationEnabled) {
250
+ if (this.hasFooter) {
251
+ const isInFooter = (e.target as HTMLElement).closest?.("[slot=\"footer\"]");
252
+ if (isInFooter) {
253
+ if (isEscape(e)) {
254
+ this.handleEscKeyPress(e);
255
+ return;
256
+ }
257
+ if (isTabNext(e)) {
258
+ e.preventDefault();
259
+ this.closeDropdown();
260
+ this._moveFocusAfterComponent();
261
+ return;
262
+ }
263
+ if (isTabPrevious(e)) {
264
+ e.preventDefault();
265
+ this._focusLastListItem();
266
+ return;
267
+ }
268
+ return;
269
+ }
270
+
271
+ if (isTabNext(e)) {
272
+ e.preventDefault();
273
+ (this._firstFocusableIn(this.footer[0]) ?? this.footer[0])?.focus();
274
+ return;
275
+ }
276
+ }
277
+
278
+ if (isTabPrevious(e)) {
228
279
  e.preventDefault();
229
280
  this._search.focus();
230
281
  return;
231
282
  }
232
283
 
233
- if (isTabNext(e) && !this.tabNavigationEnabled) {
284
+ if (isTabNext(e)) {
285
+ e.preventDefault();
234
286
  this.closeDropdown();
287
+ this._moveFocusAfterComponent();
235
288
  return;
236
289
  }
237
290
 
@@ -240,15 +293,32 @@ class UDExSelectBase extends UDExSearchableListBase {
240
293
  return;
241
294
  }
242
295
 
243
- if (!items || this.tabNavigationEnabled) {
296
+ if (!items) {
297
+ return;
298
+ }
299
+
300
+ if (isRight(e)) {
301
+ e.preventDefault();
302
+ this._handleArrowRightInRow(e.target as HTMLElement);
303
+ return;
304
+ }
305
+
306
+ if (isLeft(e)) {
307
+ e.preventDefault();
308
+ this._handleArrowLeftInRow(e.target as HTMLElement);
244
309
  return;
245
310
  }
246
311
 
247
- this.handleListNavigationKeys(e, items);
312
+ const iconSlot = this._getIconButtonSlot(e.target as HTMLElement);
313
+ const effectiveTarget = iconSlot
314
+ ? (e.target as HTMLElement).parentElement as UDExListItemBase | undefined
315
+ : undefined;
316
+
317
+ this.handleListNavigationKeys(e, items, effectiveTarget);
248
318
  }
249
319
 
250
- handleListNavigationKeys(e: KeyboardEvent, items: UDExListItemBase[]) {
251
- const currentSelectedIndex = this._list?.getCurrentIndex(items, e.target as UDExListItemBase);
320
+ handleListNavigationKeys(e: KeyboardEvent, items: UDExListItemBase[], effectiveTarget?: UDExListItemBase) {
321
+ const currentSelectedIndex = this._list?.getCurrentIndex(items, effectiveTarget ?? e.target as UDExListItemBase);
252
322
  const lastItemIndex = items.length - 1;
253
323
 
254
324
  if (isDown(e)) {
@@ -352,10 +422,7 @@ class UDExSelectBase extends UDExSearchableListBase {
352
422
  e.stopPropagation();
353
423
  this.closeDropdown();
354
424
  this._search.focus();
355
-
356
- if (!this.tabNavigationEnabled) {
357
- this._selectedOption = "";
358
- }
425
+ this._selectedOption = "";
359
426
  }
360
427
 
361
428
  handleItemSelection(e:CustomEvent) {
@@ -412,10 +479,7 @@ class UDExSelectBase extends UDExSearchableListBase {
412
479
  const firstIndex = (items[0]?.nonInteractive && !items[0]?.collapsible) ? 1 : 0;
413
480
  const firstItem = items[firstIndex];
414
481
  this._list.setTabIndexByIndex(items, firstIndex);
415
-
416
- if (!this.tabNavigationEnabled) {
417
- this._selectedOption = firstItem.label;
418
- }
482
+ this._selectedOption = firstItem.label;
419
483
 
420
484
  if (firstItem.isGroup) {
421
485
  this._list.focusHeader(firstItem);
@@ -537,6 +601,129 @@ class UDExSelectBase extends UDExSearchableListBase {
537
601
  }
538
602
  }
539
603
 
604
+ get hasFooter(): boolean {
605
+ return this.footer.length > 0;
606
+ }
607
+
608
+ _getIconButtonSlot(el: HTMLElement): string | null {
609
+ const iconSlot = el.getAttribute?.("slot");
610
+ return (iconSlot === "firstIcon" || iconSlot === "secondIcon") ? iconSlot : null;
611
+ }
612
+
613
+ _handleArrowRightInRow(target: HTMLElement): void {
614
+ const iconSlot = this._getIconButtonSlot(target);
615
+ if (!iconSlot) {
616
+ const firstIcon = target.querySelector<HTMLElement>("[slot=\"firstIcon\"]");
617
+ firstIcon?.focus();
618
+ return;
619
+ }
620
+ if (iconSlot === "firstIcon") {
621
+ const parentItem = target.parentElement;
622
+ if (parentItem) {
623
+ const secondIcon = parentItem.querySelector<HTMLElement>("[slot=\"secondIcon\"]");
624
+ secondIcon?.focus();
625
+ }
626
+ }
627
+ }
628
+
629
+ _handleArrowLeftInRow(target: HTMLElement): void {
630
+ const iconSlot = this._getIconButtonSlot(target);
631
+ if (!iconSlot) { return; }
632
+ const parentItem = target.parentElement;
633
+ if (!parentItem) { return; }
634
+ if (iconSlot === "secondIcon") {
635
+ const firstIcon = parentItem.querySelector<HTMLElement>("[slot=\"firstIcon\"]");
636
+ firstIcon?.focus();
637
+ } else if (iconSlot === "firstIcon") {
638
+ (parentItem as UDExListItemBase).focus();
639
+ }
640
+ }
641
+
642
+ _handleListKeyDownCapture(e: KeyboardEvent): void {
643
+ const target = e.target as HTMLElement;
644
+ if (!this._getIconButtonSlot(target)) { return; }
645
+ if (!isDown(e) && !isUp(e)) { return; }
646
+
647
+ const items = this._list?.getKeyboardNavigationItems();
648
+ if (!items?.length) { return; }
649
+
650
+ e.stopImmediatePropagation();
651
+ e.preventDefault();
652
+
653
+ const parentItem = target.parentElement as UDExListItemBase | null;
654
+ if (!parentItem) { return; }
655
+
656
+ const currentIndex = this._list.getCurrentIndex(items, parentItem);
657
+ if (currentIndex < 0) { return; }
658
+
659
+ const lastItemIndex = items.length - 1;
660
+ let targetIndex: number;
661
+
662
+ if (isDown(e)) {
663
+ const nextIndex = currentIndex + 1 > lastItemIndex ? 0 : currentIndex + 1;
664
+ const isNonInteractive = items[nextIndex].nonInteractive && !items[nextIndex].collapsible;
665
+ const wrappedNext = nextIndex + 1 > lastItemIndex ? 0 : nextIndex + 1;
666
+ targetIndex = isNonInteractive ? wrappedNext : nextIndex;
667
+ } else {
668
+ const prevIndex = currentIndex - 1 < 0 ? lastItemIndex : currentIndex - 1;
669
+ const isNonInteractive = items[prevIndex].nonInteractive && !items[prevIndex].collapsible;
670
+ const wrappedPrev = prevIndex === 0 ? lastItemIndex : prevIndex - 1;
671
+ targetIndex = isNonInteractive ? wrappedPrev : prevIndex;
672
+ }
673
+
674
+ this._list.unsetTabIndexByIndex(items, currentIndex);
675
+ this._list.setTabIndexByIndex(items, targetIndex);
676
+ items[targetIndex].focus();
677
+ this._selectedOption = items[targetIndex].label;
678
+ }
679
+
680
+ _focusLastListItem() {
681
+ const items = this._list?.getKeyboardNavigationItems();
682
+ if (!items || items.length === 0) {
683
+ this._search.focus();
684
+ return;
685
+ }
686
+ const lastIndex = items.length - 1;
687
+ const lastItem = items[lastIndex];
688
+ this._list.setTabIndexByIndex(items, lastIndex);
689
+ this._selectedOption = lastItem.label;
690
+ if (lastItem.isGroup) {
691
+ this._list.focusHeader(lastItem);
692
+ } else {
693
+ lastItem.focus();
694
+ }
695
+ }
696
+
697
+ _firstFocusableIn(el: Element | undefined): HTMLElement | null {
698
+ if (!(el instanceof HTMLElement)) { return null; }
699
+ if (el.tabIndex >= 0 && !el.hidden && !(el as HTMLElement & { disabled?: boolean }).disabled && (!!el.offsetParent || el.matches("body"))) {
700
+ return el;
701
+ }
702
+ return Array.from(el.children).reduce<HTMLElement | null>(
703
+ (found, child) => found ?? this._firstFocusableIn(child), null,
704
+ );
705
+ }
706
+
707
+ _moveFocusAfterComponent() {
708
+ const findAndFocus = (from: Element) => {
709
+ let node: Element | null = from;
710
+ while (node && node !== document.documentElement) {
711
+ let sibling = node.nextElementSibling;
712
+ while (sibling) {
713
+ const focusable = this._firstFocusableIn(sibling);
714
+ if (focusable) {
715
+ focusable.focus();
716
+ return;
717
+ }
718
+ sibling = sibling.nextElementSibling;
719
+ }
720
+ node = node.parentElement;
721
+ }
722
+ };
723
+
724
+ findAndFocus(this);
725
+ }
726
+
540
727
  get ariaDescribedbyText(): string {
541
728
  if (this.modeSingleSelect) {
542
729
  return this._selectedOption;
package/src/SelectBox.ts CHANGED
@@ -211,9 +211,10 @@ class UDExSelectBox extends UDExSelectBase {
211
211
  handleInputFocusOut(e: FocusEvent) {
212
212
  const relatedTarget = e.relatedTarget as HTMLElement;
213
213
  const isInsideComponent = relatedTarget?.closest(`.${this._rootClass}`);
214
+ const isFooterElement = this.hasFooter && !!relatedTarget && this.footer.some(el => el === relatedTarget || el.contains(relatedTarget));
214
215
  const hasTags = this.tokenizer?.tokens.length > 0;
215
216
 
216
- if (!isInsideComponent) {
217
+ if (!isInsideComponent && !isFooterElement) {
217
218
  this.closeDropdown();
218
219
  this.resetSearchState();
219
220
 
@@ -95,6 +95,7 @@ export default function SelectBoxTemplate(this: UDExSelectBox) {
95
95
  {this._showTagsDropdownContent
96
96
  ? renderTruncatableTagContent.call(this)
97
97
  : this.searchableList}
98
+ {this.hasFooter && <slot name="footer" slot="footer" />}
98
99
  </UI5Popover>
99
100
  <span aria-live="polite" aria-atomic="true" class="hidden">{this._accSearchResultInfo}</span>
100
101
  </div>
package/src/TextField.ts CHANGED
@@ -280,7 +280,7 @@ class UDExTextField extends UI5Element {
280
280
  */
281
281
 
282
282
  @property({ type: String })
283
- showButtonTooltip: string = "";
283
+ buttonTooltip: string = "";
284
284
 
285
285
  /**
286
286
  * Defines the inner stored class of the label.
@@ -49,7 +49,7 @@ export default function TextFieldTemplate(this: TextField) {
49
49
  part="textfield-control-btn"
50
50
  class={`${rootClass}__icon-container`}
51
51
  size="Large"
52
- tooltip={this.showButtonTooltip}
52
+ tooltip={this.buttonTooltip}
53
53
  active={this.active}
54
54
  disabled={this.disabled}
55
55
  readonly={this.readonly}
@@ -10,7 +10,7 @@ interface SuggestionComponent extends UI5Element {
10
10
  open: boolean;
11
11
  value?: string;
12
12
  suggestionItems?: string;
13
- openSuggestionPopup?: boolean;
13
+ openSuggestionPopup: boolean;
14
14
  }
15
15
 
16
16
  type SearchInputSuggestion = {
@@ -32,8 +32,6 @@ class Suggestion {
32
32
  focusabledList?: HTMLDivElement;
33
33
  lists?: Array<HTMLDivElement>;
34
34
  _focusHandler: UDExListWithFirstLetterFocus = new UDExFirstLetterFocusHandler();
35
- _tabEscapedFromPopover: boolean = false;
36
- _shiftTabEscapedFromPopover: boolean = false;
37
35
 
38
36
  constructor(component: SuggestionComponent) {
39
37
  this.component = component;
@@ -76,22 +74,15 @@ class Suggestion {
76
74
  this._focusHandler.focusMatchingListItem(key);
77
75
  }
78
76
  }
79
- initLists(): void {
80
- const list = this.popover?.querySelectorAll(".udex-search-suggestions__list") ? this.popover?.querySelectorAll<HTMLDivElement>(".udex-search-suggestions__list") : [];
81
- const newLists = Array.from(list);
82
-
83
- if (this.focusabledList && this.lists?.length && newLists.length) {
84
- const oldIndex = this.lists.indexOf(this.focusabledList);
85
- if (oldIndex >= 0 && oldIndex < newLists.length) {
86
- this.focusabledList = newLists[oldIndex];
87
- }
88
- }
89
77
 
90
- this.lists = newLists;
78
+ initLists(): void {
79
+ const list = this.popover?.querySelectorAll<HTMLDivElement>(".udex-search-suggestions__list") ?? [];
80
+ this.lists = Array.from(list);
91
81
  }
92
82
 
93
83
  close(): void {
94
84
  this._getSuggestionPopover();
85
+ this.getAllFocusableItems().forEach(item => item.removeAttribute("aria-selected"));
95
86
  this._getComponent().open = false;
96
87
  this.popover!.open = false;
97
88
  this.selectedItemIndex = -1;
@@ -114,192 +105,79 @@ class Suggestion {
114
105
  }
115
106
  }
116
107
 
117
- onTab(e: KeyboardEvent) {
118
- if (this._tabEscapedFromPopover) {
119
- this._tabEscapedFromPopover = false;
120
- return;
121
- }
122
- if (this.lists?.length && this._getComponent().open) {
123
- this._shiftTabEscapedFromPopover = false;
124
- e.preventDefault();
125
- this.focusabledList = this.lists[0];
126
- this.selectedItemIndex = -1;
127
- this.focusabledList.focus();
128
- }
108
+ getAllFocusableItems(): Element[] {
109
+ return Array.from(this.popover?.querySelectorAll<Button>("[ui5-button]") ?? []);
129
110
  }
130
111
 
131
- getFocusableItems(): Element[] {
132
- const items = this.focusabledList?.children ? this.focusabledList?.querySelectorAll<Button>("[ui5-button]") : [];
133
- return Array.from(items);
134
- }
135
-
136
- onShiftTab(e: KeyboardEvent): void {
137
- if (this._shiftTabEscapedFromPopover) {
138
- this._shiftTabEscapedFromPopover = false;
139
- return;
140
- }
141
- const keepOpen = !!this.component.openSuggestionPopup;
142
- if (keepOpen && this.lists?.length && this._getComponent().open) {
143
- this._tabEscapedFromPopover = false;
144
- e.preventDefault();
145
- this.focusabledList = this.lists[this.lists.length - 1];
146
- this.focusabledList.focus();
147
- this.selectedItemIndex = -1;
112
+ private _updateItemSelection(items: Element[], nextIndex: number): void {
113
+ if (this.selectedItemIndex >= 0 && this.selectedItemIndex < items.length) {
114
+ items[this.selectedItemIndex].removeAttribute("aria-selected");
148
115
  }
116
+ const button = items[nextIndex] as UI5Element;
117
+ button.setAttribute("aria-selected", "true");
118
+ button.focus();
119
+ this.selectedItemIndex = nextIndex;
149
120
  }
150
121
 
151
- onEscape(e: KeyboardEvent) {
122
+ onKeyDown(e: KeyboardEvent): void {
152
123
  e.preventDefault();
153
- this.close();
154
- this.onFocusInput();
155
- }
124
+ const items = this.getAllFocusableItems();
125
+ if (!items.length) {
126
+ return;
127
+ }
156
128
 
157
- onKeyTabList(e: KeyboardEvent) {
158
- const keepOpen = !!this.component.openSuggestionPopup;
129
+ const nextIndex = this.selectedItemIndex < 0 || this.selectedItemIndex >= items.length - 1
130
+ ? 0
131
+ : this.selectedItemIndex + 1;
159
132
 
160
- if (this.lists?.length === 2) {
161
- if (this.focusabledList === this.lists[1]) {
162
- e.preventDefault();
163
- if (keepOpen) {
164
- this.focusabledList = undefined;
165
- this.selectedItemIndex = -1;
166
- this._tabEscapedFromPopover = true;
167
- this.onFocusInput();
168
- return;
169
- }
170
- this.close();
171
- this.onFocusInput();
172
- return;
173
- }
174
- if (this.focusabledList === this.lists[0]) {
175
- e.preventDefault();
176
- this.focusabledList = this.lists[1];
177
- this.focusabledList.focus();
178
- this.selectedItemIndex = -1;
179
- }
180
- }
181
- if (this.lists?.length === 1) {
182
- e.preventDefault();
183
- if (keepOpen) {
184
- this.focusabledList = undefined;
185
- this.selectedItemIndex = -1;
186
- this._tabEscapedFromPopover = true;
187
- this.onFocusInput();
188
- return;
189
- }
190
- this.close();
191
- this.onFocusInput();
192
- }
133
+ this._updateItemSelection(items, nextIndex);
193
134
  }
194
135
 
195
- onPrevKeyTabList(e: KeyboardEvent) {
196
- const keepOpen = !!this.component.openSuggestionPopup;
136
+ onKeyUp(e: KeyboardEvent): void {
197
137
  e.preventDefault();
198
- if (this.lists?.length === 2) {
199
- if (this.focusabledList === this.lists[0]) {
200
- if (keepOpen) {
201
- this.focusabledList = undefined;
202
- this.selectedItemIndex = -1;
203
- this._shiftTabEscapedFromPopover = true;
204
- }
205
- this.onFocusInput();
206
- return;
207
- }
208
- if (this.focusabledList === this.lists[1]) {
209
- this.focusabledList = this.lists[0];
210
- this.focusabledList.focus();
211
- this.selectedItemIndex = -1;
212
- }
213
- }
214
- if (this.lists?.length === 1) {
215
- if (keepOpen) {
216
- this.focusabledList = undefined;
217
- this.selectedItemIndex = -1;
218
- this._shiftTabEscapedFromPopover = true;
219
- }
220
- this.onFocusInput();
138
+ const items = this.getAllFocusableItems();
139
+ if (!items.length) {
140
+ return;
221
141
  }
222
- }
223
142
 
224
- onKeyDown(e: KeyboardEvent) {
225
- e.preventDefault();
226
- const nextSelectedIdx = this.selectedItemIndex;
227
- const focusabledItems = this.getFocusableItems();
228
- const itemCount = focusabledItems.length - 1;
143
+ const prevIndex = this.selectedItemIndex <= 0
144
+ ? items.length - 1
145
+ : this.selectedItemIndex - 1;
229
146
 
230
- if (this.lists?.length) {
231
- if (nextSelectedIdx === -1) {
232
- const button = focusabledItems[0] as UI5Element;
233
-
234
- button.setAttribute("aria-selected", "true");
235
- button.focus();
236
- this.selectedItemIndex = 0;
237
- this.focusabledList?.setAttribute("aria-activedescendant", button.id);
238
- return true;
239
- }
240
- if (nextSelectedIdx >= 0 && nextSelectedIdx !== itemCount) {
241
- const button = focusabledItems[nextSelectedIdx + 1] as UI5Element;
147
+ this._updateItemSelection(items, prevIndex);
148
+ }
242
149
 
243
- focusabledItems[nextSelectedIdx].removeAttribute("aria-selected");
244
- button.setAttribute("aria-selected", "true");
245
- button.focus();
246
- this.selectedItemIndex = nextSelectedIdx + 1;
247
- this.focusabledList?.setAttribute("aria-activedescendant", button.id);
248
- return true;
249
- }
250
- if (nextSelectedIdx === itemCount && this.focusabledList === this.lists[0] && this.lists.length === 2) {
251
- focusabledItems[nextSelectedIdx].removeAttribute("aria-selected");
252
- this.focusabledList?.removeAttribute("aria-activedescendant");
253
- this.focusabledList = this.lists[1];
254
- this.focusabledList.focus();
255
- this.selectedItemIndex = -1;
256
- return true;
257
- }
150
+ onTab(): void {
151
+ if (!this._getComponent().openSuggestionPopup) {
152
+ this.close();
258
153
  }
154
+ this.resetFocusState();
259
155
  }
260
156
 
261
- onKeyUp(e: KeyboardEvent) {
157
+ onEscape(e: KeyboardEvent): void {
262
158
  e.preventDefault();
263
- const focusabledItems = this.getFocusableItems();
264
- const itemCount = focusabledItems.length - 1;
265
- const previousSelectedIdx = this.selectedItemIndex;
266
-
267
- if (this.lists?.length) {
268
- if (previousSelectedIdx === -1) {
269
- const button = focusabledItems[0] as UI5Element;
270
-
271
- button.setAttribute("aria-selected", "true");
272
- button.focus();
273
- this.selectedItemIndex = 0;
274
- this.focusabledList?.setAttribute("aria-activedescendant", button.id);
275
- return true;
276
- }
277
- if (previousSelectedIdx > 0 && previousSelectedIdx <= itemCount) {
278
- const button = focusabledItems[previousSelectedIdx - 1] as UI5Element;
159
+ this.close();
160
+ this.onFocusInput();
161
+ }
279
162
 
280
- focusabledItems[previousSelectedIdx].removeAttribute("aria-selected");
281
- button.setAttribute("aria-selected", "true");
282
- button.focus();
283
- this.selectedItemIndex = previousSelectedIdx - 1;
284
- this.focusabledList?.setAttribute("aria-activedescendant", button.id);
285
- return true;
286
- }
287
- if (previousSelectedIdx === 0 && this.lists.length === 2 && this.focusabledList === this.lists[1]) {
288
- focusabledItems[previousSelectedIdx].removeAttribute("aria-selected");
289
- this.focusabledList?.removeAttribute("aria-activedescendant");
290
- this.focusabledList = this.lists[0];
291
- this.focusabledList.focus();
292
- this.selectedItemIndex = -1;
293
- return true;
294
- }
163
+ onKeyTabList(e: KeyboardEvent): void {
164
+ e.preventDefault();
165
+ this.close();
166
+ this.resetFocusState();
167
+ const shadow = this._getComponent().shadowRoot;
168
+ const firstBtn = shadow?.querySelector<HTMLElement>("#reset-button, #voice-button, #submit-button");
169
+ if (firstBtn) {
170
+ firstBtn.focus();
171
+ } else {
172
+ this.onFocusInput();
295
173
  }
296
174
  }
297
175
 
298
- handlePrevTabReset(e: KeyboardEvent) {
299
- if (this.focusabledList) {
300
- e.preventDefault();
301
- this.focusabledList.focus();
302
- }
176
+ onPrevKeyTabList(e: KeyboardEvent): void {
177
+ e.preventDefault();
178
+ this.close();
179
+ this.resetFocusState();
180
+ this.onFocusInput();
303
181
  }
304
182
 
305
183
  static get dependencies() {