@sapudex/web-components 2.31.0 → 2.32.1

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 (134) hide show
  1. package/cypress/specs/Breadcrumbs.cy.tsx +26 -0
  2. package/cypress/specs/MediaPlayer.cy.tsx +209 -0
  3. package/cypress/specs/SecondaryNavigationDesktop.cy.tsx +74 -21
  4. package/cypress/specs/SecondaryNavigationMobile.cy.tsx +58 -1
  5. package/cypress/specs/SelectBox.cy.tsx +35 -0
  6. package/cypress/specs/StepInput.cy.tsx +507 -0
  7. package/cypress/specs/TabNavigation.cy.tsx +603 -39
  8. package/cypress/support/component-imports.instrumented.ts +4 -0
  9. package/cypress/support/component-imports.src.ts +7 -3
  10. package/dist/Breadcrumbs.d.ts +5 -0
  11. package/dist/Breadcrumbs.js +8 -1
  12. package/dist/Breadcrumbs.js.map +1 -1
  13. package/dist/HeroBanner.d.ts +0 -3
  14. package/dist/HeroBanner.js +0 -5
  15. package/dist/HeroBanner.js.map +1 -1
  16. package/dist/HeroBannerTemplate.d.ts +2 -0
  17. package/dist/HeroBannerTemplate.js +6 -1
  18. package/dist/HeroBannerTemplate.js.map +1 -1
  19. package/dist/MastheadItem.js +5 -4
  20. package/dist/MastheadItem.js.map +1 -1
  21. package/dist/MediaPlayer.d.ts +15 -0
  22. package/dist/MediaPlayer.js +28 -0
  23. package/dist/MediaPlayer.js.map +1 -1
  24. package/dist/SecondaryNavigation.d.ts +12 -2
  25. package/dist/SecondaryNavigation.js +59 -8
  26. package/dist/SecondaryNavigation.js.map +1 -1
  27. package/dist/SecondaryNavigationItem.d.ts +0 -1
  28. package/dist/SecondaryNavigationItem.js +0 -4
  29. package/dist/SecondaryNavigationItem.js.map +1 -1
  30. package/dist/SecondaryNavigationSubItem.js +1 -1
  31. package/dist/SecondaryNavigationSubItem.js.map +1 -1
  32. package/dist/SelectBase.js +2 -1
  33. package/dist/SelectBase.js.map +1 -1
  34. package/dist/StepInput.d.ts +3 -0
  35. package/dist/StepInput.js +17 -0
  36. package/dist/StepInput.js.map +1 -1
  37. package/dist/StepInputTemplate.js +1 -1
  38. package/dist/StepInputTemplate.js.map +1 -1
  39. package/dist/Tab.d.ts +1 -1
  40. package/dist/Tab.js +3 -3
  41. package/dist/Tab.js.map +1 -1
  42. package/dist/TabNavigation.d.ts +64 -0
  43. package/dist/TabNavigation.js +124 -1
  44. package/dist/TabNavigation.js.map +1 -1
  45. package/dist/assets/{InputSuggestions-i2kFpCfC.js → InputSuggestions-DeXhpK0F.js} +1 -1
  46. package/dist/assets/{ListItemStandardExpandableTextTemplate-C1ceDaxu.js → ListItemStandardExpandableTextTemplate-jMEdrC07.js} +1 -1
  47. package/dist/assets/{bundle.esm-DQ3cxywT.js → bundle.esm-CwJAt93x.js} +219 -219
  48. package/dist/assets/messagebundle_en-iNqRB1VZ.js +1 -0
  49. package/dist/css/themes/BreadcrumbsPopover.css +1 -1
  50. package/dist/css/themes/FilterMenu.css +1 -1
  51. package/dist/css/themes/HeroBanner.css +1 -1
  52. package/dist/css/themes/Modal.css +1 -1
  53. package/dist/css/themes/RatingIndicator.css +1 -1
  54. package/dist/css/themes/SecondaryNavigationItem.css +1 -1
  55. package/dist/css/themes/Slider.css +1 -1
  56. package/dist/css/themes/TabNavigation.css +1 -1
  57. package/dist/custom-elements-internal.json +221 -0
  58. package/dist/custom-elements.json +149 -0
  59. package/dist/generated/assets/i18n/messagebundle_en.json +1 -1
  60. package/dist/generated/i18n/i18n-defaults.js +1 -1
  61. package/dist/generated/i18n/i18n-defaults.js.map +1 -1
  62. package/dist/generated/templates/SecondaryNavigationTemplate.lit.js +1 -1
  63. package/dist/generated/templates/SecondaryNavigationTemplate.lit.js.map +1 -1
  64. package/dist/generated/templates/TabNavigationTemplate.lit.js +10 -8
  65. package/dist/generated/templates/TabNavigationTemplate.lit.js.map +1 -1
  66. package/dist/generated/themes/BreadcrumbsPopover.css.d.ts +1 -1
  67. package/dist/generated/themes/BreadcrumbsPopover.css.js +1 -1
  68. package/dist/generated/themes/BreadcrumbsPopover.css.js.map +1 -1
  69. package/dist/generated/themes/FilterMenu.css.d.ts +1 -1
  70. package/dist/generated/themes/FilterMenu.css.js +1 -1
  71. package/dist/generated/themes/FilterMenu.css.js.map +1 -1
  72. package/dist/generated/themes/HeroBanner.css.d.ts +1 -1
  73. package/dist/generated/themes/HeroBanner.css.js +1 -1
  74. package/dist/generated/themes/HeroBanner.css.js.map +1 -1
  75. package/dist/generated/themes/Modal.css.d.ts +1 -1
  76. package/dist/generated/themes/Modal.css.js +1 -1
  77. package/dist/generated/themes/Modal.css.js.map +1 -1
  78. package/dist/generated/themes/RatingIndicator.css.d.ts +1 -1
  79. package/dist/generated/themes/RatingIndicator.css.js +1 -1
  80. package/dist/generated/themes/RatingIndicator.css.js.map +1 -1
  81. package/dist/generated/themes/SecondaryNavigationItem.css.d.ts +1 -1
  82. package/dist/generated/themes/SecondaryNavigationItem.css.js +1 -1
  83. package/dist/generated/themes/SecondaryNavigationItem.css.js.map +1 -1
  84. package/dist/generated/themes/Slider.css.d.ts +1 -1
  85. package/dist/generated/themes/Slider.css.js +1 -1
  86. package/dist/generated/themes/Slider.css.js.map +1 -1
  87. package/dist/generated/themes/TabNavigation.css.d.ts +1 -1
  88. package/dist/generated/themes/TabNavigation.css.js +1 -1
  89. package/dist/generated/themes/TabNavigation.css.js.map +1 -1
  90. package/dist/messagebundle.properties +1 -1
  91. package/dist/messagebundle_en.properties +1 -1
  92. package/dist/vscode.html-custom-data.json +23 -3
  93. package/dist/web-types.json +64 -4
  94. package/package.json +1 -1
  95. package/src/Breadcrumbs.ts +9 -1
  96. package/src/HeroBanner.ts +0 -5
  97. package/src/HeroBannerTemplate.tsx +6 -1
  98. package/src/MastheadItem.ts +5 -4
  99. package/src/MediaPlayer.ts +35 -0
  100. package/src/SecondaryNavigation.hbs +1 -0
  101. package/src/SecondaryNavigation.ts +77 -9
  102. package/src/SecondaryNavigationItem.ts +0 -6
  103. package/src/SecondaryNavigationSubItem.ts +1 -1
  104. package/src/SelectBase.ts +2 -1
  105. package/src/StepInput.ts +20 -0
  106. package/src/StepInputTemplate.tsx +1 -1
  107. package/src/Tab.ts +4 -4
  108. package/src/TabNavigation.hbs +15 -0
  109. package/src/TabNavigation.ts +134 -1
  110. package/src/generated/i18n/i18n-defaults.ts +1 -1
  111. package/src/generated/templates/SecondaryNavigationTemplate.lit.ts +1 -1
  112. package/src/generated/templates/TabNavigationTemplate.lit.ts +10 -8
  113. package/src/generated/themes/BreadcrumbsPopover.css.ts +1 -1
  114. package/src/generated/themes/FilterMenu.css.ts +1 -1
  115. package/src/generated/themes/HeroBanner.css.ts +1 -1
  116. package/src/generated/themes/Modal.css.ts +1 -1
  117. package/src/generated/themes/RatingIndicator.css.ts +1 -1
  118. package/src/generated/themes/SecondaryNavigationItem.css.ts +1 -1
  119. package/src/generated/themes/Slider.css.ts +1 -1
  120. package/src/generated/themes/TabNavigation.css.ts +1 -1
  121. package/src/i18n/messagebundle.properties +1 -1
  122. package/src/i18n/messagebundle_en.properties +1 -1
  123. package/src/themes/BreadcrumbsPopover.css +2 -2
  124. package/src/themes/FilterMenu.css +5 -1
  125. package/src/themes/HeroBanner.css +93 -76
  126. package/src/themes/Modal.css +3 -2
  127. package/src/themes/SecondaryNavigationItem.css +4 -1
  128. package/src/themes/TabNavigation.css +27 -16
  129. package/dist/assets/messagebundle_en-DWAgV5VH.js +0 -1
  130. package/dist/generated/templates/HeroBannerTemplate.lit.d.ts +0 -3
  131. package/dist/generated/templates/HeroBannerTemplate.lit.js +0 -15
  132. package/dist/generated/templates/HeroBannerTemplate.lit.js.map +0 -1
  133. package/src/HeroBanner.hbs +0 -46
  134. package/src/generated/templates/HeroBannerTemplate.lit.ts +0 -19
