@eui/ecl 22.0.0-alpha.52 → 22.0.0-alpha.53

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 (189) hide show
  1. package/assets/i18n-ecl/bg.json +6 -2
  2. package/assets/i18n-ecl/cs.json +6 -2
  3. package/assets/i18n-ecl/da.json +6 -2
  4. package/assets/i18n-ecl/de.json +6 -2
  5. package/assets/i18n-ecl/el.json +6 -2
  6. package/assets/i18n-ecl/en.json +6 -2
  7. package/assets/i18n-ecl/es.json +6 -2
  8. package/assets/i18n-ecl/et.json +6 -2
  9. package/assets/i18n-ecl/fi.json +6 -2
  10. package/assets/i18n-ecl/fr.json +6 -2
  11. package/assets/i18n-ecl/ga.json +6 -2
  12. package/assets/i18n-ecl/hr.json +6 -2
  13. package/assets/i18n-ecl/hu.json +6 -2
  14. package/assets/i18n-ecl/it.json +6 -2
  15. package/assets/i18n-ecl/lt.json +6 -2
  16. package/assets/i18n-ecl/lv.json +6 -2
  17. package/assets/i18n-ecl/mt.json +6 -2
  18. package/assets/i18n-ecl/nl.json +6 -2
  19. package/assets/i18n-ecl/pl.json +6 -2
  20. package/assets/i18n-ecl/pt.json +6 -2
  21. package/assets/i18n-ecl/ro.json +6 -2
  22. package/assets/i18n-ecl/sk.json +6 -2
  23. package/assets/i18n-ecl/sl.json +6 -2
  24. package/assets/i18n-ecl/sv.json +6 -2
  25. package/components/ecl-slider/package.json +4 -0
  26. package/docs/changelog.html +14 -0
  27. package/docs/components/EclAccordionSummaryComponent.html +1 -1
  28. package/docs/components/EclAddToCalendarComponent.html +1 -1
  29. package/docs/components/EclAnimatedNumbersComponent.html +1 -1
  30. package/docs/components/EclAnimatedNumbersItemComponent.html +1 -1
  31. package/docs/components/EclAppComponent.html +1 -1
  32. package/docs/components/EclBannerComponent.html +1 -1
  33. package/docs/components/EclBlockquoteComponent.html +1 -1
  34. package/docs/components/EclBreadcrumbComponent.html +1 -1
  35. package/docs/components/EclBreadcrumbSegmentComponent.html +1 -1
  36. package/docs/components/EclButtonComponent.html +1 -1
  37. package/docs/components/EclCardBodyComponent.html +1 -1
  38. package/docs/components/EclCardComponent.html +1 -1
  39. package/docs/components/EclCarouselComponent.html +182 -898
  40. package/docs/components/EclCarouselItemComponent.html +32 -1
  41. package/docs/components/EclCategoryFilterComponent.html +1 -1
  42. package/docs/components/EclCategoryFilterItemComponent.html +1 -1
  43. package/docs/components/EclCategoryFilterListComponent.html +1 -1
  44. package/docs/components/EclCheckboxHelpComponent.html +1 -1
  45. package/docs/components/EclCheckboxLabelComponent.html +1 -1
  46. package/docs/components/EclContentBlockComponent.html +1 -1
  47. package/docs/components/EclContentItemComponent.html +1 -1
  48. package/docs/components/EclDateBlockComponent.html +1 -1
  49. package/docs/components/EclDescriptionListDefinitionComponent.html +1 -1
  50. package/docs/components/EclExpandableComponent.html +1 -1
  51. package/docs/components/EclFactFiguresComponent.html +1 -1
  52. package/docs/components/EclFactFiguresItemComponent.html +1 -1
  53. package/docs/components/EclFactFiguresViewAllComponent.html +1 -1
  54. package/docs/components/EclFeaturedComponent.html +1 -1
  55. package/docs/components/EclFeaturedItemComponent.html +1 -1
  56. package/docs/components/EclFeaturedItemDescriptionComponent.html +1 -1
  57. package/docs/components/EclFileComponent.html +1 -1
  58. package/docs/components/EclFileItemComponent.html +1 -1
  59. package/docs/components/EclFileItemsComponent.html +1 -1
  60. package/docs/components/EclFileListsComponent.html +1 -1
  61. package/docs/components/EclFormGroupComponent.html +1 -1
  62. package/docs/components/EclFormLabelComponent.html +1 -1
  63. package/docs/components/EclGalleryComponent.html +1 -1
  64. package/docs/components/EclGalleryFooterComponent.html +1 -1
  65. package/docs/components/EclGalleryItemComponent.html +1 -1
  66. package/docs/components/EclHighlightBoxComponent.html +1 -1
  67. package/docs/components/EclHighlightedSearchComponent.html +1 -1
  68. package/docs/components/EclIconComponent.html +1 -10
  69. package/docs/components/EclIndicatorComponent.html +1 -1
  70. package/docs/components/EclInpageNavigationComponent.html +1 -1
  71. package/docs/components/EclInpageNavigationItemComponent.html +1 -1
  72. package/docs/components/EclLayoutWrapperComponent.html +1 -1
  73. package/docs/components/EclListIllustrationComponent.html +1 -1
  74. package/docs/components/EclListIllustrationItemComponent.html +1 -1
  75. package/docs/components/EclLoadingIndicatorComponent.html +1 -1
  76. package/docs/components/EclMediaContainerComponent.html +1 -1
  77. package/docs/components/EclMegaMenuComponent.html +1 -1
  78. package/docs/components/EclMegaMenuFeaturedComponent.html +1 -1
  79. package/docs/components/EclMegaMenuInfoComponent.html +1 -1
  80. package/docs/components/EclMegaMenuItemComponent.html +1 -1
  81. package/docs/components/EclMegaMenuSubitemComponent.html +1 -1
  82. package/docs/components/EclMenuComponent.html +1 -1
  83. package/docs/components/EclMenuItemComponent.html +1 -1
  84. package/docs/components/EclMenuMegaComponent.html +1 -1
  85. package/docs/components/EclMenuMegaItemComponent.html +1 -1
  86. package/docs/components/EclModalBodyComponent.html +1 -1
  87. package/docs/components/EclModalComponent.html +1 -1
  88. package/docs/components/EclModalFooterComponent.html +1 -1
  89. package/docs/components/EclModalHeaderComponent.html +1 -1
  90. package/docs/components/EclMultiselectComponent.html +1 -1
  91. package/docs/components/EclMultiselectDropdownComponent.html +1 -1
  92. package/docs/components/EclMultiselectInputComponent.html +1 -1
  93. package/docs/components/EclMultiselectOptgroupComponent.html +1 -1
  94. package/docs/components/EclMultiselectOptionComponent.html +1 -1
  95. package/docs/components/EclNavigationListComponent.html +1 -1
  96. package/docs/components/EclNavigationListItemComponent.html +1 -1
  97. package/docs/components/EclNewsTickerComponent.html +1 -1
  98. package/docs/components/EclNewsTickerItemComponent.html +1 -1
  99. package/docs/components/EclNotificationComponent.html +1 -1
  100. package/docs/components/EclPageHeaderComponent.html +1 -1
  101. package/docs/components/EclPageHeaderDescriptionContainerComponent.html +1 -1
  102. package/docs/components/EclPageHeaderExpandableComponent.html +1 -1
  103. package/docs/components/EclPageHeaderSectionComponent.html +1 -1
  104. package/docs/components/EclPageHeaderSectionInfoComponent.html +1 -1
  105. package/docs/components/EclPageSummaryComponent.html +1 -1
  106. package/docs/components/EclPaginationComponent.html +1 -1
  107. package/docs/components/EclPaginationItemComponent.html +1 -1
  108. package/docs/components/EclPopoverComponent.html +1 -1
  109. package/docs/components/EclQuizCardComponent.html +113 -44
  110. package/docs/components/EclQuizComponent.html +4 -9
  111. package/docs/components/EclRadioLabelComponent.html +1 -1
  112. package/docs/components/EclRangeBubbleComponent.html +1 -1
  113. package/docs/components/EclRangeValueComponent.html +1 -1
  114. package/docs/components/EclRatingFieldComponent.html +1 -1
  115. package/docs/components/EclSearchFormComponent.html +1 -1
  116. package/docs/components/EclSelectContainerComponent.html +1 -1
  117. package/docs/components/EclSiteFooterComponent.html +1 -1
  118. package/docs/components/EclSiteFooterCoownerComponent.html +1 -1
  119. package/docs/components/EclSiteFooterCoreComponent.html +1 -1
  120. package/docs/components/EclSiteFooterFeedbackComponent.html +1 -1
  121. package/docs/components/EclSiteFooterFixedContentEUComponent.html +1 -1
  122. package/docs/components/EclSiteFooterRowCommonComponent.html +1 -1
  123. package/docs/components/EclSiteHeaderActionComponent.html +1 -1
  124. package/docs/components/EclSiteHeaderBannerTopComponent.html +1 -1
  125. package/docs/components/EclSiteHeaderComponent.html +1 -1
  126. package/docs/components/EclSiteHeaderCustomActionComponent.html +1 -1
  127. package/docs/components/EclSiteHeaderEnvironmentComponent.html +1 -1
  128. package/docs/components/EclSiteHeaderLanguageComponent.html +1 -1
  129. package/docs/components/EclSiteHeaderLanguagePopoverComponent.html +1 -1
  130. package/docs/components/EclSiteHeaderLoginComponent.html +1 -1
  131. package/docs/components/EclSiteHeaderNotificationComponent.html +1 -1
  132. package/docs/components/EclSiteHeaderSearchComponent.html +1 -1
  133. package/docs/components/EclSliderPagerComponent.html +1159 -0
  134. package/docs/components/EclSloganTickerComponent.html +1 -1
  135. package/docs/components/EclSocialMediaFollowComponent.html +1 -1
  136. package/docs/components/EclSocialMediaFollowItemComponent.html +1 -1
  137. package/docs/components/EclSplashPageComponent.html +1 -1
  138. package/docs/components/EclSplashPageLanguageCategoryComponent.html +1 -1
  139. package/docs/components/EclSplashPageLanguageContainerComponent.html +1 -1
  140. package/docs/components/EclSplashPageLanguageLinkComponent.html +1 -1
  141. package/docs/components/EclSpotlightComponent.html +1 -1
  142. package/docs/components/EclSpotlightLinkComponent.html +1 -1
  143. package/docs/components/EclStickyContainerComponent.html +1 -1
  144. package/docs/components/EclStoryCardComponent.html +4 -4
  145. package/docs/components/EclStoryCardGridDetailsComponent.html +57 -2
  146. package/docs/components/EclStoryCardItemComponent.html +77 -2
  147. package/docs/components/EclTabComponent.html +1 -1
  148. package/docs/components/EclTabLabelComponent.html +1 -1
  149. package/docs/components/EclTableSortButtonComponent.html +1 -1
  150. package/docs/components/EclTabsComponent.html +1 -1
  151. package/docs/components/EclTagComponent.html +1 -1
  152. package/docs/components/EclTextMediaComponent.html +1 -1
  153. package/docs/components/EclTimelineComponent.html +1 -1
  154. package/docs/components/EclTimelineItemComponent.html +1 -1
  155. package/docs/components/EclTimelineItemTogglerComponent.html +1 -1
  156. package/docs/js/menu-wc.js +3 -0
  157. package/docs/js/search/search_index.js +2 -2
  158. package/docs/json/EclCarouselComponent.md +8 -4
  159. package/docs/json/EclCarouselItemComponent.md +1 -0
  160. package/docs/json/EclQuizCardComponent.md +6 -3
  161. package/docs/json/EclSliderPagerComponent.md +55 -0
  162. package/docs/json/EclStoryCardGridDetailsComponent.md +1 -0
  163. package/docs/json/EclStoryCardItemComponent.md +1 -0
  164. package/docs/json/documentation.json +1889 -565
  165. package/docs/llms.txt +69 -34
  166. package/docs/miscellaneous/variables.html +132 -20
  167. package/docs/overview.html +1 -1
  168. package/docs/properties.html +1 -1
  169. package/fesm2022/eui-ecl-components-ecl-carousel.mjs +405 -407
  170. package/fesm2022/eui-ecl-components-ecl-carousel.mjs.map +1 -1
  171. package/fesm2022/eui-ecl-components-ecl-menu.mjs +15 -15
  172. package/fesm2022/eui-ecl-components-ecl-menu.mjs.map +1 -1
  173. package/fesm2022/eui-ecl-components-ecl-quiz.mjs +93 -55
  174. package/fesm2022/eui-ecl-components-ecl-quiz.mjs.map +1 -1
  175. package/fesm2022/eui-ecl-components-ecl-slider.mjs +187 -0
  176. package/fesm2022/eui-ecl-components-ecl-slider.mjs.map +1 -0
  177. package/fesm2022/eui-ecl-components-ecl-story-card.mjs +96 -18
  178. package/fesm2022/eui-ecl-components-ecl-story-card.mjs.map +1 -1
  179. package/package.json +18 -12
  180. package/types/eui-ecl-components-ecl-carousel.d.ts +234 -202
  181. package/types/eui-ecl-components-ecl-carousel.d.ts.map +1 -1
  182. package/types/eui-ecl-components-ecl-menu.d.ts +0 -2
  183. package/types/eui-ecl-components-ecl-menu.d.ts.map +1 -1
  184. package/types/eui-ecl-components-ecl-quiz.d.ts +94 -57
  185. package/types/eui-ecl-components-ecl-quiz.d.ts.map +1 -1
  186. package/types/eui-ecl-components-ecl-slider.d.ts +118 -0
  187. package/types/eui-ecl-components-ecl-slider.d.ts.map +1 -0
  188. package/types/eui-ecl-components-ecl-story-card.d.ts +78 -48
  189. package/types/eui-ecl-components-ecl-story-card.d.ts.map +1 -1
