@tedi-design-system/angular 8.2.0-rc.4 → 8.2.0-rc.6

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.
@@ -1,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { input, computed, ViewEncapsulation, ChangeDetectionStrategy, Component, signal, inject, ElementRef, Directive, booleanAttribute, output, ViewContainerRef, Renderer2, effect, HostListener, Injectable, InjectionToken, model, isDevMode, forwardRef, contentChild, DestroyRef, afterRenderEffect, contentChildren, PLATFORM_ID, REQUEST, isSignal, Pipe, Injector, HostAttributeToken, viewChild, NgZone, viewChildren, afterNextRender, Optional, SkipSelf, linkedSignal, untracked, ViewChild, TemplateRef, numberAttribute, HostBinding, runInInjectionContext, ContentChildren, RendererStyleFlags2, ContentChild, makeEnvironmentProviders } from '@angular/core';
2
+ import { input, computed, ViewEncapsulation, ChangeDetectionStrategy, Component, signal, inject, ElementRef, Directive, booleanAttribute, output, ViewContainerRef, Renderer2, effect, HostListener, Injectable, InjectionToken, model, isDevMode, forwardRef, contentChild, DestroyRef, afterRenderEffect, contentChildren, PLATFORM_ID, REQUEST, isSignal, Pipe, Injector, HostAttributeToken, viewChild, NgZone, viewChildren, afterNextRender, ErrorHandler, Optional, SkipSelf, linkedSignal, untracked, ViewChild, TemplateRef, numberAttribute, HostBinding, runInInjectionContext, ContentChildren, RendererStyleFlags2, ContentChild, makeEnvironmentProviders } from '@angular/core';
3
3
  import { BreakpointObserver } from '@angular/cdk/layout';
4
4
  import * as i1 from '@angular/cdk/overlay';
5
5
  import { CdkOverlayOrigin, OverlayContainer, OverlayModule, Overlay, OverlayConfig, CdkConnectedOverlay } from '@angular/cdk/overlay';
@@ -11,6 +11,7 @@ import * as i2 from '@angular/cdk/a11y';
11
11
  import { _IdGenerator, CdkTrapFocus, A11yModule, LiveAnnouncer, ActiveDescendantKeyManager } from '@angular/cdk/a11y';
12
12
  import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
13
13
  import { Dialog } from '@angular/cdk/dialog';
14
+ import { isObservable, firstValueFrom } from 'rxjs';
14
15
  import * as i1$2 from '@angular/cdk/scrolling';
15
16
  import { CdkScrollable, CdkVirtualScrollViewport, ScrollingModule } from '@angular/cdk/scrolling';
16
17
  import * as i3 from '@angular/cdk/listbox';
@@ -8691,15 +8692,50 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.24", ngImpo
8691
8692
  /**
8692
8693
  * Reference to a modal opened via `ModalService`.
8693
8694
  * Provides methods to close the modal and observe its lifecycle.
8695
+ * The opener and the content share the same instance.
8694
8696
  */