@@ -491,6 +491,7 @@ class UDExMastheadItem extends UI5Element implements IMastheadItem {
491
491
 
492
492
  calculateMarginInlineStart(rect: DOMRect): number | null {
493
493
  const { left, right } = rect;
494
+ const viewportWidth = document.documentElement.clientWidth;
494
495
 
495
496
  if (this.isLtrDirection && left < 0) {
496
497
  return left * -1 + 15;
@@ -500,12 +501,12 @@ class UDExMastheadItem extends UI5Element implements IMastheadItem {
500
501
  return left - 15;
501
502
  }
502
503
 
503
- if (this.isRtlDirection && right > window.innerWidth) {
504
- return right - window.innerWidth + 15;
504
+ if (this.isRtlDirection && right > viewportWidth) {
505
+ return right - viewportWidth + 15;
505
506
  }
506
507
 
507
- if (this.isLtrDirection && right > window.innerWidth) {
508
- return window.innerWidth - right - 15;
508
+ if (this.isLtrDirection && right > viewportWidth) {
509
+ return viewportWidth - right - 15;
509
510
  }
510
511
 
511
512
  return null;
@@ -35,6 +35,10 @@ type UDExMediaPlayerVideoProgressEventDetail = {
35
35
  progress: number,
36
36
  }
37
37
 
38
+ type UDExMediaPlayerVideojsReadyEventDetail = {
39
+ videojs: any,
40
+ }
41
+
38
42
  /**
39
43
  * @class
40
44
  *
@@ -117,6 +121,15 @@ type UDExMediaPlayerVideoProgressEventDetail = {
117
121
  * @public
118
122
  */
119
123
  @event("video75", { bubbles: true })
124
+ /**
125
+ * Fires when the VideoJS library is loaded but before the player is created.
126
+ * Use this event to register custom VideoJS plugins via
127
+ * <code>event.detail.videojs.registerPlugin()</code>.
128
+ *
129
+ * @public
130
+ * @param {object} videojs the VideoJS constructor object
131
+ */
132
+ @event("videojsReady", { bubbles: true })
120
133
  class UDExMediaPlayer extends UI5Element {
121
134
  eventDetails!: {
122
135
  "videoStart": void,
@@ -127,6 +140,7 @@ class UDExMediaPlayer extends UI5Element {
127
140
  "video25": void,
128
141
  "video50": void,
129
142
  "video75": void,
143
+ "videojsReady": UDExMediaPlayerVideojsReadyEventDetail,
130
144
  };
131
145
  /**
132
146
  * Defines the options object of video.js element
@@ -251,6 +265,20 @@ class UDExMediaPlayer extends UI5Element {
251
265
  videoStart!: boolean;
252
266
  videoReady!: boolean;
253
267
  videoProgress!: number;
268
+ private _pendingPlugins: Array<{ name: string; plugin: (...args: any[]) => any }> = [];
269
+
270
+ /**
271
+ * Registers a custom VideoJS plugin to be applied before player initialization.
272
+ * Must be called before the component renders.
273
+ * For plugins registered after rendering, use the videojsReady event instead.
274
+ *
275
+ * @param name - The plugin name
276
+ * @param plugin - The plugin function or class
277
+ * @public
278
+ */
279
+ registerPlugin(name: string, plugin: (...args: any[]) => any): void {
280
+ this._pendingPlugins.push({ name, plugin });
281
+ }
254
282
 
255
283
  getPlayedTime = (target: EventTarget): number => {
256
284
  const progressControl = (target as any)?.player.controlBar?.progressControl?.childNameIndex_.seekBar;
@@ -518,6 +546,13 @@ class UDExMediaPlayer extends UI5Element {
518
546
  window.videojs = videojs;
519
547
  }
520
548
 
549
+ this._pendingPlugins.forEach(({ name, plugin }) => {
550
+ videojs.registerPlugin(name, plugin);
551
+ });
552
+ this._pendingPlugins = [];
553
+
554
+ this.fireDecoratorEvent("videojsReady", { videojs });
555
+
521
556
  const options = this.getOptionsValue();
522
557
  const language: string = options?.language;
523
558
  if (language) {
@@ -70,6 +70,7 @@
70
70
  aria-label="{{mobileMenuOpenerAriaLabel}}"
71
71
  label="{{heading}}"
72
72
  mobile-link-title="{{title}}"
73
+ @ui5-nav-item-click="{{handleMobileMenuOpenerClick}}"
73
74
  @ui5-sub-item-click="{{handleSubItemClick}}"
74
75
  _is-mobile-menu-opener
75
76
  .subItems="{{this.parsedItems}}">
@@ -34,8 +34,10 @@ type UDExSecondaryNavigationClickEventDetail = {
34
34
  originalEvent: Event,
35
35
  clickedItem: NavItem;
36
36
  clickedIndex: number;
37
+ isMore?: boolean,
37
38
  isMobile?: boolean,
38
39
  analyticsName: AnalyticsName,
40
+ analyticsPath: Array<string>,
39
41
  sourceNode: HTMLElement,
40
42
  };
41
43
 
@@ -45,6 +47,12 @@ type UDExSecondaryNavigationHeadingClickEventDetail = {
45
47
  sourceNode: HTMLElement,
46
48
  };
47
49
 
50
+ type UDExSecondaryNavigationMobileMenuOpenerClickEventDetail = {
51
+ originalEvent: Event,
52
+ analyticsName: AnalyticsName,
53
+ sourceNode: HTMLElement,
54
+ };
55
+
48
56
  /**
49
57
  * @class
50
58
  *
@@ -97,10 +105,23 @@ type UDExSecondaryNavigationHeadingClickEventDetail = {
97
105
  */
98
106
  @event("heading-click", { cancelable: true, bubbles: true })
99
107
 
108
+ /**
109
+ * The event is triggered on user interaction with the mobile menu opener
110
+ *
111
+ * @allowPreventDefault
112
+ * @public
113
+ * @param {Event} originalEvent The original event object
114
+ * @param {AnalyticsName} analyticsName The analytics name for the mobile menu opener
115
+ * @param {HTMLElement} sourceNode The source node of the mobile menu opener
116
+ *
117
+ */
118
+ @event("mobile-menu-opener-click", { cancelable: true, bubbles: true })
119
+
100
120
  class UDExSecondaryNavigation extends UI5Element {
101
121
  eventDetails!: {
102
122
  "item-click": UDExSecondaryNavigationClickEventDetail,
103
123
  "heading-click": UDExSecondaryNavigationHeadingClickEventDetail,
124
+ "mobile-menu-opener-click": UDExSecondaryNavigationMobileMenuOpenerClickEventDetail,
104
125
  };
105
126
  /**
106
127
  * Defines an aria label for the mobile menu opener
@@ -247,7 +268,7 @@ class UDExSecondaryNavigation extends UI5Element {
247
268
  });
248
269
 
249
270
  if ((steps.at(-1) || 0) <= availableWidth) {
250
- this._visibleItems.splice(0, Infinity, ...this.parsedItems);
271
+ this._visibleItems = [...this.parsedItems];
251
272
  this._showMoreButton = false;
252
273
  this.refreshRendering();
253
274
  return;
@@ -256,8 +277,8 @@ class UDExSecondaryNavigation extends UI5Element {
256
277
  if (this.parsedItems) {
257
278
  // @ts-ignore
258
279
  const lastVisibleIndex: number = steps.findLastIndex((size: number) => size < (availableWidth - moreButtonWidth - itemsGap));
259
- this._visibleItems.splice(0, Infinity, ...this.parsedItems.slice(0, lastVisibleIndex + 1));
260
- this._overflowItems.splice(0, Infinity, ...this.parsedItems.slice(lastVisibleIndex + 1));
280
+ this._visibleItems = this.parsedItems.slice(0, lastVisibleIndex + 1);
281
+ this._overflowItems = this.parsedItems.slice(lastVisibleIndex + 1);
261
282
  this._showMoreButton = true;
262
283
  this.refreshRendering();
263
284
  }
@@ -345,7 +366,7 @@ class UDExSecondaryNavigation extends UI5Element {
345
366
  active,
346
367
  mobileLinkTitle,
347
368
  items: subItems,
348
- });
369
+ }, this.getAnalyticsPath(title, path), title === this.moreButtonText);
349
370
 
350
371
  if (prevented) {
351
372
  e.preventDefault();
@@ -366,7 +387,9 @@ class UDExSecondaryNavigation extends UI5Element {
366
387
  const { originalEvent: actualOriginalEvent, index } = e.detail;
367
388
  const item = e.detail.item as UDExSecondaryNavigationSubItem;
368
389
  const { items } = item as NavItem;
369
- const prevented = !this.fireEventItemClick(actualOriginalEvent, index, item);
390
+ const analyticsPath = this.getAnalyticsPath(item.title, item.path);
391
+ const isMore = this._clickedNavItem?.title === this.moreButtonText;
392
+ const prevented = !this.fireEventItemClick(actualOriginalEvent, index, item, analyticsPath, isMore);
370
393
 
371
394
  if (prevented) {
372
395
  e.preventDefault();
@@ -397,10 +420,14 @@ class UDExSecondaryNavigation extends UI5Element {
397
420
  {
398
421
  items, target, path, title, active, mobileLinkTitle,
399
422
  }: NavItem,
423
+ analyticsPath: Array<string> = [],
424
+ isMore: boolean = false,
400
425
  ): boolean {
401
426
  return this.fireDecoratorEvent("item-click", {
402
427
  ...(this.isMobileScreen && { isMobile: true }),
428
+ ...(isMore && { isMore: true }),
403
429
  analyticsName: AnalyticsName.HeaderSub,
430
+ analyticsPath,
404
431
  sourceNode: e?.currentTarget as HTMLElement,
405
432
  originalEvent: e,
406
433
  clickedItem: {
@@ -415,25 +442,62 @@ class UDExSecondaryNavigation extends UI5Element {
415
442
  });
416
443
  }
417
444
 
445
+ getAnalyticsPath(clickedTitle: string, clickedPath?: string): Array<string> {
446
+ const getLabel = (item: NavItem) => (this.isMobileScreen ? (item.mobileLinkTitle || item.title) : item.title);
447
+ const findPath = (items: Array<NavItem>, parentPath: Array<string>): Array<string> | null => items.reduce<Array<string> | null>((found, item) => {
448
+ if (found !== null) {
449
+ return found;
450
+ }
451
+ if (item.title === clickedTitle && (!clickedPath || item.path === clickedPath)) {
452
+ return [...parentPath, getLabel(item)];
453
+ }
454
+ if (item.items?.length) {
455
+ return findPath(item.items, [...parentPath, getLabel(item)]);
456
+ }
457
+ return null;
458
+ }, null);
459
+ return findPath(this.parsedItems, []) || [];
460
+ }
461
+
418
462
  setActiveItem(navItems: Array<NavItem>, clickedItem: UDExSecondaryNavigationItemBase): void {
419
- const updateNav = (items: Array<NavItem>, { title, path }: UDExSecondaryNavigationItemBase) => {
463
+ const updateNav = (items: Array<NavItem>, { title, path }: UDExSecondaryNavigationItemBase): boolean => {
464
+ let hasActiveDescendant = false;
465
+
420
466
  items.forEach(item => {
421
467
  if (item.title === title && (item.path === path || !item.path)) {
422
468
  item.active = true;
469
+ hasActiveDescendant = true;
423
470
  return;
424
471
  }
425
- delete item.active;
426
472
 
427
473
  if (item.items) {
428
- updateNav(item.items, clickedItem);
474
+ const childIsActive = updateNav(item.items, clickedItem);
475
+ if (childIsActive) {
476
+ item.active = true;
477
+ hasActiveDescendant = true;
478
+ } else {
479
+ delete item.active;
480
+ }
481
+ } else {
482
+ delete item.active;
429
483
  }
430
484
  });
485
+
486
+ return hasActiveDescendant;
431
487
  };
432
488
 
433
489
  updateNav(navItems, clickedItem);
434
490
  this.navItems = JSON.stringify(navItems);
435
491
  }
436
492
 
493
+ handleMobileMenuOpenerClick(e: CustomEvent): void {
494
+ this.fireDecoratorEvent("mobile-menu-opener-click", {
495
+ analyticsName: AnalyticsName.HeaderSub,
496
+ sourceNode: e.target as HTMLElement,
497
+ originalEvent: e,
498
+ });
499
+ }
500
+
437
501
  handleOutsideClick(): void {
438
502
  document.addEventListener("mousedown", this.outsideClickListener);
439
503
  }
@@ -545,4 +609,8 @@ class UDExSecondaryNavigation extends UI5Element {
545
609
  UDExSecondaryNavigation.define();
546
610
 
547
611
  export default UDExSecondaryNavigation;
548
- export type { UDExSecondaryNavigationClickEventDetail, UDExSecondaryNavigationHeadingClickEventDetail };
612
+ export type {
613
+ UDExSecondaryNavigationClickEventDetail,
614
+ UDExSecondaryNavigationHeadingClickEventDetail,
615
+ UDExSecondaryNavigationMobileMenuOpenerClickEventDetail,
616
+ };
@@ -166,7 +166,6 @@ class UDExSecondaryNavigationItem extends UDExSecondaryNavigationItemBase {
166
166
  _dropdownHistory: Array<Array<NavItem>> = [];
167
167
  _shouldFocusFirstSubItem!: boolean;
168
168
  _subTitle!: string | undefined;
169
- _shouldResetTabIndex!: boolean;
170
169
  _shouldSetTabIndex!: boolean;
171
170
 
172
171
  onAfterRendering(): void {
@@ -187,11 +186,6 @@ class UDExSecondaryNavigationItem extends UDExSecondaryNavigationItemBase {
187
186
  this._tabIndex = "0";
188
187
  this._shouldSetTabIndex = false;
189
188
  }
190
-
191
- if (this._shouldResetTabIndex) {
192
- this._tabIndex = "-1";
193
- this._shouldResetTabIndex = false;
194
- }
195
189
  }
196
190
 
197
191
  onBeforeRendering() : void {
@@ -121,7 +121,7 @@ class UDExSecondaryNavigationSubItem extends UDExSecondaryNavigationItemBase {
121
121
 
122
122
  get backButtonAriaLabel(): string {
123
123
  const label = translate(UDExSecondaryNavigationSubItem, BACK_BUTTON_LABEL);
124
- return `${label} ${this.itemLabel}`;
124
+ return `${this.itemLabel}, ${label}`;
125
125
  }
126
126
  }
127
127
 
package/src/SelectBase.ts CHANGED
@@ -194,7 +194,7 @@ class UDExSelectBase extends UDExSearchableListBase {
194
194
  }
195
195
 
196
196
  onInvalidation({ name }: ChangeInfo): void {
197
- if (name === "default" && this.modeSingleSelect) {
197
+ if (name === "default" && this.modeSingleSelect && !this._inputValue) {
198
198
  this.setSelectOption();
199
199
  }
200
200
  }
@@ -340,6 +340,7 @@ class UDExSelectBase extends UDExSearchableListBase {
340
340
  e.stopPropagation();
341
341
  e.preventDefault();
342
342
  this._selectedOption = "";
343
+ this._inputValue = e.key;
343
344
  this._search.value = e.key;
344
345
  this._search.focus();
345
346
  this.onSearch(this._search.value);
package/src/StepInput.ts CHANGED
@@ -294,6 +294,18 @@ class UDExStepInput extends UI5Element {
294
294
  this._setButtonState();
295
295
  }
296
296
 
297
+ onAfterRendering() {
298
+ this._skipLiveAnnouncement = false;
299
+ }
300
+
301
+ // Plain (non-reactive) flag — set before a re-render when focus is being
302
+ // redirected to the input so the live region stays silent for that render.
303
+ private _skipLiveAnnouncement = false;
304
+
305
+ get _liveRegionText() {
306
+ return this._skipLiveAnnouncement ? "" : this._valuePrecisioned;
307
+ }
308
+
297
309
  get input() {
298
310
  return this.inputOuter.shadowRoot!.querySelector("input")!;
299
311
  }
@@ -405,6 +417,10 @@ class UDExStepInput extends UI5Element {
405
417
  if (!this._incIconInactive) {
406
418
  this._modifyValue(this.step, true);
407
419
  this._previousValue = this.value;
420
+ if (this._incIconDisabled) {
421
+ this._skipLiveAnnouncement = true;
422
+ this.input.focus();
423
+ }
408
424
  }
409
425
  }
410
426
 
@@ -412,6 +428,10 @@ class UDExStepInput extends UI5Element {
412
428
  if (!this._decIconInactive) {
413
429
  this._modifyValue(-this.step, true);
414
430
  this._previousValue = this.value;
431
+ if (this._decIconDisabled) {
432
+ this._skipLiveAnnouncement = true;
433
+ this.input.focus();
434
+ }
415
435
  }
416
436
  }
417
437
 
@@ -60,6 +60,6 @@ export default function StepInputTemplate(this: UDExStepInput) {
60
60
  ></UDExControlButton>
61
61
  </>)
62
62
  }
63
- <div class="hidden" aria-live="polite">{this._valuePrecisioned}</div>
63
+ <div class="hidden" aria-live="polite">{this._liveRegionText}</div>
64
64
  </div>);
65
65
  }
package/src/Tab.ts CHANGED
@@ -299,6 +299,10 @@ class UDExTab extends UI5Element implements ITab {
299
299
  return this.effectiveSelected && this.hasBodyText && this._isVertical;
300
300
  }
301
301
 
302
+ get hasBodyTextOrLink(): boolean {
303
+ return this._isVertical && (this.hasBodyText || this.hasLinkSlot);
304
+ }
305
+
302
306
  get hasLinkSlot(): boolean {
303
307
  return this.link && this.link.length > 0;
304
308
  }
@@ -314,10 +318,6 @@ class UDExTab extends UI5Element implements ITab {
314
318
  return this.link.map(l => l.outerHTML).join("");
315
319
  }
316
320
 
317
- get hasBodyTextOrLink(): boolean {
318
- return this._isVertical && (this.hasBodyText || this.hasLinkSlot);
319
- }
320
-
321
321
  get effectiveSelected(): boolean {
322
322
  return this.selected || this._selected;
323
323
  }
@@ -4,6 +4,16 @@
4
4
  class="udex-tab-navigation__header"
5
5
  part="tab-navigation-header"
6
6
  @keydown="{{handleKeyDown}}">
7
+ {{#if showPlayButton}}
8
+ <udex-button
9
+ class="udex-tab-navigation__play-button"
10
+ design="Tertiary"
11
+ size="Large"
12
+ icon="{{playBtnIcon}}"
13
+ tooltip="{{playBtnTooltipText}}"
14
+ @click="{{onPlayButtonClick}}">
15
+ </udex-button>
16
+ {{/if}}
7
17
  <div
8
18
  id="{{_id}}-header-tabs"
9
19
  class="udex-tab-navigation__header-tabs"
@@ -49,6 +59,11 @@
49
59
  {{#if this._overflowItems.length}}
50
60
  {{tabNavigationPopover}}
51
61
  {{/if}}
62
+ {{#if showFooterSlot}}
63
+ <div class="udex-tab-navigation__footer" part="tab-navigation-footer">
64
+ <slot name="footer"></slot>
65
+ </div>
66
+ {{/if}}
52
67
  </div>
53
68
  <div
54
69
  class="udex-tab-navigation__content"
@@ -18,9 +18,11 @@ import Icon from "@ui5/webcomponents/dist/Icon.js";
18
18
  import "@ui5/webcomponents-icons/dist/navigation-down-arrow.js";
19
19
  import i18n from "@ui5/webcomponents-base/dist/decorators/i18n.js";
20
20
  import type I18nBundle from "@ui5/webcomponents-base/dist/i18nBundle.js";
21
- import { MORE } from "./generated/i18n/i18n-defaults.js";
21
+ import { MORE, PLAY_BUTTON_ACCESSIBLE_NAME, PAUSE_BUTTON_ACCESSIBLE_NAME } from "./generated/i18n/i18n-defaults.js";
22
22
  import { BreakpointUtils } from "./utils/Breakpoints.js";
23
23
  import "@ui5/webcomponents-icons/dist/slim-arrow-right.js";
24
+ import "@ui5/webcomponents-icons/dist/play.js";
25
+ import "@ui5/webcomponents-icons/dist/pause.js";
24
26
  import UDExButton from "./Button.js";
25
27
 
26
28
  // Templates
@@ -49,6 +51,16 @@ export enum UDExTabNavigationOrientation {
49
51
  Vertical = "Vertical",
50
52
  }
51
53
 
54
+ export enum UDExTabNavigationPlacement {
55
+ Start = "Start",
56
+ End = "End",
57
+ }
58
+
59
+ export enum UDExTabNavigationVerticalAlignment {
60
+ Top = "Top",
61
+ Center = "Center",
62
+ }
63
+
52
64
  type UDExTabNavigationClickDetail = {
53
65
  currentPage: number
54
66
  }
@@ -132,6 +144,53 @@ class UDExTabNavigation extends UI5Element {
132
144
  @property()
133
145
  orientation: `${UDExTabNavigationOrientation}` = UDExTabNavigationOrientation.Horizontal;
134
146
 
147
+ /**
148
+ * Defines the placement of the tab strip relative to the content area.
149
+ * Applies to Vertical orientation only.
150
+ *
151
+ * @public
152
+ * @default "Start"
153
+ */
154
+ @property()
155
+ placement: `${UDExTabNavigationPlacement}` = UDExTabNavigationPlacement.Start;
156
+
157
+ /**
158
+ * Defines the vertical alignment of the tab strip relative to the content area.
159
+ * Applies to Vertical orientation only.
160
+ *
161
+ * @public
162
+ * @default "Top"
163
+ */
164
+ @property()
165
+ verticalAlignment: `${UDExTabNavigationVerticalAlignment}` = UDExTabNavigationVerticalAlignment.Top;
166
+
167
+ /**
168
+ * Enables automatic tab switching for Vertical orientation.
169
+ *
170
+ * @public
171
+ * @default false
172
+ */
173
+ @property({ type: Boolean })
174
+ autoPlay: boolean = false;
175
+
176
+ /**
177
+ * Defines the interval in milliseconds between automatic tab switches.
178
+ * Applies only when autoPlay is enabled.
179
+ *
180
+ * @public
181
+ * @default 5000
182
+ */
183
+ @property({ type: Number })
184
+ autoPlayInterval: number = 5000;
185
+
186
+ /**
187
+ * Indicates whether the auto-play is paused.
188
+ *
189
+ * @private
190
+ */
191
+ @property({ type: Boolean, noAttribute: true })
192
+ _isPaused: boolean = false;
193
+
135
194
  /**
136
195
  * Defines the background color of the component.
137
196
  *
@@ -182,6 +241,16 @@ class UDExTabNavigation extends UI5Element {
182
241
  })
183
242
  items!: Array<ITab>;
184
243
 
244
+ /**
245
+ * Defines optional content rendered below the tab list in Vertical orientation.
246
+ * Use to place a button or any action element beneath the tabs.
247
+ *
248
+ * @slot footer
249
+ * @public
250
+ */
251
+ @slot({ type: HTMLElement })
252
+ footer!: Array<HTMLElement>;
253
+
185
254
  @i18n("@sapudex/web-components")
186
255
  static i18nBundle: I18nBundle;
187
256
 
@@ -192,6 +261,7 @@ class UDExTabNavigation extends UI5Element {
192
261
  _isExpanded: boolean = false;
193
262
  _maxTitleLength: number = 25;
194
263
  _maxTitleLengthInSecondLines: number = this._maxTitleLength * 2;
264
+ _autoPlayTimer?: number;
195
265
 
196
266
  constructor() {
197
267
  super();
@@ -217,11 +287,20 @@ class UDExTabNavigation extends UI5Element {
217
287
  if (isDesktop()) {
218
288
  this.setAttribute("desktop", "");
219
289
  }
290
+ if (this.showPlayButton) {
291
+ const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
292
+ if (prefersReducedMotion) {
293
+ this._isPaused = true;
294
+ } else {
295
+ this._startAutoPlay();
296
+ }
297
+ }
220
298
  }
221
299
 
222
300
  onExitDOM(): void {
223
301
  ResizeHandler.deregister(this.getHeader(), this._handleResizeBound);
224
302
  window.removeEventListener("resize", this._handleWindowResizeBound);
303
+ this._stopAutoPlay();
225
304
  }
226
305
 
227
306
  _handleWindowResize() {
@@ -334,6 +413,10 @@ class UDExTabNavigation extends UI5Element {
334
413
  this._selectedTab = this.items[selectedTabIndex] as Tab;
335
414
  this.updateSelectedTab(selectedTabIndex);
336
415
  this.emitClickEvent(selectedTabIndex + 1);
416
+
417
+ if (this.showPlayButton && !this._isPaused) {
418
+ this._startAutoPlay();
419
+ }
337
420
  }
338
421
 
339
422
  getTab(el: HTMLElement | null): Tab | false {
@@ -921,6 +1004,56 @@ class UDExTabNavigation extends UI5Element {
921
1004
  tab._isMobile = isMobile;
922
1005
  });
923
1006
  }
1007
+
1008
+ _startAutoPlay(): void {
1009
+ this._stopAutoPlay();
1010
+ this._isPaused = false;
1011
+ this._autoPlayTimer = window.setInterval(() => {
1012
+ this._advanceTab();
1013
+ }, this.autoPlayInterval);
1014
+ }
1015
+
1016
+ _stopAutoPlay(): void {
1017
+ if (this._autoPlayTimer !== undefined) {
1018
+ window.clearInterval(this._autoPlayTimer);
1019
+ this._autoPlayTimer = undefined;
1020
+ }
1021
+ }
1022
+
1023
+ _advanceTab(): void {
1024
+ const currentIndex = (this.items as Array<Tab>).findIndex(item => item === this._selectedTab);
1025
+ const nextIndex = (currentIndex + 1) % this.items.length;
1026
+ this._selectedTab = this.items[nextIndex] as Tab;
1027
+ this.updateSelectedTab(nextIndex);
1028
+ this.emitClickEvent(nextIndex + 1);
1029
+ }
1030
+
1031
+ onPlayButtonClick(): void {
1032
+ if (this._isPaused) {
1033
+ this._startAutoPlay();
1034
+ } else {
1035
+ this._stopAutoPlay();
1036
+ this._isPaused = true;
1037
+ }
1038
+ }
1039
+
1040
+ get showPlayButton(): boolean {
1041
+ return this.isVertical && this.autoPlay;
1042
+ }
1043
+
1044
+ get showFooterSlot(): boolean {
1045
+ return this.isVertical && this.footer?.length > 0;
1046
+ }
1047
+
1048
+ get playBtnIcon(): string {
1049
+ return this._isPaused ? "play" : "pause";
1050
+ }
1051
+
1052
+ get playBtnTooltipText(): string {
1053
+ return this._isPaused
1054
+ ? translate(UDExTabNavigation, PLAY_BUTTON_ACCESSIBLE_NAME)
1055
+ : translate(UDExTabNavigation, PAUSE_BUTTON_ACCESSIBLE_NAME);
1056
+ }
924
1057
  }
925
1058
 
926
1059
  UDExTabNavigation.define();