@@ -1,10 +1,13 @@
1
1
  import { BreakpointObserver } from '@angular/cdk/layout';
2
2
  import * as i0 from '@angular/core';
3
3
  import { inject, ElementRef, signal, input, booleanAttribute, linkedSignal, output, contentChild, forwardRef, computed, Component, numberAttribute, viewChild, contentChildren, viewChildren, ChangeDetectorRef, Renderer2, DOCUMENT, afterNextRender } from '@angular/core';
4
- import { EclIconComponent } from '@eui/ecl/components/ecl-icon';
4
+ import * as i1 from '@eui/ecl/components/ecl-slider';
5
+ import { EUI_ECL_SLIDER } from '@eui/ecl/components/ecl-slider';
5
6
  import { EclBaseEvent, ECLBaseDirective, EclRtlService, KeyCode } from '@eui/ecl/core';
6
- import { TranslatePipe } from '@ngx-translate/core';
7
- import { Subject, fromEvent, auditTime, takeUntil, switchMap, interval } from 'rxjs';
7
+ import { TranslateService, TranslatePipe } from '@ngx-translate/core';
8
+ import { EmblaCarouselDirective } from 'embla-carousel-angular';
9
+ import Autoplay from 'embla-carousel-autoplay';
10
+ import { Subject, fromEvent, auditTime, takeUntil } from 'rxjs';
8
11
  import { EclBannerComponent } from '@eui/ecl/components/ecl-banner';
9
12
 
10
13
  /**
@@ -61,6 +64,12 @@ class EclCarouselItemComponent extends ECLBaseDirective {
61
64
  */
62
65
  this.isCurrent = linkedSignal(() => this.isCurrentInput(), /* @ts-ignore */
63
66
  ...(ngDevMode ? [{ debugName: "isCurrent" }] : /* istanbul ignore next */ []));
67
+ /**
68
+ * Text shown in the carousel teaser (navigation) button for this slide.
69
+ * Optional; when empty, no teaser label is rendered for the slide.
70
+ */
71
+ this.teaserText = input('', /* @ts-ignore */
72
+ ...(ngDevMode ? [{ debugName: "teaserText" }] : /* istanbul ignore next */ []));
64
73
  /**
65
74
  * Emits an event when the slide is clicked.
66
75
  */
@@ -88,7 +97,7 @@ class EclCarouselItemComponent extends ECLBaseDirective {
88
97
  this.slideClick.emit(new EclSlideEvent(this));
89
98
  }
90
99
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: EclCarouselItemComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
91
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "22.1.2", type: EclCarouselItemComponent, isStandalone: true, selector: "ecl-carousel-item", inputs: { id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, isCurrentInput: { classPropertyName: "isCurrentInput", publicName: "isCurrent", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { slideClick: "slideClick" }, host: { attributes: { "role": "tabpanel" }, listeners: { "click": "onSlideClick()" }, properties: { "attr.aria-label": "ariaLabel()", "attr.aria-roledescription": "\"slide\"", "attr.inert": "!isCurrent() || undefined", "class": "cssClasses()" } }, queries: [{ propertyName: "banner", first: true, predicate: i0.forwardRef(() => EclBannerComponent), descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<ng-content></ng-content>\n", styles: [":host:before{left:0!important;display:none}\n"] }); }
100
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "22.1.2", type: EclCarouselItemComponent, isStandalone: true, selector: "ecl-carousel-item", inputs: { id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, isCurrentInput: { classPropertyName: "isCurrentInput", publicName: "isCurrent", isSignal: true, isRequired: false, transformFunction: null }, teaserText: { classPropertyName: "teaserText", publicName: "teaserText", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { slideClick: "slideClick" }, host: { attributes: { "role": "tabpanel" }, listeners: { "click": "onSlideClick()" }, properties: { "attr.aria-label": "ariaLabel()", "attr.aria-roledescription": "\"slide\"", "attr.inert": "!isCurrent() || undefined", "class": "cssClasses()" } }, queries: [{ propertyName: "banner", first: true, predicate: i0.forwardRef(() => EclBannerComponent), descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<ng-content></ng-content>\n", styles: [":host:before{left:0!important;display:none}\n"] }); }
92
101
  }
93
102
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: EclCarouselItemComponent, decorators: [{
94
103
  type: Component,
@@ -100,22 +109,29 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.2", ngImpor
100
109
  '[class]': 'cssClasses()',
101
110
  '(click)': 'onSlideClick()',
102
111
  }, template: "<ng-content></ng-content>\n", styles: [":host:before{left:0!important;display:none}\n"] }]
103
- }], propDecorators: { id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }], isCurrentInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "isCurrent", required: false }] }], slideClick: [{ type: i0.Output, args: ["slideClick"] }], banner: [{ type: i0.ContentChild, args: [forwardRef(() => EclBannerComponent), { isSignal: true }] }] } });
112
+ }], propDecorators: { id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }], isCurrentInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "isCurrent", required: false }] }], teaserText: [{ type: i0.Input, args: [{ isSignal: true, alias: "teaserText", required: false }] }], slideClick: [{ type: i0.Output, args: ["slideClick"] }], banner: [{ type: i0.ContentChild, args: [forwardRef(() => EclBannerComponent), { isSignal: true }] }] } });
104
113
 
114
+ /**
115
+ * Number of teaser buttons visible at once on the desktop breakpoint.
116
+ */
117
+ const VISIBLE_TEASERS = 4;
105
118
  /**
106
119
  * Carousel (slideshow) component following the ECL design system.
107
120
  * Renders a sequence of `EclCarouselItemComponent` slides with autoplay, keyboard navigation,
108
121
  * responsive breakpoint awareness, and RTL layout support.
109
122
  *
123
+ * The slide engine is powered by Embla (via `embla-carousel-angular`), matching the upstream
124
+ * ECL implementation: an infinite `loop`, the `Autoplay` plugin driving the teaser loading bars,
125
+ * and a slider pager with previous/next/play/pause controls, a counter, and a tablist of teaser
126
+ * buttons (one per slide).
127
+ *
110
128
  * - Selector: `ecl-carousel`.
111
129
  * - Project `ecl-carousel-item` elements as slides (also via `contentChildren`).
112
- * - `ariaLabel` — ARIA label for the carousel landmark; aliased as `aria-label`; default `'Carousel description here'`.
130
+ * - `ariaLabel` — ARIA label for the carousel landmark; aliased as `aria-label`.
113
131
  * - `isAutoplay` — enables automatic slide advancement; boolean attribute.
114
132
  * - `playInterval` — milliseconds between automatic transitions; default `5000`.
133
+ * - `counterLabel` — separator label used by the counter, defaults to `ecl.common.OF`.
115
134
  * - `slide` — emits `EclSlideEvent` when the active slide changes.
116
- * - `isPlaying` / `isPausedForced` — public flags reflecting playback state.
117
- * - `isRtl` — public flag reflecting RTL mode.
118
- * - Navigation controls and play/pause buttons are rendered internally.
119
135
  */