8695
8697
  class ModalRef {
8696
8698
  dialogRef;
8697
- constructor(dialogRef) {
8699
+ errorHandler;
8700
+ /** Checked before every close, except `ModalService.closeAll()` and navigation. */
8701
+ canClose = null;
8702
+ pendingGuard = false;
8703
+ constructor(dialogRef, errorHandler = new ErrorHandler()) {
8698
8704
  this.dialogRef = dialogRef;
8705
+ this.errorHandler = errorHandler;
8699
8706
  }
8700
- /** Close the modal, optionally returning a result. */
8707
+ /** Close the modal, optionally returning a result. Subject to `canClose`. */
8701
8708
  close(result) {
8702
- this.dialogRef.close(result);
8709
+ this.requestClose("programmatic", result);
8710
+ }
8711
+ /** @internal */
8712
+ requestClose(reason, result) {
8713
+ if (this.pendingGuard)
8714
+ return;
8715
+ let verdict;
8716
+ try {
8717
+ verdict = this.canClose ? this.canClose(result, reason) : true;
8718
+ }
8719
+ catch (error) {
8720
+ this.errorHandler.handleError(error);
8721
+ return;
8722
+ }
8723
+ if (typeof verdict === "boolean") {
8724
+ if (verdict)
8725
+ this.dialogRef.close(result);
8726
+ return;
8727
+ }
8728
+ this.pendingGuard = true;
8729
+ const decision = isObservable(verdict)
8730
+ ? firstValueFrom(verdict, { defaultValue: false })
8731
+ : verdict;
8732
+ decision
8733
+ .then((allowed) => {
8734
+ if (allowed)
8735
+ this.dialogRef.close(result);
8736
+ })
8737
+ .catch((error) => this.errorHandler.handleError(error))
8738
+ .finally(() => (this.pendingGuard = false));
8703
8739
  }
8704
8740
  /** Observable that emits when the modal is closed, with the optional result value. */
8705
8741
  get closed() {
@@ -8755,6 +8791,7 @@ class ModalService {
8755
8791
  overlay = inject(Overlay);
8756
8792
  injector = inject(Injector);
8757
8793
  idGenerator = inject(_IdGenerator);
8794
+ errorHandler = inject(ErrorHandler);
8758
8795
  /**
8759
8796
  * Open a modal dialog with the given component as content.
8760
8797
  *
@@ -8777,6 +8814,7 @@ class ModalService {
8777
8814
  const { data, size = "default", width = "sm", position = "center", scrollBehavior = "content", closeOnBackdropClick = true, closeOnEscape = true, fullscreen = false, maxWidth, ariaLabel, ariaLabelledBy, } = config;
8778
8815
  const panelClasses = this.buildPanelClasses(size, width, position, scrollBehavior, fullscreen);
8779
8816
  const isPresetWidth = WIDTH_PRESETS.includes(width);
8817
+ let modalRef;
8780
8818
  const dialogRef = this.dialog.open(component, {
8781
8819
  data,
8782
8820
  panelClass: panelClasses,
@@ -8791,7 +8829,10 @@ class ModalService {
8791
8829
  positionStrategy: this.buildPositionStrategy(position, scrollBehavior),
8792
8830
  scrollStrategy: this.overlay.scrollStrategies.block(),
8793
8831
  providers: (ref) => [
8794
- { provide: ModalRef, useValue: new ModalRef(ref) },
8832
+ {
8833
+ provide: ModalRef,
8834
+ useValue: (modalRef = new ModalRef(ref, this.errorHandler)),
8835
+ },
8795
8836
  { provide: MODAL_DATA, useValue: data },
8796
8837
  { provide: MODAL_SIZE, useValue: signal(size) },
8797
8838
  ],
@@ -8802,13 +8843,13 @@ class ModalService {
8802
8843
  if (maxWidth) {
8803
8844
  dialogRef.overlayRef.overlayElement.style.setProperty("--_tedi-modal-max-width", maxWidth);
8804
8845
  }
8805
- this.setupDialogBehavior(dialogRef, scrollBehavior, closeOnBackdropClick, closeOnEscape);
8846
+ this.setupDialogBehavior(dialogRef, modalRef, scrollBehavior, closeOnBackdropClick, closeOnEscape);
8806
8847
  if (!ariaLabel && !ariaLabelledBy) {
8807
8848
  this.labelFromHeading(dialogRef);
8808
8849
  }
8809
- return new ModalRef(dialogRef);
8850
+ return modalRef;
8810
8851
  }
8811
- /** Close all open modals. */
8852
+ /** Close all open modals, ignoring `canClose`. */
8812
8853
  closeAll() {
8813
8854
  this.dialog.closeAll();
8814
8855
  }
@@ -8824,7 +8865,8 @@ class ModalService {
8824
8865
  labelFromHeading(dialogRef) {
8825
8866
  afterNextRender(() => {
8826
8867
  // The container, not the pane, is what carries role="dialog".
8827
- const container = dialogRef.overlayRef.overlayElement.querySelector("cdk-dialog-container");
8868
+ // The pane is null if the modal closed before rendering.
8869
+ const container = dialogRef.overlayRef.overlayElement?.querySelector("cdk-dialog-container");
8828
8870
  if (!container)
8829
8871
  return;
8830
8872
  const headingId = resolveModalHeadingId(container, this.idGenerator);
@@ -8855,19 +8897,19 @@ class ModalService {
8855
8897
  }
8856
8898
  return classes;
8857
8899
  }
8858
- setupDialogBehavior(dialogRef, scrollBehavior, closeOnBackdropClick, closeOnEscape) {
8900
+ setupDialogBehavior(dialogRef, modalRef, scrollBehavior, closeOnBackdropClick, closeOnEscape) {
8859
8901
  if (scrollBehavior === "page") {
8860
8902
  const host = dialogRef.overlayRef.hostElement;
8861
8903
  host.style.overflow = "auto";
8862
8904
  host.style.paddingBlock = "var(--layout-grid-gutters-16)";
8863
8905
  }
8864
8906
  if (closeOnBackdropClick) {
8865
- dialogRef.backdropClick.subscribe(() => dialogRef.close());
8907
+ dialogRef.backdropClick.subscribe(() => modalRef.requestClose("backdrop"));
8866
8908
  }
8867
8909
  if (closeOnEscape) {
8868
8910
  dialogRef.keydownEvents.subscribe((event) => {
8869
8911
  if (event.key === "Escape") {
8870
- dialogRef.close();
8912
+ modalRef.requestClose("escape");
8871
8913
  }
8872
8914
  });
8873
8915
  }
@@ -9123,7 +9165,7 @@ class ModalHeaderComponent {
9123
9165
  }, ...(ngDevMode ? [{ debugName: "effectiveCloseButtonSize" }] : []));
9124
9166
  closeModal() {
9125
9167
  if (this.modalRef) {
9126
- this.modalRef.close();
9168
+ this.modalRef.requestClose("close-button");
9127
9169
  }
9128
9170
  else if (this.modal) {
9129
9171
  this.modal.open.set(false);
@@ -15612,6 +15654,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.24", ngImpo
15612
15654
  }]
15613
15655
  }] });
15614
15656
 
15657
+ /** Ignore tiny calculation differences when deciding the carousel’s position. */
15658
+ const EPSILON = 0.001;
15615
15659
  class CarouselContentComponent {
15616
15660
  /** Slides per view (minimum 1, can be fractional, e.g. 1.25 for peeking) */
15617
15661
  slidesPerView = input({ xs: 1 }, ...(ngDevMode ? [{ debugName: "slidesPerView", transform: (v) => breakpointInput(v) }] : [{ transform: (v) => breakpointInput(v) }]));
@@ -15621,6 +15665,12 @@ class CarouselContentComponent {
15621
15665
  fade = input(false, ...(ngDevMode ? [{ debugName: "fade" }] : []));
15622
15666
  /** Transition duration in ms */
15623
15667
  transitionMs = input(400, ...(ngDevMode ? [{ debugName: "transitionMs" }] : []));
15668
+ /**
15669
+ * Whether navigation wraps around at the ends. When `false`, it stops at the
15670
+ * first and last reachable positions and disables the corresponding arrow.
15671
+ * @default true
15672
+ */
15673
+ loop = input(true, ...(ngDevMode ? [{ debugName: "loop", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
15624
15674
  /** Accessible label for the carousel region. Falls back to the `carousel` translation. */
15625
15675
  ariaLabel = input(...(ngDevMode ? [undefined, { debugName: "ariaLabel" }] : []));
15626
15676
  translationService = inject(TediTranslationService);
@@ -15684,18 +15734,46 @@ class CarouselContentComponent {
15684
15734
  return this.gap().xs;
15685
15735
  }
15686
15736
  }, ...(ngDevMode ? [{ debugName: "currentGap" }] : []));
15687
- buffer = computed(() => this.slides().length, ...(ngDevMode ? [{ debugName: "buffer" }] : []));
15737
+ buffer = computed(() => (this.loop() ? this.slides().length : 0), ...(ngDevMode ? [{ debugName: "buffer" }] : []));
15738
+ /** Furthest track position in bounded mode; may be fractional to show the last slide fully. */
15739
+ maxIndex = computed(() => Math.max(0, this.slides().length - this.currentSlidesPerView()), ...(ngDevMode ? [{ debugName: "maxIndex" }] : []));
15740
+ /** Number of indicator positions: one per slide when looping, or one per reachable stop when bounded. */
15741
+ positionCount = computed(() => {
15742
+ if (this.loop() || !this.slides().length) {
15743
+ return this.slides().length;
15744
+ }
15745
+ return Math.ceil(this.maxIndex() - EPSILON) + 1;
15746
+ }, ...(ngDevMode ? [{ debugName: "positionCount" }] : []));
15747
+ /** Index of the active indicator; a fractional final stop gets its own index. */
15748
+ activePosition = computed(() => {
15749
+ if (this.loop()) {
15750
+ return this.slideIndex();
15751
+ }
15752
+ const nearest = this.snap(this.trackIndex());
15753
+ return nearest >= this.maxIndex() - EPSILON
15754
+ ? this.positionCount() - 1
15755
+ : Math.round(nearest);
15756
+ }, ...(ngDevMode ? [{ debugName: "activePosition" }] : []));
15757
+ /** Index of the leftmost visible slide, skipping a partially visible one in bounded mode. */
15688
15758
  slideIndex = computed(() => {
15689
15759
  const slidesCount = this.slides().length;
15690
15760
  if (slidesCount === 0) {
15691
15761
  return 0;
15692
15762
  }
15763
+ if (!this.loop()) {
15764
+ return Math.min(Math.max(Math.ceil(this.trackIndex() - EPSILON), 0), slidesCount - 1);
15765
+ }
15693
15766
  const i = Math.floor(this.trackIndex());
15694
15767
  return ((i % slidesCount) + slidesCount) % slidesCount;
15695
15768
  }, ...(ngDevMode ? [{ debugName: "slideIndex" }] : []));
15696
15769
  renderedActiveIndex = computed(() => {
15770
+ if (!this.loop()) {
15771
+ return this.trackIndex();
15772
+ }
15697
15773
  return this.trackIndex() - this.windowBase() + this.buffer();
15698
15774
  }, ...(ngDevMode ? [{ debugName: "renderedActiveIndex" }] : []));
15775
+ canPrev = computed(() => this.loop() || this.trackIndex() > EPSILON, ...(ngDevMode ? [{ debugName: "canPrev" }] : []));
15776
+ canNext = computed(() => this.loop() || this.trackIndex() < this.maxIndex() - EPSILON, ...(ngDevMode ? [{ debugName: "canNext" }] : []));
15699
15777
  /**
15700
15778
  * Checks if a slide at the given rendered index is currently visible in the viewport.
15701
15779
  * Used to determine which slides should be accessible to screen readers.
@@ -15711,6 +15789,9 @@ class CarouselContentComponent {
15711
15789
  if (!slidesCount) {
15712
15790
  return [];
15713
15791
  }
15792
+ if (!this.loop()) {
15793
+ return Array.from({ length: slidesCount }, (_, i) => i);
15794
+ }
15714
15795
  const total = 2 * this.buffer() + Math.ceil(this.currentSlidesPerView());
15715
15796
  const start = this.windowBase() - this.buffer();
15716
15797
  return Array.from({ length: total }, (_, i) => (((start + i) % slidesCount) + slidesCount) % slidesCount);
@@ -15743,7 +15824,7 @@ class CarouselContentComponent {
15743
15824
  }
15744
15825
  const totalGapWidth = gap * (slidesPerView - 1);
15745
15826
  const slideWidth = (viewportWidth - totalGapWidth) / slidesPerView;
15746
- const offsetSlides = this.trackIndex() - this.windowBase() + this.buffer();
15827
+ const offsetSlides = this.renderedActiveIndex();
15747
15828
  const translateX = -offsetSlides * (slideWidth + gap);
15748
15829
  return {
15749
15830
  gap: `${gap}px`,
@@ -15753,6 +15834,16 @@ class CarouselContentComponent {
15753
15834
  : "none",
15754
15835
  };
15755
15836
  }, ...(ngDevMode ? [{ debugName: "trackStyle" }] : []));
15837
+ constructor() {
15838
+ // Re-clamp without animation when bounded mode starts or its limit changes.
15839
+ // Don't track the current position: normal navigation must not rerun this effect.
15840
+ effect(() => {
15841
+ if (!this.loop()) {
15842
+ const max = this.maxIndex();
15843
+ untracked(() => this.clampIntoRange(max));
15844
+ }
15845
+ });
15846
+ }
15756
15847
  locked = false;
15757
15848
  dragging = false;
15758
15849
  pendingFocus = false;
@@ -15789,9 +15880,7 @@ class CarouselContentComponent {
15789
15880
  }
15790
15881
  const deltaSlides = delta / cellWidth;
15791
15882
  this.scrollDelta += deltaSlides;
15792
- const maxDelta = this.buffer() * 0.9;
15793
- const min = this.windowBase() - maxDelta;
15794
- const max = this.windowBase() + maxDelta;
15883
+ const { min, max } = this.dragBounds(this.windowBase());
15795
15884
  const unclamped = this.trackIndex() + deltaSlides;
15796
15885
  const clamped = Math.min(Math.max(unclamped, min), max);
15797
15886
  const wasClamped = clamped !== unclamped;
@@ -15800,17 +15889,20 @@ class CarouselContentComponent {
15800
15889
  clearTimeout(this.wheelTimeout);
15801
15890
  this.wheelTimeout = setTimeout(() => {
15802
15891
  this.animate.set(true);
15892
+ // Recalculate bounds: the viewport or slide count may have changed before snapping.
15893
+ const { min, max } = this.dragBounds(this.windowBase());
15803
15894
  const direction = Math.sign(this.scrollDelta);
15804
15895
  const current = this.trackIndex();
15805
- let snapIndex = Math.round(current);
15896
+ let snapIndex = this.snap(current);
15806
15897
  if (Math.abs(this.scrollDelta) > 0.3) {
15807
15898
  snapIndex = direction > 0 ? Math.ceil(current) : Math.floor(current);
15808
15899
  }
15900
+ // If scrolling hit a limit, snap inward when looping or to the exact bound otherwise.
15809
15901
  if (wasClamped) {
15810
15902
  if (current <= min)
15811
- snapIndex = Math.ceil(min);
15903
+ snapIndex = this.loop() ? Math.ceil(min) : min;
15812
15904
  if (current >= max)
15813
- snapIndex = Math.floor(max);
15905
+ snapIndex = this.loop() ? Math.floor(max) : max;
15814
15906
  }
15815
15907
  const finalIndex = Math.min(Math.max(snapIndex, min), max);
15816
15908
  this.trackIndex.set(finalIndex);
@@ -15851,7 +15943,9 @@ class CarouselContentComponent {
15851
15943
  this.animate.set(false);
15852
15944
  this.startX = ev.clientX;
15853
15945
  this.startIndex = this.trackIndex();
15854
- this.windowBase.set(Math.floor(this.startIndex));
15946
+ if (this.loop()) {
15947
+ this.windowBase.set(Math.floor(this.startIndex));
15948
+ }
15855
15949
  }
15856
15950
  onPointerMove(ev) {
15857
15951
  if (!this.dragging) {
@@ -15867,9 +15961,7 @@ class CarouselContentComponent {
15867
15961
  }
15868
15962
  const deltaSlides = dx / cellWidth;
15869
15963
  const targetIndex = this.startIndex - deltaSlides;
15870
- const maxDelta = this.buffer() * 0.9;
15871
- const min = this.windowBase() - maxDelta;
15872
- const max = this.windowBase() + maxDelta;
15964
+ const { min, max } = this.dragBounds(Math.floor(this.startIndex));
15873
15965
  const clamped = Math.min(Math.max(targetIndex, min), max);
15874
15966
  this.trackIndex.set(clamped);
15875
15967
  }
@@ -15879,13 +15971,14 @@ class CarouselContentComponent {
15879
15971
  }
15880
15972
  this.dragging = false;
15881
15973
  this.animate.set(true);
15882
- this.trackIndex.set(Math.round(this.trackIndex()));
15974
+ this.trackIndex.set(this.snap(this.trackIndex()));
15883
15975
  }
15884
15976
  ngAfterViewInit() {
15885
15977
  const viewport = this.host.nativeElement;
15886
15978
  this.viewportWidth.set(viewport.clientWidth);
15887
- this.ro = new ResizeObserver(() => {
15888
- this.viewportWidth.set(viewport.clientWidth);
15979
+ // Use the fractional width to keep track translation aligned with CSS-sized slides.
15980
+ this.ro = new ResizeObserver(([entry]) => {
15981
+ this.viewportWidth.set(entry.contentRect.width);
15889
15982
  });
15890
15983
  this.ro.observe(viewport);
15891
15984
  }
@@ -15893,20 +15986,21 @@ class CarouselContentComponent {
15893
15986
  this.ro?.disconnect();
15894
15987
  }
15895
15988
  next() {
15896
- if (!this.slides().length || this.locked) {
15989
+ if (!this.slides().length || this.locked || !this.canNext()) {
15897
15990
  return;
15898
15991
  }
15899
15992
  this.animate.set(true);
15900
- this.trackIndex.update((i) => i + 1);
15993
+ // Move to the next whole position, clamping to a fractional final position.
15994
+ this.trackIndex.update((i) => this.clampIndex(Math.floor(i + EPSILON) + 1));
15901
15995
  this.lockNavigation();
15902
15996
  this.announceSlideChange();
15903
15997
  }
15904
15998
  prev() {
15905
- if (!this.slides().length || this.locked) {
15999
+ if (!this.slides().length || this.locked || !this.canPrev()) {
15906
16000
  return;
15907
16001
  }
15908
16002
  this.animate.set(true);
15909
- this.trackIndex.update((i) => i - 1);
16003
+ this.trackIndex.update((i) => this.clampIndex(Math.ceil(i - EPSILON) - 1));
15910
16004
  this.lockNavigation();
15911
16005
  this.announceSlideChange();
15912
16006
  }
@@ -15915,11 +16009,16 @@ class CarouselContentComponent {
15915
16009
  if (!slidesCount || this.locked) {
15916
16010
  return;
15917
16011
  }
15918
- const current = this.slideIndex();
15919
- const normalized = ((index % slidesCount) + slidesCount) % slidesCount;
15920
- const delta = normalized - current;
15921
16012
  this.animate.set(true);
15922
- this.trackIndex.update((i) => i + delta);
16013
+ if (this.loop()) {
16014
+ const current = this.slideIndex();
16015
+ const normalized = ((index % slidesCount) + slidesCount) % slidesCount;
16016
+ const delta = normalized - current;
16017
+ this.trackIndex.update((i) => i + delta);
16018
+ }
16019
+ else {
16020
+ this.trackIndex.set(this.clampIndex(index));
16021
+ }
15923
16022
  if (options?.focusSlide) {
15924
16023
  // Focus after transition completes so DOM positions are stable
15925
16024
  this.pendingFocus = true;
@@ -15934,7 +16033,8 @@ class CarouselContentComponent {
15934
16033
  */
15935
16034
  focusActiveSlide() {
15936
16035
  setTimeout(() => {
15937
- const activeIndex = this.renderedActiveIndex();
16036
+ // A bounded track can rest at a fractional index; focus the first fully visible slide.
16037
+ const activeIndex = Math.ceil(this.renderedActiveIndex() - EPSILON);
15938
16038
  const slideElement = this.slideElements()[activeIndex];
15939
16039
  if (slideElement) {
15940
16040
  slideElement.nativeElement.focus({ preventScroll: true });
@@ -15954,14 +16054,42 @@ class CarouselContentComponent {
15954
16054
  this.focusActiveSlide();
15955
16055
  }
15956
16056
  }
16057
+ clampIntoRange(max) {
16058
+ const current = this.trackIndex();
16059
+ const clamped = Math.min(Math.max(current, 0), max);
16060
+ if (clamped !== current) {
16061
+ this.animate.set(false);
16062
+ this.trackIndex.set(clamped);
16063
+ this.windowBase.set(Math.floor(clamped));
16064
+ }
16065
+ }
16066
+ clampIndex(value) {
16067
+ return this.loop() ? value : Math.min(Math.max(value, 0), this.maxIndex());
16068
+ }
16069
+ /** Snap to the nearest reachable position, including a fractional end. */
16070
+ snap(value) {
16071
+ if (this.loop()) {
16072
+ return Math.round(value);
16073
+ }
16074
+ const below = this.clampIndex(Math.floor(value));
16075
+ const above = this.clampIndex(Math.ceil(value));
16076
+ return value - below <= above - value ? below : above;
16077
+ }
16078
+ /**
16079
+ * Range the track may be dragged or wheeled within. A looping track stays inside
16080
+ * its rendered duplicate buffer around `base`; a bounded one stops at the ends.
16081
+ */
16082
+ dragBounds(base) {
16083
+ if (!this.loop()) {
16084
+ return { min: 0, max: this.maxIndex() };
16085
+ }
16086
+ const maxDelta = this.buffer() * 0.9;
16087
+ return { min: base - maxDelta, max: base + maxDelta };
16088
+ }
15957
16089
  lockNavigation() {
15958
16090
  this.locked = true;
15959
16091
  setTimeout(() => (this.locked = false), this.transitionMs());
15960
16092
  }
15961
- /**
15962
- * Announces the current slide position to screen readers via LiveAnnouncer.
15963
- * Called after navigation to inform users of the slide change.
15964
- */
15965
16093
  announceSlideChange() {
15966
16094
  setTimeout(() => {
15967
16095
  const slideNumber = this.slideIndex() + 1;
@@ -15971,7 +16099,7 @@ class CarouselContentComponent {
15971
16099
  }, 100);
15972
16100
  }
15973
16101
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.24", ngImport: i0, type: CarouselContentComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
15974
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.24", type: CarouselContentComponent, isStandalone: true, selector: "tedi-carousel-content", inputs: { slidesPerView: { classPropertyName: "slidesPerView", publicName: "slidesPerView", isSignal: true, isRequired: false, transformFunction: null }, gap: { classPropertyName: "gap", publicName: "gap", isSignal: true, isRequired: false, transformFunction: null }, fade: { classPropertyName: "fade", publicName: "fade", isSignal: true, isRequired: false, transformFunction: null }, transitionMs: { classPropertyName: "transitionMs", publicName: "transitionMs", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "tabindex": "0", "role": "region", "aria-roledescription": "carousel", "aria-live": "off" }, listeners: { "scroll": "onScroll()", "wheel": "onWheel($event)", "keydown": "onKeyDown($event)", "pointerdown": "onPointerDown($event)", "pointermove": "onPointerMove($event)", "pointerup": "onPointerUp()", "pointercancel": "onPointerUp()", "lostpointercapture": "onPointerUp()" }, properties: { "attr.aria-label": "resolvedAriaLabel()", "class": "classes()" } }, queries: [{ propertyName: "slides", predicate: CarouselSlideDirective, isSignal: true }], viewQueries: [{ propertyName: "track", first: true, predicate: ["track"], descendants: true, isSignal: true }, { propertyName: "slideElements", predicate: ["slide"], descendants: true, isSignal: true }], ngImport: i0, template: "<div\n #track\n class=\"tedi-carousel__track\"\n [style]=\"trackStyle()\"\n (transitionend)=\"onTransitionEnd($event)\"\n>\n @for (idx of renderedIndices(); track $index) {\n <div\n #slide\n class=\"tedi-carousel__slide\"\n [attr.role]=\"isSlideVisible($index) ? 'group' : 'presentation'\"\n aria-roledescription=\"slide\"\n [attr.aria-label]=\"\n translationService.track('carousel.slide', idx + 1, slides().length)()\n \"\n [attr.aria-current]=\"$index === renderedActiveIndex() ? 'true' : null\"\n [attr.aria-hidden]=\"isSlideVisible($index) ? null : 'true'\"\n [attr.tabindex]=\"isSlideVisible($index) ? '-1' : null\"\n [style.flex]=\"slideFlex()\"\n >\n <ng-container *ngTemplateOutlet=\"slides()[idx].template\"></ng-container>\n </div>\n }\n</div>\n", styles: [".tedi-carousel__content{position:relative;width:100%;overflow:hidden;overscroll-behavior:contain;touch-action:pan-y;cursor:grab}.tedi-carousel__content:focus-visible{outline:var(--tedi-borders-02) solid var(--tedi-primary-500);outline-offset:calc(-1 * var(--tedi-borders-03))}.tedi-carousel__content:active{cursor:grabbing}.tedi-carousel__content--fade-right{-webkit-mask-image:linear-gradient(to right,black 90%,transparent 100%);mask-image:linear-gradient(to right,black 90%,transparent 100%)}.tedi-carousel__content--fade-x{-webkit-mask-image:linear-gradient(to right,black 85%,transparent 100%),linear-gradient(to left,black 85%,transparent 100%);mask-image:linear-gradient(to right,black 85%,transparent 100%),linear-gradient(to left,black 85%,transparent 100%);-webkit-mask-composite:source-in;mask-composite:intersect}.tedi-carousel__track{display:flex;will-change:transform}.tedi-carousel__slide{-webkit-user-select:none;user-select:none;scroll-snap-align:none;scroll-margin:0;-webkit-user-drag:none}.tedi-carousel__slide:focus{outline:none}.tedi-carousel__slide:focus-visible{outline:var(--tedi-borders-02) solid var(--tedi-primary-500);outline-offset:calc(-1 * var(--tedi-borders-03))}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
16102
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.24", type: CarouselContentComponent, isStandalone: true, selector: "tedi-carousel-content", inputs: { slidesPerView: { classPropertyName: "slidesPerView", publicName: "slidesPerView", isSignal: true, isRequired: false, transformFunction: null }, gap: { classPropertyName: "gap", publicName: "gap", isSignal: true, isRequired: false, transformFunction: null }, fade: { classPropertyName: "fade", publicName: "fade", isSignal: true, isRequired: false, transformFunction: null }, transitionMs: { classPropertyName: "transitionMs", publicName: "transitionMs", isSignal: true, isRequired: false, transformFunction: null }, loop: { classPropertyName: "loop", publicName: "loop", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "tabindex": "0", "role": "region", "aria-roledescription": "carousel", "aria-live": "off" }, listeners: { "scroll": "onScroll()", "wheel": "onWheel($event)", "keydown": "onKeyDown($event)", "pointerdown": "onPointerDown($event)", "pointermove": "onPointerMove($event)", "pointerup": "onPointerUp()", "pointercancel": "onPointerUp()", "lostpointercapture": "onPointerUp()" }, properties: { "attr.aria-label": "resolvedAriaLabel()", "class": "classes()" } }, queries: [{ propertyName: "slides", predicate: CarouselSlideDirective, isSignal: true }], viewQueries: [{ propertyName: "track", first: true, predicate: ["track"], descendants: true, isSignal: true }, { propertyName: "slideElements", predicate: ["slide"], descendants: true, isSignal: true }], ngImport: i0, template: "<div\n #track\n class=\"tedi-carousel__track\"\n [style]=\"trackStyle()\"\n (transitionend)=\"onTransitionEnd($event)\"\n>\n @for (idx of renderedIndices(); track $index) {\n <div\n #slide\n class=\"tedi-carousel__slide\"\n [attr.role]=\"isSlideVisible($index) ? 'group' : 'presentation'\"\n aria-roledescription=\"slide\"\n [attr.aria-label]=\"\n translationService.track('carousel.slide', idx + 1, slides().length)()\n \"\n [attr.aria-current]=\"$index === renderedActiveIndex() ? 'true' : null\"\n [attr.aria-hidden]=\"isSlideVisible($index) ? null : 'true'\"\n [attr.tabindex]=\"isSlideVisible($index) ? '-1' : null\"\n [style.flex]=\"slideFlex()\"\n >\n <ng-container *ngTemplateOutlet=\"slides()[idx].template\"></ng-container>\n </div>\n }\n</div>\n", styles: [".tedi-carousel__content{position:relative;width:100%;overflow:hidden;overscroll-behavior:contain;touch-action:pan-y;cursor:grab}.tedi-carousel__content:focus-visible{outline:var(--tedi-borders-02) solid var(--tedi-primary-500);outline-offset:calc(-1 * var(--tedi-borders-03))}.tedi-carousel__content:active{cursor:grabbing}.tedi-carousel__content--fade-right{-webkit-mask-image:linear-gradient(to right,black 90%,transparent 100%);mask-image:linear-gradient(to right,black 90%,transparent 100%)}.tedi-carousel__content--fade-x{-webkit-mask-image:linear-gradient(to right,black 85%,transparent 100%),linear-gradient(to left,black 85%,transparent 100%);mask-image:linear-gradient(to right,black 85%,transparent 100%),linear-gradient(to left,black 85%,transparent 100%);-webkit-mask-composite:source-in;mask-composite:intersect}.tedi-carousel__track{display:flex;will-change:transform}.tedi-carousel__slide{-webkit-user-select:none;user-select:none;scroll-snap-align:none;scroll-margin:0;-webkit-user-drag:none}.tedi-carousel__slide:focus{outline:none}.tedi-carousel__slide:focus-visible{outline:var(--tedi-borders-02) solid var(--tedi-primary-500);outline-offset:calc(-1 * var(--tedi-borders-03))}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
15975
16103
  }
15976
16104
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.24", ngImport: i0, type: CarouselContentComponent, decorators: [{
15977
16105
  type: Component,
@@ -15983,7 +16111,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.24", ngImpo
15983
16111
  "aria-live": "off",
15984
16112
  "[class]": "classes()",
15985
16113
  }, template: "<div\n #track\n class=\"tedi-carousel__track\"\n [style]=\"trackStyle()\"\n (transitionend)=\"onTransitionEnd($event)\"\n>\n @for (idx of renderedIndices(); track $index) {\n <div\n #slide\n class=\"tedi-carousel__slide\"\n [attr.role]=\"isSlideVisible($index) ? 'group' : 'presentation'\"\n aria-roledescription=\"slide\"\n [attr.aria-label]=\"\n translationService.track('carousel.slide', idx + 1, slides().length)()\n \"\n [attr.aria-current]=\"$index === renderedActiveIndex() ? 'true' : null\"\n [attr.aria-hidden]=\"isSlideVisible($index) ? null : 'true'\"\n [attr.tabindex]=\"isSlideVisible($index) ? '-1' : null\"\n [style.flex]=\"slideFlex()\"\n >\n <ng-container *ngTemplateOutlet=\"slides()[idx].template\"></ng-container>\n </div>\n }\n</div>\n", styles: [".tedi-carousel__content{position:relative;width:100%;overflow:hidden;overscroll-behavior:contain;touch-action:pan-y;cursor:grab}.tedi-carousel__content:focus-visible{outline:var(--tedi-borders-02) solid var(--tedi-primary-500);outline-offset:calc(-1 * var(--tedi-borders-03))}.tedi-carousel__content:active{cursor:grabbing}.tedi-carousel__content--fade-right{-webkit-mask-image:linear-gradient(to right,black 90%,transparent 100%);mask-image:linear-gradient(to right,black 90%,transparent 100%)}.tedi-carousel__content--fade-x{-webkit-mask-image:linear-gradient(to right,black 85%,transparent 100%),linear-gradient(to left,black 85%,transparent 100%);mask-image:linear-gradient(to right,black 85%,transparent 100%),linear-gradient(to left,black 85%,transparent 100%);-webkit-mask-composite:source-in;mask-composite:intersect}.tedi-carousel__track{display:flex;will-change:transform}.tedi-carousel__slide{-webkit-user-select:none;user-select:none;scroll-snap-align:none;scroll-margin:0;-webkit-user-drag:none}.tedi-carousel__slide:focus{outline:none}.tedi-carousel__slide:focus-visible{outline:var(--tedi-borders-02) solid var(--tedi-primary-500);outline-offset:calc(-1 * var(--tedi-borders-03))}\n"] }]
15986
- }], propDecorators: { slidesPerView: [{ type: i0.Input, args: [{ isSignal: true, alias: "slidesPerView", required: false }] }], gap: [{ type: i0.Input, args: [{ isSignal: true, alias: "gap", required: false }] }], fade: [{ type: i0.Input, args: [{ isSignal: true, alias: "fade", required: false }] }], transitionMs: [{ type: i0.Input, args: [{ isSignal: true, alias: "transitionMs", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], track: [{ type: i0.ViewChild, args: ["track", { isSignal: true }] }], slideElements: [{ type: i0.ViewChildren, args: ["slide", { isSignal: true }] }], slides: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => CarouselSlideDirective), { isSignal: true }] }], onScroll: [{
16114
+ }], ctorParameters: () => [], propDecorators: { slidesPerView: [{ type: i0.Input, args: [{ isSignal: true, alias: "slidesPerView", required: false }] }], gap: [{ type: i0.Input, args: [{ isSignal: true, alias: "gap", required: false }] }], fade: [{ type: i0.Input, args: [{ isSignal: true, alias: "fade", required: false }] }], transitionMs: [{ type: i0.Input, args: [{ isSignal: true, alias: "transitionMs", required: false }] }], loop: [{ type: i0.Input, args: [{ isSignal: true, alias: "loop", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], track: [{ type: i0.ViewChild, args: ["track", { isSignal: true }] }], slideElements: [{ type: i0.ViewChildren, args: ["slide", { isSignal: true }] }], slides: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => CarouselSlideDirective), { isSignal: true }] }], onScroll: [{
15987
16115
  type: HostListener,
15988
16116
  args: ["scroll"]
15989
16117
  }], onWheel: [{
@@ -16012,11 +16140,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.24", ngImpo
16012
16140
  class CarouselComponent {
16013
16141
  carouselContent = contentChild.required(CarouselContentComponent);
16014
16142
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.24", ngImport: i0, type: CarouselComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
16015
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "20.3.24", type: CarouselComponent, isStandalone: true, selector: "tedi-carousel", queries: [{ propertyName: "carouselContent", first: true, predicate: CarouselContentComponent, descendants: true, isSignal: true }], ngImport: i0, template: "<ng-content select=\"tedi-carousel-header\" />\n<ng-content select=\"tedi-carousel-content\" />\n<ng-content select=\"tedi-carousel-footer\" />\n", styles: ["tedi-carousel{display:flex;flex-direction:column;gap:var(--layout-grid-gutters-08)}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
16143
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "20.3.24", type: CarouselComponent, isStandalone: true, selector: "tedi-carousel", queries: [{ propertyName: "carouselContent", first: true, predicate: CarouselContentComponent, descendants: true, isSignal: true }], ngImport: i0, template: "<ng-content select=\"tedi-carousel-header\" />\n<div class=\"tedi-carousel__body\">\n <ng-content select=\"tedi-carousel-content\" />\n <ng-content select=\"tedi-carousel-navigation\" />\n</div>\n<ng-content select=\"tedi-carousel-footer\" />\n", styles: ["tedi-carousel{display:flex;flex-direction:column;gap:var(--layout-grid-gutters-08)}.tedi-carousel__body{position:relative;display:flex;flex-direction:column}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
16016
16144
  }
16017
16145
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.24", ngImport: i0, type: CarouselComponent, decorators: [{
16018
16146
  type: Component,
16019
- args: [{ standalone: true, selector: "tedi-carousel", encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-content select=\"tedi-carousel-header\" />\n<ng-content select=\"tedi-carousel-content\" />\n<ng-content select=\"tedi-carousel-footer\" />\n", styles: ["tedi-carousel{display:flex;flex-direction:column;gap:var(--layout-grid-gutters-08)}\n"] }]
16147
+ args: [{ standalone: true, selector: "tedi-carousel", encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-content select=\"tedi-carousel-header\" />\n<div class=\"tedi-carousel__body\">\n <ng-content select=\"tedi-carousel-content\" />\n <ng-content select=\"tedi-carousel-navigation\" />\n</div>\n<ng-content select=\"tedi-carousel-footer\" />\n", styles: ["tedi-carousel{display:flex;flex-direction:column;gap:var(--layout-grid-gutters-08)}.tedi-carousel__body{position:relative;display:flex;flex-direction:column}\n"] }]
16020
16148
  }], propDecorators: { carouselContent: [{ type: i0.ContentChild, args: [i0.forwardRef(() => CarouselContentComponent), { isSignal: true }] }] } });
16021
16149
 
16022
16150
  class CarouselFooterComponent {
@@ -16044,11 +16172,11 @@ class CarouselIndicatorsComponent {
16044
16172
  variant = input("dots", ...(ngDevMode ? [{ debugName: "variant" }] : []));
16045
16173
  translationService = inject(TediTranslationService);
16046
16174
  carousel = inject(CarouselComponent);
16047
- indicatorsArray = computed(() => Array.from({ length: this.carousel.carouselContent().slides().length }, (_, i) => ({
16175
+ indicatorsArray = computed(() => Array.from({ length: this.carousel.carouselContent().positionCount() }, (_, i) => ({
16048
16176
  index: i,
16049
- active: this.carousel.carouselContent().slideIndex() === i,
16177
+ active: this.carousel.carouselContent().activePosition() === i,
16050
16178
  })), ...(ngDevMode ? [{ debugName: "indicatorsArray" }] : []));
16051
- activeSlideNumber = computed(() => this.carousel.carouselContent().slideIndex() + 1, ...(ngDevMode ? [{ debugName: "activeSlideNumber" }] : []));
16179
+ activeSlideNumber = computed(() => this.carousel.carouselContent().activePosition() + 1, ...(ngDevMode ? [{ debugName: "activeSlideNumber" }] : []));
16052
16180
  handleNext() {
16053
16181
  this.carousel.carouselContent().next();
16054
16182
  }
@@ -16059,16 +16187,24 @@ class CarouselIndicatorsComponent {
16059
16187
  this.carousel.carouselContent().goToIndex(index, { focusSlide: true });
16060
16188
  }
16061
16189
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.24", ngImport: i0, type: CarouselIndicatorsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
16062
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.24", type: CarouselIndicatorsComponent, isStandalone: true, selector: "tedi-carousel-indicators", inputs: { withArrows: { classPropertyName: "withArrows", publicName: "withArrows", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "@if (withArrows()) {\n <button\n tedi-button\n type=\"button\"\n variant=\"neutral\"\n [attr.aria-label]=\"translationService.track('carousel.moveBack')()\"\n (click)=\"handlePrev()\"\n >\n <tedi-icon name=\"arrow_back\" [size]=\"18\" />\n </button>\n}\n\n@if (variant() === \"dots\") {\n @for (indicator of indicatorsArray(); track indicator.index) {\n <button\n type=\"button\"\n class=\"tedi-carousel__indicator\"\n [attr.aria-label]=\"\n translationService.track('carousel.showSlide', indicator.index + 1)()\n \"\n [ngClass]=\"indicator.active ? 'tedi-carousel__indicator--active' : null\"\n (click)=\"handleIndicatorClick(indicator.index)\"\n ></button>\n }\n} @else if (activeSlideNumber()) {\n <div>\n <b tedi-text color=\"brand\">{{ activeSlideNumber() }}</b>\n <span tedi-text color=\"tertiary\"> / {{ indicatorsArray().length }}</span>\n </div>\n}\n\n@if (withArrows()) {\n <button\n tedi-button\n type=\"button\"\n variant=\"neutral\"\n [attr.aria-label]=\"translationService.track('carousel.moveForward')()\"\n (click)=\"handleNext()\"\n >\n <tedi-icon name=\"arrow_forward\" [size]=\"18\" />\n </button>\n}\n", styles: ["tedi-carousel-indicators{display:flex;gap:var(--layout-grid-gutters-04);align-items:center;min-height:24px}.tedi-carousel__indicator{position:relative;width:24px;height:24px;cursor:pointer;background:none;border:none}.tedi-carousel__indicator:after{position:absolute;top:50%;left:50%;width:22px;height:8px;content:\"\";background-color:transparent;border:1px solid var(--tedi-primary-600);border-radius:100px;transform:translate(-50%,-50%)}.tedi-carousel__indicator:hover:after{border-color:var(--tedi-primary-700)}.tedi-carousel__indicator:active:after{background-color:var(--tedi-primary-800);border-color:var(--tedi-primary-800)}.tedi-carousel__indicator:focus-visible:after{outline:var(--tedi-borders-02) solid var(--tedi-primary-500);outline-offset:var(--tedi-borders-01)}.tedi-carousel__indicator--active:after{background-color:var(--tedi-primary-600)}\n"], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: ButtonComponent, selector: "[tedi-button]", inputs: ["variant", "size"] }, { kind: "component", type: IconComponent, selector: "tedi-icon", inputs: ["name", "size", "color", "background", "variant", "type", "label"] }, { kind: "component", type: TextComponent, selector: "[tedi-text]", inputs: ["modifiers", "color"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
16190
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.24", type: CarouselIndicatorsComponent, isStandalone: true, selector: "tedi-carousel-indicators", inputs: { withArrows: { classPropertyName: "withArrows", publicName: "withArrows", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "@if (withArrows()) {\n <button\n tedi-button\n type=\"button\"\n variant=\"neutral\"\n [attr.aria-label]=\"translationService.track('carousel.moveBack')()\"\n [disabled]=\"!carousel.carouselContent().canPrev()\"\n (click)=\"handlePrev()\"\n >\n <tedi-icon name=\"arrow_back\" [size]=\"18\" />\n </button>\n}\n\n@if (variant() === \"dots\") {\n @for (indicator of indicatorsArray(); track indicator.index) {\n <button\n type=\"button\"\n class=\"tedi-carousel__indicator\"\n [attr.aria-label]=\"\n translationService.track('carousel.showSlide', indicator.index + 1)()\n \"\n [ngClass]=\"indicator.active ? 'tedi-carousel__indicator--active' : null\"\n (click)=\"handleIndicatorClick(indicator.index)\"\n ></button>\n }\n} @else if (indicatorsArray().length) {\n <div>\n <b tedi-text color=\"brand\">{{ activeSlideNumber() }}</b>\n <span tedi-text color=\"tertiary\"> / {{ indicatorsArray().length }}</span>\n </div>\n}\n\n@if (withArrows()) {\n <button\n tedi-button\n type=\"button\"\n variant=\"neutral\"\n [attr.aria-label]=\"translationService.track('carousel.moveForward')()\"\n [disabled]=\"!carousel.carouselContent().canNext()\"\n (click)=\"handleNext()\"\n >\n <tedi-icon name=\"arrow_forward\" [size]=\"18\" />\n </button>\n}\n", styles: ["tedi-carousel-indicators{display:flex;gap:var(--layout-grid-gutters-04);align-items:center;min-height:24px}.tedi-carousel__indicator{position:relative;width:24px;height:24px;cursor:pointer;background:none;border:none}.tedi-carousel__indicator:after{position:absolute;top:50%;left:50%;width:22px;height:8px;content:\"\";background-color:transparent;border:1px solid var(--tedi-primary-600);border-radius:100px;transform:translate(-50%,-50%)}.tedi-carousel__indicator:hover:after{border-color:var(--tedi-primary-700)}.tedi-carousel__indicator:active:after{background-color:var(--tedi-primary-800);border-color:var(--tedi-primary-800)}.tedi-carousel__indicator:focus-visible:after{outline:var(--tedi-borders-02) solid var(--tedi-primary-500);outline-offset:var(--tedi-borders-01)}.tedi-carousel__indicator--active:after{background-color:var(--tedi-primary-600)}\n"], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: ButtonComponent, selector: "[tedi-button]", inputs: ["variant", "size"] }, { kind: "component", type: IconComponent, selector: "tedi-icon", inputs: ["name", "size", "color", "background", "variant", "type", "label"] }, { kind: "component", type: TextComponent, selector: "[tedi-text]", inputs: ["modifiers", "color"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
16063
16191
  }
16064
16192
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.24", ngImport: i0, type: CarouselIndicatorsComponent, decorators: [{
16065
16193
  type: Component,
16066
- args: [{ standalone: true, selector: "tedi-carousel-indicators", changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [NgClass, ButtonComponent, IconComponent, TextComponent], template: "@if (withArrows()) {\n <button\n tedi-button\n type=\"button\"\n variant=\"neutral\"\n [attr.aria-label]=\"translationService.track('carousel.moveBack')()\"\n (click)=\"handlePrev()\"\n >\n <tedi-icon name=\"arrow_back\" [size]=\"18\" />\n </button>\n}\n\n@if (variant() === \"dots\") {\n @for (indicator of indicatorsArray(); track indicator.index) {\n <button\n type=\"button\"\n class=\"tedi-carousel__indicator\"\n [attr.aria-label]=\"\n translationService.track('carousel.showSlide', indicator.index + 1)()\n \"\n [ngClass]=\"indicator.active ? 'tedi-carousel__indicator--active' : null\"\n (click)=\"handleIndicatorClick(indicator.index)\"\n ></button>\n }\n} @else if (activeSlideNumber()) {\n <div>\n <b tedi-text color=\"brand\">{{ activeSlideNumber() }}</b>\n <span tedi-text color=\"tertiary\"> / {{ indicatorsArray().length }}</span>\n </div>\n}\n\n@if (withArrows()) {\n <button\n tedi-button\n type=\"button\"\n variant=\"neutral\"\n [attr.aria-label]=\"translationService.track('carousel.moveForward')()\"\n (click)=\"handleNext()\"\n >\n <tedi-icon name=\"arrow_forward\" [size]=\"18\" />\n </button>\n}\n", styles: ["tedi-carousel-indicators{display:flex;gap:var(--layout-grid-gutters-04);align-items:center;min-height:24px}.tedi-carousel__indicator{position:relative;width:24px;height:24px;cursor:pointer;background:none;border:none}.tedi-carousel__indicator:after{position:absolute;top:50%;left:50%;width:22px;height:8px;content:\"\";background-color:transparent;border:1px solid var(--tedi-primary-600);border-radius:100px;transform:translate(-50%,-50%)}.tedi-carousel__indicator:hover:after{border-color:var(--tedi-primary-700)}.tedi-carousel__indicator:active:after{background-color:var(--tedi-primary-800);border-color:var(--tedi-primary-800)}.tedi-carousel__indicator:focus-visible:after{outline:var(--tedi-borders-02) solid var(--tedi-primary-500);outline-offset:var(--tedi-borders-01)}.tedi-carousel__indicator--active:after{background-color:var(--tedi-primary-600)}\n"] }]
16194
+ args: [{ standalone: true, selector: "tedi-carousel-indicators", changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [NgClass, ButtonComponent, IconComponent, TextComponent], template: "@if (withArrows()) {\n <button\n tedi-button\n type=\"button\"\n variant=\"neutral\"\n [attr.aria-label]=\"translationService.track('carousel.moveBack')()\"\n [disabled]=\"!carousel.carouselContent().canPrev()\"\n (click)=\"handlePrev()\"\n >\n <tedi-icon name=\"arrow_back\" [size]=\"18\" />\n </button>\n}\n\n@if (variant() === \"dots\") {\n @for (indicator of indicatorsArray(); track indicator.index) {\n <button\n type=\"button\"\n class=\"tedi-carousel__indicator\"\n [attr.aria-label]=\"\n translationService.track('carousel.showSlide', indicator.index + 1)()\n \"\n [ngClass]=\"indicator.active ? 'tedi-carousel__indicator--active' : null\"\n (click)=\"handleIndicatorClick(indicator.index)\"\n ></button>\n }\n} @else if (indicatorsArray().length) {\n <div>\n <b tedi-text color=\"brand\">{{ activeSlideNumber() }}</b>\n <span tedi-text color=\"tertiary\"> / {{ indicatorsArray().length }}</span>\n </div>\n}\n\n@if (withArrows()) {\n <button\n tedi-button\n type=\"button\"\n variant=\"neutral\"\n [attr.aria-label]=\"translationService.track('carousel.moveForward')()\"\n [disabled]=\"!carousel.carouselContent().canNext()\"\n (click)=\"handleNext()\"\n >\n <tedi-icon name=\"arrow_forward\" [size]=\"18\" />\n </button>\n}\n", styles: ["tedi-carousel-indicators{display:flex;gap:var(--layout-grid-gutters-04);align-items:center;min-height:24px}.tedi-carousel__indicator{position:relative;width:24px;height:24px;cursor:pointer;background:none;border:none}.tedi-carousel__indicator:after{position:absolute;top:50%;left:50%;width:22px;height:8px;content:\"\";background-color:transparent;border:1px solid var(--tedi-primary-600);border-radius:100px;transform:translate(-50%,-50%)}.tedi-carousel__indicator:hover:after{border-color:var(--tedi-primary-700)}.tedi-carousel__indicator:active:after{background-color:var(--tedi-primary-800);border-color:var(--tedi-primary-800)}.tedi-carousel__indicator:focus-visible:after{outline:var(--tedi-borders-02) solid var(--tedi-primary-500);outline-offset:var(--tedi-borders-01)}.tedi-carousel__indicator--active:after{background-color:var(--tedi-primary-600)}\n"] }]
16067
16195
  }], propDecorators: { withArrows: [{ type: i0.Input, args: [{ isSignal: true, alias: "withArrows", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }] } });
16068
16196
 
16069
16197
  class CarouselNavigationComponent {
16198
+ /**
16199
+ * Shows floating navigation buttons over the slides. Place navigation directly
16200
+ * inside `tedi-carousel`, outside the header and footer.
16201
+ * @default false
16202
+ */
16203
+ overlay = input(false, ...(ngDevMode ? [{ debugName: "overlay", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
16070
16204
  translationService = inject(TediTranslationService);
16071
16205
  carousel = inject(CarouselComponent);
16206
+ canPrev = computed(() => this.carousel.carouselContent().canPrev(), ...(ngDevMode ? [{ debugName: "canPrev" }] : []));
16207
+ canNext = computed(() => this.carousel.carouselContent().canNext(), ...(ngDevMode ? [{ debugName: "canNext" }] : []));
16072
16208
  handleNext() {
16073
16209
  this.carousel.carouselContent().next();
16074
16210
  }
@@ -16076,12 +16212,14 @@ class CarouselNavigationComponent {
16076
16212
  this.carousel.carouselContent().prev();
16077
16213
  }
16078
16214
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.24", ngImport: i0, type: CarouselNavigationComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
16079
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.24", type: CarouselNavigationComponent, isStandalone: true, selector: "tedi-carousel-navigation", ngImport: i0, template: "<button\n tedi-button\n type=\"button\"\n variant=\"secondary\"\n [attr.aria-label]=\"translationService.track('carousel.moveBack')()\"\n (click)=\"handlePrev()\"\n>\n <tedi-icon name=\"arrow_back\" [size]=\"18\" />\n</button>\n<button\n tedi-button\n type=\"button\"\n variant=\"secondary\"\n [attr.aria-label]=\"translationService.track('carousel.moveForward')()\"\n (click)=\"handleNext()\"\n>\n <tedi-icon name=\"arrow_forward\" [size]=\"18\" />\n</button>\n", styles: ["tedi-carousel-navigation{display:flex;gap:var(--layout-grid-gutters-08);align-items:center}\n"], dependencies: [{ kind: "component", type: ButtonComponent, selector: "[tedi-button]", inputs: ["variant", "size"] }, { kind: "component", type: IconComponent, selector: "tedi-icon", inputs: ["name", "size", "color", "background", "variant", "type", "label"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
16215
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.24", type: CarouselNavigationComponent, isStandalone: true, selector: "tedi-carousel-navigation", inputs: { overlay: { classPropertyName: "overlay", publicName: "overlay", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class.tedi-carousel-navigation--overlay": "overlay()" } }, ngImport: i0, template: "@if (overlay()) {\n <button\n tedi-floating-button\n type=\"button\"\n variant=\"secondary\"\n position=\"static\"\n [attr.aria-label]=\"translationService.track('carousel.moveBack')()\"\n [disabled]=\"!canPrev()\"\n (click)=\"handlePrev()\"\n >\n <tedi-icon name=\"arrow_back\" [size]=\"18\" />\n </button>\n <button\n tedi-floating-button\n type=\"button\"\n variant=\"secondary\"\n position=\"static\"\n [attr.aria-label]=\"translationService.track('carousel.moveForward')()\"\n [disabled]=\"!canNext()\"\n (click)=\"handleNext()\"\n >\n <tedi-icon name=\"arrow_forward\" [size]=\"18\" />\n </button>\n} @else {\n <button\n tedi-button\n type=\"button\"\n variant=\"secondary\"\n [attr.aria-label]=\"translationService.track('carousel.moveBack')()\"\n [disabled]=\"!canPrev()\"\n (click)=\"handlePrev()\"\n >\n <tedi-icon name=\"arrow_back\" [size]=\"18\" />\n </button>\n <button\n tedi-button\n type=\"button\"\n variant=\"secondary\"\n [attr.aria-label]=\"translationService.track('carousel.moveForward')()\"\n [disabled]=\"!canNext()\"\n (click)=\"handleNext()\"\n >\n <tedi-icon name=\"arrow_forward\" [size]=\"18\" />\n </button>\n}\n", styles: ["tedi-carousel-navigation{display:flex;gap:var(--layout-grid-gutters-08);align-items:center}tedi-carousel-navigation.tedi-carousel-navigation--overlay{position:absolute;inset:0;z-index:1;box-sizing:border-box;justify-content:space-between;padding:var(--layout-grid-gutters-08);pointer-events:none}tedi-carousel-navigation.tedi-carousel-navigation--overlay>*{pointer-events:auto}\n"], dependencies: [{ kind: "component", type: ButtonComponent, selector: "[tedi-button]", inputs: ["variant", "size"] }, { kind: "component", type: FloatingButtonComponent, selector: "[tedi-floating-button]", inputs: ["variant", "size", "axis", "position", "placement", "offset", "zIndex"] }, { kind: "component", type: IconComponent, selector: "tedi-icon", inputs: ["name", "size", "color", "background", "variant", "type", "label"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
16080
16216
  }
16081
16217
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.24", ngImport: i0, type: CarouselNavigationComponent, decorators: [{
16082
16218
  type: Component,
16083
- args: [{ standalone: true, selector: "tedi-carousel-navigation", changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [ButtonComponent, IconComponent], template: "<button\n tedi-button\n type=\"button\"\n variant=\"secondary\"\n [attr.aria-label]=\"translationService.track('carousel.moveBack')()\"\n (click)=\"handlePrev()\"\n>\n <tedi-icon name=\"arrow_back\" [size]=\"18\" />\n</button>\n<button\n tedi-button\n type=\"button\"\n variant=\"secondary\"\n [attr.aria-label]=\"translationService.track('carousel.moveForward')()\"\n (click)=\"handleNext()\"\n>\n <tedi-icon name=\"arrow_forward\" [size]=\"18\" />\n</button>\n", styles: ["tedi-carousel-navigation{display:flex;gap:var(--layout-grid-gutters-08);align-items:center}\n"] }]
16084
- }] });
16219
+ args: [{ standalone: true, selector: "tedi-carousel-navigation", changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [ButtonComponent, FloatingButtonComponent, IconComponent], host: {
16220
+ "[class.tedi-carousel-navigation--overlay]": "overlay()",
16221
+ }, template: "@if (overlay()) {\n <button\n tedi-floating-button\n type=\"button\"\n variant=\"secondary\"\n position=\"static\"\n [attr.aria-label]=\"translationService.track('carousel.moveBack')()\"\n [disabled]=\"!canPrev()\"\n (click)=\"handlePrev()\"\n >\n <tedi-icon name=\"arrow_back\" [size]=\"18\" />\n </button>\n <button\n tedi-floating-button\n type=\"button\"\n variant=\"secondary\"\n position=\"static\"\n [attr.aria-label]=\"translationService.track('carousel.moveForward')()\"\n [disabled]=\"!canNext()\"\n (click)=\"handleNext()\"\n >\n <tedi-icon name=\"arrow_forward\" [size]=\"18\" />\n </button>\n} @else {\n <button\n tedi-button\n type=\"button\"\n variant=\"secondary\"\n [attr.aria-label]=\"translationService.track('carousel.moveBack')()\"\n [disabled]=\"!canPrev()\"\n (click)=\"handlePrev()\"\n >\n <tedi-icon name=\"arrow_back\" [size]=\"18\" />\n </button>\n <button\n tedi-button\n type=\"button\"\n variant=\"secondary\"\n [attr.aria-label]=\"translationService.track('carousel.moveForward')()\"\n [disabled]=\"!canNext()\"\n (click)=\"handleNext()\"\n >\n <tedi-icon name=\"arrow_forward\" [size]=\"18\" />\n </button>\n}\n", styles: ["tedi-carousel-navigation{display:flex;gap:var(--layout-grid-gutters-08);align-items:center}tedi-carousel-navigation.tedi-carousel-navigation--overlay{position:absolute;inset:0;z-index:1;box-sizing:border-box;justify-content:space-between;padding:var(--layout-grid-gutters-08);pointer-events:none}tedi-carousel-navigation.tedi-carousel-navigation--overlay>*{pointer-events:auto}\n"] }]
16222
+ }], propDecorators: { overlay: [{ type: i0.Input, args: [{ isSignal: true, alias: "overlay", required: false }] }] } });
16085
16223
 
16086
16224
  const range = (start, end) => {
16087
16225
  if (end < start)