120
136
  class EclCarouselComponent extends ECLBaseDirective {
121
137
  constructor() {
@@ -132,12 +148,18 @@ class EclCarouselComponent extends ECLBaseDirective {
132
148
  * Interval in milliseconds between automatic slide transitions.
133
149
  */
134
150
  this.playInterval = input(5000, { ...(ngDevMode ? { debugName: "playInterval" } : /* istanbul ignore next */ {}), transform: numberAttribute });
151
+ /**
152
+ * Separator label used by the counter, e.g. the "of" in "1 of 5".
153
+ * Defaults to the translated `ecl.common.OF` key.
154
+ */
155
+ this.counterLabel = input('', /* @ts-ignore */
156
+ ...(ngDevMode ? [{ debugName: "counterLabel" }] : /* istanbul ignore next */ []));
135
157
  /**
136
158
  * Event emitted when a slide change occurs.
137
159
  */
138
160
  this.slide = output();
139
161
  /**
140
- * Reference to the main carousel container element.
162
+ * Reference to the main carousel viewport element (the Embla root).
141
163
  */
142
164
  this.carouselContainerElement = viewChild('carouselContainer', /* @ts-ignore */
143
165
  ...(ngDevMode ? [{ debugName: "carouselContainerElement" }] : /* istanbul ignore next */ []));
@@ -147,58 +169,90 @@ class EclCarouselComponent extends ECLBaseDirective {
147
169
  this.slides = contentChildren(forwardRef(() => EclCarouselItemComponent), /* @ts-ignore */
148
170
  ...(ngDevMode ? [{ debugName: "slides" }] : /* istanbul ignore next */ []));
149
171
  /**
150
- * References to the navigation buttons corresponding to each slide.
172
+ * Indicates whether autoplay is currently active.
151
173
  */
152
- this.navigationButtons = viewChildren('currButton', /* @ts-ignore */
153
- ...(ngDevMode ? [{ debugName: "navigationButtons" }] : /* istanbul ignore next */ []));
174
+ this.isPlaying = signal(false, /* @ts-ignore */
175
+ ...(ngDevMode ? [{ debugName: "isPlaying" }] : /* istanbul ignore next */ []));
154
176
  /**
155
- * Reference to the play button element.
177
+ * Indicates whether autoplay was manually paused (via the pause button or
178
+ * on mobile). When paused this way the loading bar is cleared.
156
179
  */
157
- this.playButton = viewChild('playButton', /* @ts-ignore */
158
- ...(ngDevMode ? [{ debugName: "playButton" }] : /* istanbul ignore next */ []));
180
+ this.isPausedForced = signal(false, /* @ts-ignore */
181
+ ...(ngDevMode ? [{ debugName: "isPausedForced" }] : /* istanbul ignore next */ []));
159
182
  /**
160
- * Reference to the pause button element.
183
+ * Indicates whether the layout is currently in RTL mode.
161
184
  */
162
- this.pauseButton = viewChild('pauseButton', /* @ts-ignore */
163
- ...(ngDevMode ? [{ debugName: "pauseButton" }] : /* istanbul ignore next */ []));
185
+ this.isRtl = false;
164
186
  /**
165
- * Reference to the first hidden slide item (used for focus management).
187
+ * Zero-based index of the currently active slide (mirrors Embla's selected snap).
166
188
  */
167
- this.firstHiddenItem = viewChild('firstHiddenItem', /* @ts-ignore */
168
- ...(ngDevMode ? [{ debugName: "firstHiddenItem" }] : /* istanbul ignore next */ []));
189
+ this.activeIndex = 0;
169
190
  /**
170
- * Reference to the last hidden slide item (used for focus management).
191
+ * Start index of the currently visible teaser window (desktop only).
171
192
  */
172
- this.lastHiddenItem = viewChild('lastHiddenItem', /* @ts-ignore */
173
- ...(ngDevMode ? [{ debugName: "lastHiddenItem" }] : /* istanbul ignore next */ []));
193
+ this.teaserWindowStart = 0;
174
194
  /**
175
- * Reference to the slides container element.
195
+ * Indicates whether autoplay is temporarily paused by hover/focus. Unlike a
196
+ * manual pause, the loading bar stays visible and frozen at its current
197
+ * progress (matching ECL), resuming when the pointer/focus leaves.
176
198
  */
177
- this.slidesContainer = viewChild('slidesContainer', /* @ts-ignore */
178
- ...(ngDevMode ? [{ debugName: "slidesContainer" }] : /* istanbul ignore next */ []));
199
+ this.isHoverPaused = signal(false, /* @ts-ignore */
200
+ ...(ngDevMode ? [{ debugName: "isHoverPaused" }] : /* istanbul ignore next */ []));
179
201
  /**
180
- * Indicates whether autoplay is currently active.
202
+ * Whether the current viewport is at the mobile breakpoint. A signal so the
203
+ * template can react (the pager drops play/pause on mobile, matching ECL,
204
+ * where play/pause is a desktop-only control).
181
205
  */
182
- this.isPlaying = false;
206
+ this.isMobileBreakpoint = signal(false, /* @ts-ignore */
207
+ ...(ngDevMode ? [{ debugName: "isMobileBreakpoint" }] : /* istanbul ignore next */ []));
183
208
  /**
184
- * Indicates whether autoplay was manually paused.
209
+ * Whether the viewport is at or above the ECL desktop breakpoint (1140px).
210
+ * This is the boundary the ECL CSS uses to show the play/pause controls and
211
+ * the teaser tablist (and to hide the mobile dots). A signal so the pager
212
+ * variant can react: play/pause only renders at/above this width, matching
213
+ * the CSS so the pause button never leaks onto narrower viewports.
185
214
  */
186
- this.isPausedForced = false;
215
+ this.isDesktopBreakpointActive = signal(false, /* @ts-ignore */
216
+ ...(ngDevMode ? [{ debugName: "isDesktopBreakpointActive" }] : /* istanbul ignore next */ []));
187
217
  /**
188
- * Indicates whether the layout is currently in RTL mode.
218
+ * Embla options, mirroring the upstream ECL configuration.
189
219
  */
190
- this.isRtl = false;
220
+ this.emblaOptions = {
221
+ loop: true,
222
+ align: 'start',
223
+ duration: 20,
224
+ };
225
+ /**
226
+ * Embla plugins. Built once so the Embla directive does not needlessly re-init.
227
+ */
228
+ this.emblaPlugins = [];
191
229
  /**
192
- * Duration of the slide transition animation (in seconds).
230
+ * Embla events the component reacts to.
193
231
  */
194
- this.transitionDuration = 0.4;
195
- this.isMobileBreakpoint = false;
196
- this.isSmallTabletBreakpoint = false;
232
+ this.emblaEvents = [
233
+ 'select',
234
+ 'reInit',
235
+ 'autoplay:timerset',
236
+ ];
237
+ /**
238
+ * Reference to the Embla directive driving the slide engine.
239
+ */
240
+ this.embla = viewChild('embla', /* @ts-ignore */
241
+ ...(ngDevMode ? [{ debugName: "embla" }] : /* istanbul ignore next */ []));
242
+ /**
243
+ * References to the teaser navigation buttons corresponding to each slide.
244
+ */
245
+ this.teaserButtons = viewChildren('teaserButton', /* @ts-ignore */
246
+ ...(ngDevMode ? [{ debugName: "teaserButtons" }] : /* istanbul ignore next */ []));
247
+ /**
248
+ * References to the teaser loading-bar completion elements (one per slide),
249
+ * used to restart the progress animation when the active slide changes.
250
+ */
251
+ this.loadingBars = viewChildren('loadingBar', /* @ts-ignore */
252
+ ...(ngDevMode ? [{ debugName: "loadingBars" }] : /* istanbul ignore next */ []));
197
253
  this.isTabletBreakpoint = false;
198
- this.isDesktopBreakpoint = false;
199
254
  this.isLargeBreakpoint = false;
200
- this.playSubject = new Subject();
201
- this.stopSubject = new Subject();
255
+ this.isDesktopBreakpoint = false;
202
256
  this.destroy$ = new Subject();
203
257
  /**
204
258
  * Debounce interval (ms) used internally for timing and transitions.
@@ -208,8 +262,14 @@ class EclCarouselComponent extends ECLBaseDirective {
208
262
  this.cd = inject(ChangeDetectorRef);
209
263
  this.renderer2 = inject(Renderer2);
210
264
  this.eclRtlService = inject(EclRtlService);
265
+ this.translate = inject(TranslateService);
211
266
  this.document = inject(DOCUMENT);
212
- this.window = this.document.defaultView; // Window | null
267
+ this.window = this.document.defaultView;
268
+ /**
269
+ * Host element, used to locate the slider pager's play/pause buttons by their
270
+ * ECL classes for focus management after a play/pause toggle.
271
+ */
272
+ this.hostEl = inject(ElementRef);
213
273
  /**
214
274
  * Returns the computed CSS class list for the component.
215
275
  */
@@ -217,25 +277,39 @@ class EclCarouselComponent extends ECLBaseDirective {
217
277
  ...(ngDevMode ? [{ debugName: "cssClasses" }] : /* istanbul ignore next */ []));
218
278
  afterNextRender(() => {
219
279
  this.observeElementSizeChanges();
220
- this.cd.detectChanges();
221
280
  this.observeBreakpointChanges();
222
- fromEvent(this.window, 'resize').pipe(auditTime(50), takeUntil(this.destroy$)).subscribe(() => {
223
- this.transitionDuration = 0;
224
- this.cd.detectChanges();
225
- // Restore transition after layout settles
226
- setTimeout(() => {
227
- this.transitionDuration = 0.4;
228
- this.cd.detectChanges();
229
- });
230
- this.observeElementSizeChanges();
231
- });
281
+ this.syncFromEmbla();
282
+ if (this.window) {
283
+ fromEvent(this.window, 'resize')
284
+ .pipe(auditTime(50), takeUntil(this.destroy$))
285
+ .subscribe(() => this.observeElementSizeChanges());
286
+ }
232
287
  });
233
288
  }
234
289
  /**
235
- * Angular lifecycle hook: initializes autoplay and RTL detection.
290
+ * Angular lifecycle hook: initializes RTL detection and the Embla plugins.
236
291
  */
237
292
  ngOnInit() {
238
- this.initAutoplay();
293
+ this.emblaOptions = {
294
+ ...this.emblaOptions,
295
+ direction: this.isRtl ? 'rtl' : 'ltr',
296
+ };
297
+ const autoplayDesired = this.isAutoplayDesired();
298
+ // Reflect the initial play state so the current slide's loading bar is
299
+ // shown on load (the plugin self-starts via playOnInit).
300
+ this.isPlaying.set(autoplayDesired);
301
+ this.emblaPlugins = [
302
+ Autoplay({
303
+ delay: this.playInterval(),
304
+ // Autoplay is on by default (matching ECL) unless explicitly
305
+ // disabled via [isAutoplay]="false". Letting the plugin start
306
+ // itself avoids a race with Embla's own initialization.
307
+ playOnInit: autoplayDesired,
308
+ stopOnInteraction: false,
309
+ stopOnMouseEnter: true,
310
+ stopOnFocusIn: true,
311
+ }),
312
+ ];
239
313
  this.handleRtlChangeState();
240
314
  }
241
315
  /**
@@ -243,447 +317,377 @@ class EclCarouselComponent extends ECLBaseDirective {
243
317
  */
244
318
  ngAfterContentInit() {
245
319
  setTimeout(() => {
246
- this.initializeSlider();
320
+ this.initSlides();
247
321
  });
248
322
  }
249
323
  /**
250
- * Angular lifecycle hook: cleans up subscriptions and event listeners.
324
+ * Angular lifecycle hook: cleans up subscriptions.
251
325
  */
252
326
  ngOnDestroy() {
253
327
  this.destroy$.next(true);
254
328
  this.destroy$.unsubscribe();
255
- if (this.unsubscribeListener) {
256
- this.unsubscribeListener();
257
- }
258
- }
259
- /**
260
- * Starts autoplay and focuses the pause button.
261
- */
262
- onPlayClick() {
263
- this.startPlaying();
264
- this.focusOnButton(this.pauseButton());
265
329
  }
266
330
  /**
267
- * Stops autoplay and focuses the play button.
268
- */
269
- onPauseClick() {
270
- this.stopPlaying();
271
- this.focusOnButton(this.playButton());
272
- }
273
- /**
274
- * Handles key press on the first carousel element to stop autoplay if TAB is pressed.
275
- */
276
- onFirstCarouselElementPress(e) {
277
- if (e.key === KeyCode.TAB) {
278
- if (this.isPlaying) {
279
- this.stopPlaying();
280
- }
281
- }
282
- }
283
- /**
284
- * Handles keyboard interaction on the play button.
285
- */
286
- onPlayKeyboardPress(e) {
287
- if (e.key === KeyCode.TAB && e.shiftKey) {
288
- return;
289
- }
290
- if ([KeyCode.TAB].includes(e.key)) {
291
- if (this.isPlaying) {
292
- this.focusOnButton(this.navigationButtons().at(this.getCurrentSlideIndex()));
293
- this.stopPlaying();
294
- }
295
- else {
296
- this.focusOnButton(this.navigationButtons().at(this.getCurrentSlideIndex()));
297
- }
298
- e.preventDefault();
299
- }
300
- else if (!this.isPlaying && [KeyCode.ARROW_RIGHT].includes(e.key)) {
301
- this.focusOnButton(this.navigationButtons().at(this.getCurrentSlideIndex()));
302
- }
303
- }
304
- /**
305
- * Handles keyboard navigation for the navigation buttons.
331
+ * Returns whether the carousel has padding applied (e.g., on desktop breakpoint).
306
332
  */
307
- onNavBtnKeyboardPress(e, index) {
308
- if (e.key === KeyCode.ARROW_RIGHT) {
309
- this.onNavigationButtonArrowRight(e);
310
- }
311
- else if (e.key === KeyCode.ARROW_LEFT) {
312
- this.onNavigationButtonArrowLeft(e, index);
313
- }
314
- return;
333
+ isPadded() {
334
+ return this.isDesktopBreakpoint;
315
335
  }
316
336
  /**
317
- * Checks whether a given button is currently visible (not hidden via style).
337
+ * Checks whether there are enough slides to render controls.
318
338
  */
319
- isButtonVisible(button) {
320
- if (!button) {
321
- return false;
322
- }
323
- return button.nativeElement.style.display === '';
339
+ hasControls() {
340
+ return this.getSlidesCount() > 1;
324
341
  }
325
342
  /**
326
- * Sets focus on the specified button.
343
+ * Retrieves the current active slide index (zero-based).
327
344
  */
328
- focusOnButton(button) {
329
- if (!button) {
330
- return;
331
- }
332
- button.nativeElement.focus();
345
+ getCurrentSlideIndex() {
346
+ return this.activeIndex;
333
347
  }
334
348
  /**
335
- * Handles user interaction with a slide control button.
349
+ * Returns the 1-based position of the active slide (for the counter).
336
350
  */
337
- onSlideControlClick(slide) {
338
- this.stopPlaying();
339
- this.activateSlide(slide, true);
351
+ getCounterCurrent() {
352
+ return this.activeIndex + 1;
340
353
  }
341
354
  /**
342
- * Navigates to the next slide and stops autoplay.
355
+ * Returns the total number of slides (for the counter).
343
356
  */
344
- onNextSlideClick() {
345
- this.stopPlaying();
346
- this.goToNextSlide(true);
357
+ getCounterTotal() {
358
+ return this.getSlidesCount();
347
359
  }
348
360
  /**
349
- * Navigates to the previous slide and stops autoplay.
361
+ * Dot indices for the mobile slider pager (one dot per slide). The dots are
362
+ * hidden on desktop via the ECL CSS (`.ecl-carousel__dots { display: none }`)
363
+ * and shown at the mobile breakpoint.
350
364
  */
351
- onPreviousSlideClick() {
352
- this.stopPlaying();
353
- this.goToPreviousSlide(true);
365
+ getDotIndices() {
366
+ return Array.from({ length: this.getSlidesCount() }, (_unused, dotIndex) => dotIndex);
354
367
  }
355
368
  /**
356
- * Pauses autoplay on mouse hover.
369
+ * Pre-translated accessible label for each pager dot. Translated here since
370
+ * the shared `ecl-slider-pager` receives plain strings.
357
371
  */
358
- onSlideHover() {
359
- this.pausePlaying();
372
+ getDotAriaLabels() {
373
+ const total = this.getSlidesCount();
374
+ return Array.from({ length: total }, (_unused, dotIndex) => this.translate.instant('ecl.carousel.GO-TO-SLIDE', {
375
+ current: dotIndex + 1,
376
+ total,
377
+ }));
360
378
  }
361
379
  /**
362
- * Resumes autoplay on mouse leave, if not manually paused.
380
+ * Returns whether a teaser button should be visible in the current window.
363
381
  */
364
- onSlideHoverout() {
365
- if (!this.isStopButtonPressed()) {
366
- this.resumePlaying();
382
+ isTeaserVisible(index) {
383
+ const count = this.getSlidesCount();
384
+ if (count <= VISIBLE_TEASERS) {
385
+ return true;
367
386
  }
387
+ const position = ((index - this.teaserWindowStart) % count + count) % count;
388
+ return position < VISIBLE_TEASERS;
368
389
  }
369
390
  /**
370
- * Returns whether the stop button (pause) has been pressed by the user.
391
+ * Returns the flex order of a teaser button within the visible window.
371
392
  */
372
- isStopButtonPressed() {
373
- return this.isPausedForced;
393
+ getTeaserOrder(index) {
394
+ const count = this.getSlidesCount();
395
+ return ((index - this.teaserWindowStart) % count + count) % count;
374
396
  }
375
397
  /**
376
- * Returns whether the carousel has padding applied (e.g., on desktop breakpoint).
398
+ * Whether the pager should show the "pause" control (i.e. autoplay is
399
+ * actively running). While the pointer/focus hover-pauses the carousel the
400
+ * timer is suspended, so the pager must show "play" even though a manual
401
+ * pause was not triggered. Mirrors upstream ECL, where hovering flips the
402
+ * control to play.
377
403
  */
378
- isPadded() {
379
- return this.isDesktopBreakpoint;
404
+ isPagerPlaying() {
405
+ return this.isPlaying() && !this.isHoverPaused();
380
406
  }
381
407
  /**
382
- * Checks whether there are enough slides to render controls.
408
+ * Pager variant by breakpoint. Below the ECL desktop breakpoint (1140px)
409
+ * the carousel shows only the dots navigation (prev / dots / next);
410
+ * play/pause is a desktop-only control in ECL. Rendering `'dots'` there
411
+ * avoids the pager's inline play/pause `display` style (which would
412
+ * otherwise beat the ECL CSS and leave the pause button visible on narrower
413
+ * viewports). At/above 1140px the dots are hidden by CSS, so
414
+ * `'play-pause-dots'` yields play/pause + prev/next + teasers.
415
+ *
416
+ * The 1140px boundary mirrors the media query the ECL CSS uses to toggle
417
+ * `.ecl-carousel__play/__pause` and `.ecl-carousel__teasers`.
383
418
  */
384
- hasControls() {
385
- return this.getSlidesCount() > 1;
419
+ pagerVariant() {
420
+ return this.isDesktopBreakpointActive() ? 'play-pause-dots' : 'dots';
386
421
  }
387
422
  /**
388
- * Retrieves the current active slide index.
389
- * Corrects index for internal structure with hidden buffer slides.
423
+ * Handles the throttled Embla events the component subscribed to.
390
424
  */
391
- getCurrentSlideIndex() {
392
- let currentIndex;
393
- this.getAllSlidesArray().forEach((item, idx) => {
394
- if (item && item.isCurrent()) {
395
- currentIndex = idx;
396
- }
397
- });
398
- if (currentIndex === this.getAllSlidesCount() - 1) {
399
- return 0;
425
+ onEmblaEvent(event) {
426
+ switch (event) {
427
+ case 'select':
428
+ case 'reInit':
429
+ this.syncFromEmbla();
430
+ break;
431
+ case 'autoplay:timerset':
432
+ // Fired each time the autoplay timer (re)starts for the current
433
+ // slide; replay the loading-bar fill animation from zero.
434
+ this.restartActiveLoadingBar();
435
+ break;
436
+ default:
437
+ break;
400
438
  }
401
- else if (currentIndex === 0) {
402
- return this.getSlidesCount() - 1;
403
- }
404
- return currentIndex - 1;
405
439
  }
406
440
  /**
407
- * Computes the left offset to shift the slides container.
441
+ * Starts autoplay.
442
+ *
443
+ * @param moveFocus when true (a user clicked play) focus moves to the pause
444
+ * button. Auto-starts (e.g. on breakpoint changes) pass false so the
445
+ * page is not scrolled to the carousel on load.
408
446
  */
409
- getCarouselSlidesOffsetLeft() {
410
- return -1 * this.carouselContainerElement()?.nativeElement.offsetWidth * this.getCurrentItemIndex();
411
- }
412
- /**
413
- * Computes the right offset to shift the slides container (same logic as left in RTL).
414
- */
415
- getCarouselSlidesOffsetRight() {
416
- return -1 * this.carouselContainerElement()?.nativeElement.offsetWidth * this.getCurrentItemIndex();
447
+ onPlayClick(moveFocus = true) {
448
+ this.getAutoplay()?.play();
449
+ this.isPlaying.set(true);
450
+ this.isPausedForced.set(false);
451
+ this.isHoverPaused.set(false);
452
+ this.cd.detectChanges();
453
+ if (moveFocus) {
454
+ this.focusPagerButton('.ecl-carousel__pause');
455
+ }
417
456
  }
418
457
  /**
419
- * Subscribes to RTL layout state changes to adapt carousel behavior.
458
+ * Stops autoplay and focuses the play button.
420
459
  */
421
- handleRtlChangeState() {
422
- this.eclRtlService.currentRtlState$.pipe(takeUntil(this.destroy$)).subscribe((rtlState) => {
423
- this.isRtl = rtlState;
424
- });
460
+ onPauseClick() {
461
+ // Pause (not full stop): halt the autoplay timer but keep the active
462
+ // teaser loading bar visible, frozen at its current progress (matching
463
+ // upstream ECL's `handleAutoPlay(false, true)`). The installed
464
+ // embla-carousel-autoplay exposes no `pause()`, so we call `stop()`
465
+ // WITHOUT `reset()` — the frozen bar is driven by our `is-paused` CSS
466
+ // class, not the plugin. A full stop+reset (which clears the bar) only
467
+ // happens on prev/next/teaser navigation via `stopAutoplay()`.
468
+ this.getAutoplay()?.stop();
469
+ this.isPlaying.set(false);
470
+ this.isPausedForced.set(true);
471
+ this.isHoverPaused.set(false);
472
+ this.cd.detectChanges();
473
+ this.focusPagerButton('.ecl-carousel__play');
425
474
  }
426
475
  /**
427
- * Initializes autoplay logic using RxJS intervals and subject triggers.
476
+ * Navigates to the next slide and stops autoplay.
428
477
  */
429
- initAutoplay() {
430
- const start$ = this.playSubject.asObservable();
431
- const stop$ = this.stopSubject.asObservable();
432
- start$
433
- .pipe(switchMap(() => interval(this.playInterval()).pipe(takeUntil(stop$))), takeUntil(this.destroy$))
434
- .subscribe(() => {
435
- this.goToNextSlide(true);
436
- });
478
+ onNextSlideClick() {
479
+ this.stopAutoplay();
480
+ this.embla()?.scrollNext();
437
481
  }
438
482
  /**
439
- * Returns the index of the currently active slide item.
483
+ * Navigates to the previous slide and stops autoplay.
440
484
  */
441
- getCurrentItemIndex() {
442
- let result = 0;
443
- this.getAllSlidesArray().forEach((item, index) => {
444
- if (item && item.isCurrent()) {
445
- result = index;
446
- }
447
- });
448
- return result;
485
+ onPreviousSlideClick() {
486
+ this.stopAutoplay();
487
+ this.embla()?.scrollPrev();
449
488
  }
450
489
  /**
451
- * Initializes slides, hidden buffer items, and sets default active slide.
490
+ * Handles user interaction with a teaser button: activates the matching slide.
452
491
  */
453
- initializeSlider() {
454
- this.slides().forEach((item, idx) => {
455
- this.initSlide(item, idx);
456
- });
457
- if (this.hasControls()) {
458
- this.initHiddenItems();
459
- if (this.isNoSlideSelected()) {
460
- this.activateSlide(this.slides().at(0), false);
461
- }
462
- }
492
+ onTeaserClick(index) {
493
+ this.stopAutoplay();
494
+ this.embla()?.scrollTo(index);
463
495
  }
464
496
  /**
465
- * Initializes hidden first and last slide clones used for infinite scroll illusion.
497
+ * Handles a dot activation from the mobile slider pager: scrolls to the
498
+ * matching slide and stops autoplay (same behavior as a teaser click).
466
499
  */
467
- initHiddenItems() {
468
- this.firstHiddenItem().el.nativeElement.innerHTML = this.getLastContentChild();
469
- this.firstHiddenItem().isCurrent.set(false);
470
- this.lastHiddenItem().el.nativeElement.innerHTML = this.getFirstContentChild();
471
- this.lastHiddenItem().isCurrent.set(false);
472
- this.unsubscribeListener = this.renderer2.listen(this.slidesContainer().nativeElement, 'transitionend', () => this.checkIndex());
500
+ onDotClick(index) {
501
+ this.stopAutoplay();
502
+ this.embla()?.scrollTo(index);
473
503
  }
474
504
  /**
475
- * Verifies current index and resets to actual slide if hidden clone is reached.
505
+ * Handles keyboard navigation (ArrowLeft/ArrowRight) across teaser buttons.
476
506
  */
477
- checkIndex() {
478
- if (this.getCurrentItemIndex() === this.getAllSlidesCount() - 1) {
479
- const item = this.slides().at(0);
480
- this.activateSlide(item, false);
507
+ onTeaserKeyboardPress(event, index) {
508
+ if (event.key !== KeyCode.ARROW_RIGHT && event.key !== KeyCode.ARROW_LEFT) {
509
+ return;
481
510
  }
482
- else if (this.getCurrentItemIndex() === 0) {
483
- const item = this.slides().at(-1);
484
- this.activateSlide(item, false);
511
+ event.preventDefault();
512
+ const direction = event.key === KeyCode.ARROW_RIGHT ? 1 : -1;
513
+ const count = this.getSlidesCount();
514
+ const nextIndex = (index + direction + count) % count;
515
+ this.stopAutoplay();
516
+ this.embla()?.scrollTo(nextIndex);
517
+ this.focusOnButton(this.teaserButtons()[nextIndex]);
518
+ }
519
+ /**
520
+ * Freezes the loading bar while the pointer/focus is over the carousel.
521
+ * The Embla autoplay plugin pauses the timer itself (stopOnMouseEnter /
522
+ * stopOnFocusIn); this only mirrors that as a visible paused state.
523
+ */
524
+ onViewportPause() {
525
+ if (this.isPlaying() && !this.isPausedForced()) {
526
+ this.isHoverPaused.set(true);
527
+ this.cd.detectChanges();
485
528
  }
486
529
  }
487
530
  /**
488
- * Returns the inner HTML of the last real slide (used for buffer item).
531
+ * Resumes the loading bar once the pointer/focus leaves the carousel.
489
532
  */
490
- getLastContentChild() {
491
- return this.getSlidesCount() > 0 ? this.slides().at(this.getSlidesCount() - 1).el.nativeElement.innerHTML : '';
492
- }
493
- /**
494
- * Returns the inner HTML of the first real slide (used for buffer item).
495
- */
496
- getFirstContentChild() {
497
- return this.getSlidesCount() > 0 ? this.slides().at(0).el.nativeElement.innerHTML : '';
498
- }
499
- /**
500
- * Sets up a slide with index and ARIA label.
501
- */
502
- initSlide(item, idx) {
503
- item.index.set(idx);
504
- item.ariaLabel.set(`slide ${idx + 1} of ${this.getSlidesCount()}`);
505
- const banner = item.banner();
506
- if (banner) {
507
- banner.skipHeightManagement = true;
533
+ onViewportResume() {
534
+ if (this.isHoverPaused()) {
535
+ this.isHoverPaused.set(false);
536
+ this.cd.detectChanges();
508
537
  }
509
538
  }
510
539
  /**
511
- * Activates the next slide, with option to loop.
512
- */
513
- goToNextSlide(isLoop) {
514
- this.activateSlide(this.getNextSlide(isLoop), true);
515
- }
516
- /**
517
- * Activates the previous slide, with option to loop.
518
- */
519
- goToPreviousSlide(isLoop) {
520
- this.activateSlide(this.getPreviousSlide(isLoop), true);
521
- }
522
- /**
523
- * Activates a specific slide and triggers slide change event if needed.
540
+ * Sets focus on the specified button.
524
541
  */
525
- activateSlide(slideItem, withTransition) {
526
- if (withTransition) {
527
- this.transitionDuration = 0.4;
528
- }
529
- else {
530
- this.transitionDuration = 0;
531
- }
532
- if (slideItem) {
533
- this.getAllSlidesArray().forEach((item) => {
534
- item.isCurrent.set(false);
535
- });
536
- slideItem.isCurrent.set(true);
537
- }
538
- this.cd.detectChanges();
539
- if (withTransition) {
540
- this.slide.emit(new EclSlideEvent(this.slides().at(this.getCurrentSlideIndex())));
542
+ focusOnButton(button) {
543
+ if (!button) {
544
+ return;
541
545
  }
546
+ button.nativeElement.focus();
542
547
  }
543
548
  /**
544
- * Checks if the currently focused button is the last in the list.
549
+ * Focuses the slider pager button matching the given ECL class selector
550
+ * (e.g. `.ecl-carousel__play`). Used after a play/pause toggle so focus
551
+ * follows the now-visible control, since the pager swaps play/pause
552
+ * visibility via CSS. No-op if the button is not rendered (e.g. mobile).
545
553
  */
546
- isLastButton(index) {
547
- const buttonsList = this.navigationButtons();
548
- return index + 1 >= buttonsList.length;
554
+ focusPagerButton(selector) {
555
+ const button = this.hostEl.nativeElement.querySelector(selector);
556
+ button?.focus();
549
557
  }
550
558
  /**
551
- * Checks if the currently focused button is the first in the list.
559
+ * Returns the Embla autoplay plugin instance, but only when the carousel
560
+ * actually has more than one scroll snap.
561
+ *
562
+ * The `embla-carousel-autoplay` plugin skips its own initialization when
563
+ * `scrollSnapList().length <= 1` (see the plugin's `init`), which leaves its
564
+ * internal `delay` lookup table undefined. Calling `play()` in that state
565
+ * throws (`delay[selectedScrollSnap()]` -> reading `0` of undefined). This
566
+ * happens whenever Embla cannot lay out multiple snaps — notably in a jsdom
567
+ * test or during SSR. Gating on the same `> 1` condition the plugin uses
568
+ * keeps autoplay a safe no-op until the carousel is genuinely slideable.
552
569
  */
553
- isFirstButton(index) {
554
- return index <= 0;
570
+ getAutoplay() {
571
+ const api = this.embla()?.emblaApi;
572
+ if (!api || api.scrollSnapList().length <= 1) {
573
+ return undefined;
574
+ }
575
+ return api.plugins()?.autoplay;
555
576
  }
556
577
  /**
557
- * Starts autoplay mode and updates state.
578
+ * Whether autoplay should run. On by default (matching ECL); only disabled
579
+ * when the consumer explicitly sets `[isAutoplay]="false"`.
558
580
  */
559
- startPlaying() {
560
- this.isPlaying = true;
561
- this.cd.detectChanges();
562
- this.isPausedForced = false;
563
- this.playSubject.next();
581
+ isAutoplayDesired() {
582
+ const isAutoplay = this.isAutoplay();
583
+ return isAutoplay || isAutoplay === undefined;
564
584
  }
565
585
  /**
566
- * Stops autoplay mode and updates state.
586
+ * Stops autoplay and resets it (mirrors ECL's `handleAutoPlay(true)`),
587
+ * clearing the loading-bar state.
567
588
  */
568
- stopPlaying() {
569
- this.isPlaying = false;
589
+ stopAutoplay() {
590
+ const autoplay = this.getAutoplay();
591
+ autoplay?.stop();
592
+ autoplay?.reset();
593
+ this.isPlaying.set(false);
594
+ this.isPausedForced.set(false);
595
+ this.isHoverPaused.set(false);
570
596
  this.cd.detectChanges();
571
- this.isPausedForced = true;
572
- this.stopSubject.next();
573
597
  }
574
598
  /**
575
- * Temporarily pauses autoplay (e.g. on mouse hover).
599
+ * Synchronizes component state from Embla's current selected snap:
600
+ * active index, slide `isCurrent` flags, teaser window and the slide event.
576
601
  */
577
- pausePlaying() {
578
- this.isPlaying = false;
602
+ syncFromEmbla() {
603
+ const api = this.embla()?.emblaApi;
604
+ if (!api) {
605
+ return;
606
+ }
607
+ const selected = api.selectedScrollSnap();
608
+ this.setActiveIndex(selected);
609
+ this.updateTeaserWindow();
610
+ this.slide.emit(new EclSlideEvent(this.slides().at(this.activeIndex)));
579
611
  this.cd.detectChanges();
580
- this.stopSubject.next();
581
612
  }
582
613
  /**
583
- * Resumes autoplay after being paused.
614
+ * Subscribes to RTL layout state changes to adapt carousel direction.
584
615
  */
585
- resumePlaying() {
586
- this.isPlaying = true;
587
- this.cd.detectChanges();
588
- this.playSubject.next();
616
+ handleRtlChangeState() {
617
+ this.eclRtlService.currentRtlState$.pipe(takeUntil(this.destroy$)).subscribe((rtlState) => {
618
+ const changed = this.isRtl !== rtlState;
619
+ this.isRtl = rtlState;
620
+ if (changed) {
621
+ this.emblaOptions = {
622
+ ...this.emblaOptions,
623
+ direction: this.isRtl ? 'rtl' : 'ltr',
624
+ };
625
+ this.cd.detectChanges();
626
+ }
627
+ });
589
628
  }
590
629
  /**
591
- * Retrieves the next slide based on the current index and loop setting.
630
+ * Initializes slides (index, ARIA label, banner height management).
592
631
  */
593
- getNextSlide(isLoop) {
594
- const slidesCount = this.getSlidesCount();
595
- const currentIndex = this.getCurrentSlideIndex();
596
- if (currentIndex >= slidesCount - 1) {
597
- if (isLoop) {
598
- return this.getAllSlidesArray()[this.getAllSlidesCount() - 1];
599
- }
600
- else {
601
- return this.slides().at(-1);
632
+ initSlides() {
633
+ const slides = this.slides();
634
+ slides.forEach((item, idx) => {
635
+ item.index.set(idx);
636
+ item.ariaLabel.set(`slide ${idx + 1} of ${slides.length}`);
637
+ item.isCurrent.set(idx === 0);
638
+ const banner = item.banner();
639
+ if (banner) {
640
+ banner.skipHeightManagement = true;
602
641
  }
603
- }
604
- else {
605
- return this.slides().at(currentIndex + 1);
606
- }
642
+ });
643
+ this.cd.detectChanges();
607
644
  }
608
645
  /**
609
- * Returns all slides, including the hidden buffer items.
646
+ * Updates the active index and synchronizes slide active/inert state.
610
647
  */
611
- getAllSlidesArray() {
612
- return [...[this.firstHiddenItem()], ...this.slides(), ...[this.lastHiddenItem()]];
648
+ setActiveIndex(index) {
649
+ this.activeIndex = index;
650
+ this.slides().forEach((item, idx) => {
651
+ item.isCurrent.set(idx === index);
652
+ });
613
653
  }
614
654
  /**
615
- * Retrieves the previous slide based on the current index and loop setting.
655
+ * Restarts the CSS progress animation on the active slide's loading bar.
656
+ * The animation uses `forwards`, so re-applying the `is-active` class alone
657
+ * will not replay it; the animation must be reset explicitly.
616
658
  */
617
- getPreviousSlide(isLoop) {
618
- const currentIndex = this.getCurrentSlideIndex();
619
- if (currentIndex <= 0) {
620
- if (isLoop) {
621
- return this.getAllSlidesArray()[0];
622
- }
623
- else {
624
- return this.slides().at(0);
625
- }
659
+ restartActiveLoadingBar() {
660
+ if (!this.getAutoplay()?.isPlaying()) {
661
+ return;
626
662
  }
627
- else {
628
- return this.slides().at(currentIndex - 1);
663
+ const bar = this.loadingBars()[this.activeIndex]?.nativeElement;
664
+ if (!bar) {
665
+ return;
629
666
  }
667
+ bar.style.animation = 'none';
668
+ // Force reflow so the browser registers the reset before re-enabling.
669
+ void bar.offsetWidth;
670
+ bar.style.animation = '';
630
671
  }
631
672
  /**
632
- * Returns the number of actual visible slides (excluding hidden buffer).
673
+ * Advances the visible teaser window when the active slide moves out of it.
633
674
  */
634
- getSlidesCount() {
635
- return this.slides().length;
636
- }
637
- /**
638
- * Returns total number of slides, including buffer (hidden) items.
639
- */
640
- getAllSlidesCount() {
641
- if (this.hasControls()) {
642
- return this.slides().length + 2;
675
+ updateTeaserWindow() {
676
+ const count = this.getSlidesCount();
677
+ if (count <= VISIBLE_TEASERS) {
678
+ this.teaserWindowStart = 0;
679
+ return;
643
680
  }
644
- else {
645
- return this.slides().length;
681
+ const firstHiddenIndex = (this.teaserWindowStart + VISIBLE_TEASERS) % count;
682
+ if (this.activeIndex === firstHiddenIndex) {
683
+ this.teaserWindowStart = firstHiddenIndex;
646
684
  }
647
685
  }
648
686
  /**
649
- * Determines whether no slide is currently marked as active.
650
- */
651
- isNoSlideSelected() {
652
- return !this.slides().some((item) => item.isCurrent());
653
- }
654
- /**
655
- * Handles right arrow key navigation for buttons.
656
- */
657
- onNavigationButtonArrowRight(e) {
658
- setTimeout(() => {
659
- this.goToNextSlide(false);
660
- const button = this.navigationButtons().at(this.getCurrentSlideIndex());
661
- this.focusOnButton(button);
662
- }, this.TIMEOUT_INTERVAL);
663
- e.preventDefault();
664
- }
665
- /**
666
- * Handles left arrow key navigation for buttons.
687
+ * Returns the number of actual visible slides.
667
688
  */
668
- onNavigationButtonArrowLeft(e, index) {
669
- if (this.isFirstButton(index)) {
670
- const playButton = this.playButton();
671
- if (playButton) {
672
- this.focusOnButton(playButton);
673
- }
674
- const pauseButton = this.pauseButton();
675
- if (pauseButton) {
676
- this.focusOnButton(pauseButton);
677
- }
678
- }
679
- else {
680
- setTimeout(() => {
681
- this.goToPreviousSlide(false);
682
- const button = this.navigationButtons().at(this.getCurrentSlideIndex());
683
- this.focusOnButton(button);
684
- }, this.TIMEOUT_INTERVAL);
685
- e.preventDefault();
686
- }
689
+ getSlidesCount() {
690
+ return this.slides()?.length ?? 0;
687
691
  }
688
692
  /**
689
693
  * Observes screen breakpoint changes to adjust autoplay and layout behavior.
@@ -692,31 +696,31 @@ class EclCarouselComponent extends ECLBaseDirective {
692
696
  this.cdkBreakpointObserver
693
697
  .observe([
694
698
  '(max-width: 768px)',
695
- '(max-width: 997px)',
696
699
  '(min-width: 998px)',
697
700
  '(min-width: 1140px)',
698
701
  '(min-width: 1140px) and (max-width: 1260px)',
699
702
  ])
700
703
  .pipe(takeUntil(this.destroy$))
701
704
  .subscribe((result) => {
702
- this.isMobileBreakpoint = result.breakpoints['(max-width: 768px)'];
703
- this.isSmallTabletBreakpoint = result.breakpoints['(max-width: 997px)'];
705
+ this.isMobileBreakpoint.set(result.breakpoints['(max-width: 768px)']);
704
706
  this.isTabletBreakpoint = result.breakpoints['(min-width: 998px)'];
705
707
  this.isLargeBreakpoint = result.breakpoints['(min-width: 1140px)'];
708
+ this.isDesktopBreakpointActive.set(result.breakpoints['(min-width: 1140px)']);
706
709
  this.isDesktopBreakpoint = result.breakpoints['(min-width: 1140px) and (max-width: 1260px)'];
707
- const isAutoplay = this.isAutoplay();
708
- if (this.isMobileBreakpoint && !isAutoplay) {
709
- this.stopPlaying();
710
+ // Deactivate autoplay on mobile; (re)activate it on larger
711
+ // screens if autoplay is desired. Focus is never moved here so
712
+ // the page is not scrolled to the carousel on load/resize.
713
+ if (this.isMobileBreakpoint()) {
714
+ this.stopAutoplay();
715
+ return;
710
716
  }
711
- if (this.isTabletBreakpoint) {
712
- if (isAutoplay || isAutoplay === undefined) {
713
- this.startPlaying();
714
- }
717
+ if (this.isTabletBreakpoint && this.isAutoplayDesired() && !this.isPausedForced()) {
718
+ this.onPlayClick(false);
715
719
  }
716
720
  });
717
721
  }
718
722
  /**
719
- * Observes changes to the carousel's container element size using ResizeObserver.
723
+ * Observes changes to the carousel's viewport element size using a timeout debounce.
720
724
  */
721
725
  observeElementSizeChanges() {
722
726
  setTimeout(() => {
@@ -726,13 +730,13 @@ class EclCarouselComponent extends ECLBaseDirective {
726
730
  else {
727
731
  this.resetHeight();
728
732
  }
733
+ this.embla()?.reInit();
729
734
  }, this.TIMEOUT_INTERVAL);
730
735
  }
731
736
  /**
732
737
  * Normalizes the height of all banners to match the tallest banner.
733
738
  */
734
739
  checkBannerHeights() {
735
- // First, let each banner calculate its own height (like ECL's bannerInstance.setHeight(ratio))
736
740
  this.slides().forEach((slide) => {
737
741
  const banner = slide.banner();
738
742
  if (banner) {
@@ -773,7 +777,6 @@ class EclCarouselComponent extends ECLBaseDirective {
773
777
  this.renderer2.setStyle(bannerVideo.el.nativeElement, 'aspect-ratio', 'auto');
774
778
  }
775
779
  });
776
- this.resetHiddenItems();
777
780
  }
778
781
  }
779
782
  /**
@@ -795,32 +798,27 @@ class EclCarouselComponent extends ECLBaseDirective {
795
798
  }
796
799
  }
797
800
  });
798
- this.resetHiddenItems();
799
- }
800
- /**
801
- * Re-renders the content of hidden buffer items after style reset.
802
- */
803
- resetHiddenItems() {
804
- if (this.hasControls()) {
805
- this.firstHiddenItem().el.nativeElement.innerHTML = this.getLastContentChild();
806
- this.lastHiddenItem().el.nativeElement.innerHTML = this.getFirstContentChild();
807
- }
808
801
  }
809
802
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: EclCarouselComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
810
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.2", type: EclCarouselComponent, isStandalone: true, selector: "ecl-carousel", inputs: { ariaLabel: { classPropertyName: "ariaLabel", publicName: "aria-label", isSignal: true, isRequired: false, transformFunction: null }, isAutoplay: { classPropertyName: "isAutoplay", publicName: "isAutoplay", isSignal: true, isRequired: false, transformFunction: null }, playInterval: { classPropertyName: "playInterval", publicName: "playInterval", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { slide: "slide" }, host: { properties: { "attr.aria-label": "ariaLabel()", "attr.aria-roledescription": "\"carousel\"", "class": "cssClasses()" } }, queries: [{ propertyName: "slides", predicate: i0.forwardRef(() => EclCarouselItemComponent), isSignal: true }], viewQueries: [{ propertyName: "carouselContainerElement", first: true, predicate: ["carouselContainer"], descendants: true, isSignal: true }, { propertyName: "navigationButtons", predicate: ["currButton"], descendants: true, isSignal: true }, { propertyName: "playButton", first: true, predicate: ["playButton"], descendants: true, isSignal: true }, { propertyName: "pauseButton", first: true, predicate: ["pauseButton"], descendants: true, isSignal: true }, { propertyName: "firstHiddenItem", first: true, predicate: ["firstHiddenItem"], descendants: true, isSignal: true }, { propertyName: "lastHiddenItem", first: true, predicate: ["lastHiddenItem"], descendants: true, isSignal: true }, { propertyName: "slidesContainer", first: true, predicate: ["slidesContainer"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "@if(hasControls()){\n<div class=\"ecl-carousel__controls\">\n <div class=\"ecl-container\">\n <div class=\"ecl-carousel__autoplay\">\n <button #playButton class=\"ecl-button ecl-button--tertiary ecl-button--neutral ecl-carousel__play ecl-button--icon-only\"\n type=\"button\" [style.display]=\"isPlaying ? 'none' : 'flex'\" (keydown)=\"onPlayKeyboardPress($event)\"\n (click)=\"onPlayClick()\">\n <span class=\"ecl-button__container\">\n <span class=\"ecl-button__label\">\n {{ 'ecl.carousel.PLAY' | translate }}\n </span>\n <ecl-icon icon=\"play-outline\" size=\"m\" class=\"ecl-button__icon ecl-carousel__icon-default\">\n </ecl-icon>\n </span>\n </button>\n\n <button #pauseButton class=\"ecl-button ecl-button--tertiary ecl-button--neutral ecl-carousel__pause ecl-button--icon-only\"\n type=\"button\" [style.display]=\"isPlaying ? 'flex' : 'none'\" (keydown)=\"onPlayKeyboardPress($event)\"\n (click)=\"onPauseClick()\">\n <span class=\"ecl-button__container\">\n <span class=\"ecl-button__label\">\n {{ 'ecl.carousel.PAUSE' | translate }}\n </span>\n <ecl-icon icon=\"pause-outline\" size=\"m\" class=\"ecl-button__icon ecl-carousel__icon-default\"> </ecl-icon>\n </span>\n\n </button>\n </div>\n\n <div class=\"ecl-carousel__navigation\" role=\"tablist\" >\n @for (slide of slides(); let i = $index; track slide) {\n <button #currButton [attr.aria-current]=\"i === getCurrentSlideIndex()\" [attr.tabindex]=\"i === getCurrentSlideIndex() ? null: -1\"\n class=\"ecl-button ecl-button--tertiary ecl-button--neutral ecl-carousel__navigation-item\" role=\"tab\" (click)=\"onSlideControlClick(slide)\"\n (keydown)=\"onNavBtnKeyboardPress($event, i)\">\n {{ i + 1 }}\n </button>\n }\n </div>\n </div>\n</div>\n}\n<div #carouselContainer class=\"ecl-carousel__container\" [class.ecl-carousel-container--padded]=\"isPadded()\">\n <div #slidesContainer (keyup)=\"onFirstCarouselElementPress($event)\" class=\"ecl-carousel__slides\"\n [style.left.px]=\"!isRtl ? getCarouselSlidesOffsetLeft() : null\"\n [style.right.px]=\"isRtl ? getCarouselSlidesOffsetRight() : null\"\n [style.transition-duration.s]=\"transitionDuration\" (mouseover)=\"onSlideHover()\" (mouseout)=\"onSlideHoverout()\">\n\n @if(hasControls()){\n <ecl-carousel-item #firstHiddenItem></ecl-carousel-item>\n }\n <ng-content></ng-content>\n\n @if(hasControls()){\n <ecl-carousel-item #lastHiddenItem></ecl-carousel-item>\n }\n </div>\n</div>\n", styles: [".ecl-carousel__slides{transition-duration:.4s}\n"], dependencies: [{ kind: "component", type: EclIconComponent, selector: "ecl-icon", inputs: ["iconSet", "icon", "size", "color", "transform", "ariaLabelledby", "role", "title", "ariaHidden", "focusable", "isFlipHorizontal"] }, { kind: "component", type: EclCarouselItemComponent, selector: "ecl-carousel-item", inputs: ["id", "isCurrent"], outputs: ["slideClick"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }] }); }
803
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.2", type: EclCarouselComponent, isStandalone: true, selector: "ecl-carousel", inputs: { ariaLabel: { classPropertyName: "ariaLabel", publicName: "aria-label", isSignal: true, isRequired: false, transformFunction: null }, isAutoplay: { classPropertyName: "isAutoplay", publicName: "isAutoplay", isSignal: true, isRequired: false, transformFunction: null }, playInterval: { classPropertyName: "playInterval", publicName: "playInterval", isSignal: true, isRequired: false, transformFunction: null }, counterLabel: { classPropertyName: "counterLabel", publicName: "counterLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { slide: "slide" }, host: { properties: { "attr.aria-label": "ariaLabel()", "attr.aria-roledescription": "\"carousel\"", "class": "cssClasses()", "style.--ecl-carousel-slide-duration": "playInterval() + \"ms\"" } }, queries: [{ propertyName: "slides", predicate: i0.forwardRef(() => EclCarouselItemComponent), isSignal: true }], viewQueries: [{ propertyName: "carouselContainerElement", first: true, predicate: ["carouselContainer"], descendants: true, isSignal: true }, { propertyName: "embla", first: true, predicate: ["embla"], descendants: true, isSignal: true }, { propertyName: "teaserButtons", predicate: ["teaserButton"], descendants: true, isSignal: true }, { propertyName: "loadingBars", predicate: ["loadingBar"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "@if (hasControls()) {\n<div class=\"ecl-carousel__controls\">\n <div class=\"ecl-container\">\n <div class=\"ecl-carousel__navigation\">\n <div class=\"ecl-carousel__pager-container\">\n <ecl-slider-pager\n class=\"ecl-carousel__pager\"\n [variant]=\"pagerVariant()\"\n size=\"m\"\n [isPlaying]=\"isPagerPlaying()\"\n [activeIndex]=\"getCurrentSlideIndex()\"\n [dotIndices]=\"getDotIndices()\"\n [dotAriaLabels]=\"getDotAriaLabels()\"\n prevExtraClasses=\"ecl-carousel__prev\"\n nextExtraClasses=\"ecl-carousel__next\"\n dotsExtraClasses=\"ecl-carousel__dots\"\n dotExtraClasses=\"ecl-carousel__dot\"\n playExtraClasses=\"ecl-carousel__play\"\n pauseExtraClasses=\"ecl-carousel__pause\"\n (play)=\"onPlayClick()\"\n (pause)=\"onPauseClick()\"\n (prev)=\"onPreviousSlideClick()\"\n (next)=\"onNextSlideClick()\"\n (goTo)=\"onDotClick($event)\">\n </ecl-slider-pager>\n <div class=\"ecl-carousel__counter\" dir=\"ltr\">\n {{ getCounterCurrent() }} {{ counterLabel() || ('ecl.common.OF' | translate) }} {{ getCounterTotal() }}\n </div>\n </div>\n\n <div class=\"ecl-carousel__teasers\" role=\"tablist\">\n @for (slide of slides(); let i = $index; track slide) {\n <button #teaserButton type=\"button\" class=\"ecl-carousel__teaser-button\" role=\"tab\"\n [attr.data-ecl-carousel-slide-index]=\"i\"\n [attr.aria-current]=\"i === getCurrentSlideIndex() ? true : null\"\n [attr.tabindex]=\"i === getCurrentSlideIndex() ? null : -1\"\n [style.display]=\"isTeaserVisible(i) ? null : 'none'\"\n [style.pointer-events]=\"isTeaserVisible(i) ? null : 'none'\"\n [style.order]=\"getTeaserOrder(i)\" (click)=\"onTeaserClick(i)\"\n (keydown)=\"onTeaserKeyboardPress($event, i)\">\n <span class=\"ecl-carousel__teaser-loading-bar\" aria-hidden=\"true\"\n [class.is-active]=\"(isPlaying() || isHoverPaused() || isPausedForced()) && i === getCurrentSlideIndex()\"\n [class.is-paused]=\"(isHoverPaused() || isPausedForced()) && i === getCurrentSlideIndex()\">\n <span #loadingBar class=\"ecl-carousel__teaser-loading-bar-completion\"></span>\n </span>\n <span class=\"ecl-carousel__teaser-content\">\n @if (slide.teaserText()) {\n <span class=\"ecl-carousel__teaser-title\">{{ slide.teaserText() }}</span>\n }\n </span>\n </button>\n }\n </div>\n </div>\n </div>\n</div>\n}\n<div #carouselContainer class=\"ecl-carousel__viewport\" [class.ecl-carousel-container--padded]=\"isPadded()\"\n emblaCarousel #embla=\"emblaCarousel\" [options]=\"emblaOptions\" [plugins]=\"emblaPlugins\"\n [subscribeToEvents]=\"emblaEvents\" (emblaChange)=\"onEmblaEvent($event)\"\n (mouseenter)=\"onViewportPause()\" (mouseleave)=\"onViewportResume()\"\n (focusin)=\"onViewportPause()\" (focusout)=\"onViewportResume()\">\n <div class=\"ecl-carousel__slides\">\n <ng-content></ng-content>\n </div>\n</div>\n", dependencies: [{ kind: "directive", type: EmblaCarouselDirective, selector: "[emblaCarousel]", inputs: ["options", "plugins", "subscribeToEvents", "eventsThrottleTime"], outputs: ["emblaChange"], exportAs: ["emblaCarousel"] }, { kind: "component", type: i1.EclSliderPagerComponent, selector: "ecl-slider-pager", inputs: ["size", "variant", "isPlaying", "isPrevDisabled", "isNextDisabled", "activeIndex", "dotIndices", "dotAriaLabels", "prevExtraClasses", "nextExtraClasses", "dotsExtraClasses", "dotExtraClasses", "playExtraClasses", "pauseExtraClasses"], outputs: ["prev", "next", "goTo", "play", "pause"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }] }); }
811
804
  }
812
805
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: EclCarouselComponent, decorators: [{
813
806
  type: Component,
814
807
  args: [{ selector: 'ecl-carousel', imports: [
815
808
  TranslatePipe,
816
- EclIconComponent,
817
809
  EclCarouselItemComponent,
810
+ EmblaCarouselDirective,
811
+ ...EUI_ECL_SLIDER,
818
812
  ], host: {
819
813
  '[attr.aria-label]': 'ariaLabel()',
820
814
  '[attr.aria-roledescription]': '"carousel"',
821
815
  '[class]': 'cssClasses()',
822
- }, template: "@if(hasControls()){\n<div class=\"ecl-carousel__controls\">\n <div class=\"ecl-container\">\n <div class=\"ecl-carousel__autoplay\">\n <button #playButton class=\"ecl-button ecl-button--tertiary ecl-button--neutral ecl-carousel__play ecl-button--icon-only\"\n type=\"button\" [style.display]=\"isPlaying ? 'none' : 'flex'\" (keydown)=\"onPlayKeyboardPress($event)\"\n (click)=\"onPlayClick()\">\n <span class=\"ecl-button__container\">\n <span class=\"ecl-button__label\">\n {{ 'ecl.carousel.PLAY' | translate }}\n </span>\n <ecl-icon icon=\"play-outline\" size=\"m\" class=\"ecl-button__icon ecl-carousel__icon-default\">\n </ecl-icon>\n </span>\n </button>\n\n <button #pauseButton class=\"ecl-button ecl-button--tertiary ecl-button--neutral ecl-carousel__pause ecl-button--icon-only\"\n type=\"button\" [style.display]=\"isPlaying ? 'flex' : 'none'\" (keydown)=\"onPlayKeyboardPress($event)\"\n (click)=\"onPauseClick()\">\n <span class=\"ecl-button__container\">\n <span class=\"ecl-button__label\">\n {{ 'ecl.carousel.PAUSE' | translate }}\n </span>\n <ecl-icon icon=\"pause-outline\" size=\"m\" class=\"ecl-button__icon ecl-carousel__icon-default\"> </ecl-icon>\n </span>\n\n </button>\n </div>\n\n <div class=\"ecl-carousel__navigation\" role=\"tablist\" >\n @for (slide of slides(); let i = $index; track slide) {\n <button #currButton [attr.aria-current]=\"i === getCurrentSlideIndex()\" [attr.tabindex]=\"i === getCurrentSlideIndex() ? null: -1\"\n class=\"ecl-button ecl-button--tertiary ecl-button--neutral ecl-carousel__navigation-item\" role=\"tab\" (click)=\"onSlideControlClick(slide)\"\n (keydown)=\"onNavBtnKeyboardPress($event, i)\">\n {{ i + 1 }}\n </button>\n }\n </div>\n </div>\n</div>\n}\n<div #carouselContainer class=\"ecl-carousel__container\" [class.ecl-carousel-container--padded]=\"isPadded()\">\n <div #slidesContainer (keyup)=\"onFirstCarouselElementPress($event)\" class=\"ecl-carousel__slides\"\n [style.left.px]=\"!isRtl ? getCarouselSlidesOffsetLeft() : null\"\n [style.right.px]=\"isRtl ? getCarouselSlidesOffsetRight() : null\"\n [style.transition-duration.s]=\"transitionDuration\" (mouseover)=\"onSlideHover()\" (mouseout)=\"onSlideHoverout()\">\n\n @if(hasControls()){\n <ecl-carousel-item #firstHiddenItem></ecl-carousel-item>\n }\n <ng-content></ng-content>\n\n @if(hasControls()){\n <ecl-carousel-item #lastHiddenItem></ecl-carousel-item>\n }\n </div>\n</div>\n", styles: [".ecl-carousel__slides{transition-duration:.4s}\n"] }]
823
- }], ctorParameters: () => [], propDecorators: { ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "aria-label", required: false }] }], isAutoplay: [{ type: i0.Input, args: [{ isSignal: true, alias: "isAutoplay", required: false }] }], playInterval: [{ type: i0.Input, args: [{ isSignal: true, alias: "playInterval", required: false }] }], slide: [{ type: i0.Output, args: ["slide"] }], carouselContainerElement: [{ type: i0.ViewChild, args: ['carouselContainer', { isSignal: true }] }], slides: [{ type: i0.ContentChildren, args: [forwardRef(() => EclCarouselItemComponent), { isSignal: true }] }], navigationButtons: [{ type: i0.ViewChildren, args: ['currButton', { isSignal: true }] }], playButton: [{ type: i0.ViewChild, args: ['playButton', { isSignal: true }] }], pauseButton: [{ type: i0.ViewChild, args: ['pauseButton', { isSignal: true }] }], firstHiddenItem: [{ type: i0.ViewChild, args: ['firstHiddenItem', { isSignal: true }] }], lastHiddenItem: [{ type: i0.ViewChild, args: ['lastHiddenItem', { isSignal: true }] }], slidesContainer: [{ type: i0.ViewChild, args: ['slidesContainer', { isSignal: true }] }] } });
816
+ // Drives the teaser loading-bar animation duration (see @eui/styles).
817
+ // It has no CSS fallback, so without it the progress animation never
818
+ // runs; it must match the autoplay delay.
819
+ '[style.--ecl-carousel-slide-duration]': 'playInterval() + "ms"',
820
+ }, template: "@if (hasControls()) {\n<div class=\"ecl-carousel__controls\">\n <div class=\"ecl-container\">\n <div class=\"ecl-carousel__navigation\">\n <div class=\"ecl-carousel__pager-container\">\n <ecl-slider-pager\n class=\"ecl-carousel__pager\"\n [variant]=\"pagerVariant()\"\n size=\"m\"\n [isPlaying]=\"isPagerPlaying()\"\n [activeIndex]=\"getCurrentSlideIndex()\"\n [dotIndices]=\"getDotIndices()\"\n [dotAriaLabels]=\"getDotAriaLabels()\"\n prevExtraClasses=\"ecl-carousel__prev\"\n nextExtraClasses=\"ecl-carousel__next\"\n dotsExtraClasses=\"ecl-carousel__dots\"\n dotExtraClasses=\"ecl-carousel__dot\"\n playExtraClasses=\"ecl-carousel__play\"\n pauseExtraClasses=\"ecl-carousel__pause\"\n (play)=\"onPlayClick()\"\n (pause)=\"onPauseClick()\"\n (prev)=\"onPreviousSlideClick()\"\n (next)=\"onNextSlideClick()\"\n (goTo)=\"onDotClick($event)\">\n </ecl-slider-pager>\n <div class=\"ecl-carousel__counter\" dir=\"ltr\">\n {{ getCounterCurrent() }} {{ counterLabel() || ('ecl.common.OF' | translate) }} {{ getCounterTotal() }}\n </div>\n </div>\n\n <div class=\"ecl-carousel__teasers\" role=\"tablist\">\n @for (slide of slides(); let i = $index; track slide) {\n <button #teaserButton type=\"button\" class=\"ecl-carousel__teaser-button\" role=\"tab\"\n [attr.data-ecl-carousel-slide-index]=\"i\"\n [attr.aria-current]=\"i === getCurrentSlideIndex() ? true : null\"\n [attr.tabindex]=\"i === getCurrentSlideIndex() ? null : -1\"\n [style.display]=\"isTeaserVisible(i) ? null : 'none'\"\n [style.pointer-events]=\"isTeaserVisible(i) ? null : 'none'\"\n [style.order]=\"getTeaserOrder(i)\" (click)=\"onTeaserClick(i)\"\n (keydown)=\"onTeaserKeyboardPress($event, i)\">\n <span class=\"ecl-carousel__teaser-loading-bar\" aria-hidden=\"true\"\n [class.is-active]=\"(isPlaying() || isHoverPaused() || isPausedForced()) && i === getCurrentSlideIndex()\"\n [class.is-paused]=\"(isHoverPaused() || isPausedForced()) && i === getCurrentSlideIndex()\">\n <span #loadingBar class=\"ecl-carousel__teaser-loading-bar-completion\"></span>\n </span>\n <span class=\"ecl-carousel__teaser-content\">\n @if (slide.teaserText()) {\n <span class=\"ecl-carousel__teaser-title\">{{ slide.teaserText() }}</span>\n }\n </span>\n </button>\n }\n </div>\n </div>\n </div>\n</div>\n}\n<div #carouselContainer class=\"ecl-carousel__viewport\" [class.ecl-carousel-container--padded]=\"isPadded()\"\n emblaCarousel #embla=\"emblaCarousel\" [options]=\"emblaOptions\" [plugins]=\"emblaPlugins\"\n [subscribeToEvents]=\"emblaEvents\" (emblaChange)=\"onEmblaEvent($event)\"\n (mouseenter)=\"onViewportPause()\" (mouseleave)=\"onViewportResume()\"\n (focusin)=\"onViewportPause()\" (focusout)=\"onViewportResume()\">\n <div class=\"ecl-carousel__slides\">\n <ng-content></ng-content>\n </div>\n</div>\n" }]
821
+ }], ctorParameters: () => [], propDecorators: { ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "aria-label", required: false }] }], isAutoplay: [{ type: i0.Input, args: [{ isSignal: true, alias: "isAutoplay", required: false }] }], playInterval: [{ type: i0.Input, args: [{ isSignal: true, alias: "playInterval", required: false }] }], counterLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "counterLabel", required: false }] }], slide: [{ type: i0.Output, args: ["slide"] }], carouselContainerElement: [{ type: i0.ViewChild, args: ['carouselContainer', { isSignal: true }] }], slides: [{ type: i0.ContentChildren, args: [forwardRef(() => EclCarouselItemComponent), { isSignal: true }] }], embla: [{ type: i0.ViewChild, args: ['embla', { isSignal: true }] }], teaserButtons: [{ type: i0.ViewChildren, args: ['teaserButton', { isSignal: true }] }], loadingBars: [{ type: i0.ViewChildren, args: ['loadingBar', { isSignal: true }] }] } });
824
822
 
825
823
  const EUI_ECL_CAROUSEL = [
826
824
  EclCarouselComponent,