@tedi-design-system/angular 6.2.0-rc.6 → 6.2.0-rc.7

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, Injectable, InjectionToken, REQUEST, PLATFORM_ID, effect, isSignal, Pipe, model, output, Injector, Renderer2, HostListener, viewChild, contentChild, contentChildren, forwardRef, ViewChild, TemplateRef, HostBinding, Attribute, ViewContainerRef, runInInjectionContext, ContentChildren, afterNextRender, ContentChild, makeEnvironmentProviders } from '@angular/core';
2
+ import { input, computed, ViewEncapsulation, ChangeDetectionStrategy, Component, signal, inject, ElementRef, Directive, Injectable, InjectionToken, REQUEST, PLATFORM_ID, effect, isSignal, Pipe, model, output, Injector, Renderer2, HostListener, viewChild, contentChild, contentChildren, forwardRef, ViewChild, TemplateRef, viewChildren, HostBinding, Attribute, ViewContainerRef, runInInjectionContext, ContentChildren, afterNextRender, ContentChild, makeEnvironmentProviders } from '@angular/core';
3
3
  import * as i1 from '@angular/cdk/layout';
4
4
  import { DOCUMENT, isPlatformBrowser, isPlatformServer, NgTemplateOutlet, NgClass, NgFor, NgIf } from '@angular/common';
5
5
  import { Overlay, OverlayConfig } from '@angular/cdk/overlay';
@@ -4254,7 +4254,9 @@ class CarouselContentComponent {
4254
4254
  translationService = inject(TediTranslationService);
4255
4255
  breakpointService = inject(BreakpointService);
4256
4256
  host = inject(ElementRef);
4257
+ liveAnnouncer = inject(LiveAnnouncer);
4257
4258
  track = viewChild.required("track");
4259
+ slideElements = viewChildren("slide");
4258
4260
  slides = contentChildren(CarouselSlideDirective);
4259
4261
  trackIndex = signal(0);
4260
4262
  animate = signal(false);
@@ -4321,6 +4323,15 @@ class CarouselContentComponent {
4321
4323
  renderedActiveIndex = computed(() => {
4322
4324
  return this.trackIndex() - this.windowBase() + this.buffer();
4323
4325
  });
4326
+ /**
4327
+ * Checks if a slide at the given rendered index is currently visible in the viewport.
4328
+ * Used to determine which slides should be accessible to screen readers.
4329
+ */
4330
+ isSlideVisible(renderedIndex) {
4331
+ const activeIndex = this.renderedActiveIndex();
4332
+ const slidesPerView = Math.ceil(this.currentSlidesPerView());
4333
+ return renderedIndex >= activeIndex && renderedIndex < activeIndex + slidesPerView;
4334
+ }
4324
4335
  renderedIndices = computed(() => {
4325
4336
  const slidesCount = this.slides().length;
4326
4337
  if (!slidesCount) {
@@ -4370,11 +4381,17 @@ class CarouselContentComponent {
4370
4381
  });
4371
4382
  locked = false;
4372
4383
  dragging = false;
4384
+ pendingFocus = false;
4373
4385
  startX = 0;
4374
4386
  startIndex = 0;
4375
4387
  ro;
4376
4388
  wheelTimeout;
4377
4389
  scrollDelta = 0;
4390
+ onScroll() {
4391
+ // Prevent any scroll triggered by focus (e.g., VoiceOver navigation)
4392
+ this.host.nativeElement.scrollLeft = 0;
4393
+ this.host.nativeElement.scrollTop = 0;
4394
+ }
4378
4395
  onWheel(event) {
4379
4396
  const slidesCount = this.slides().length;
4380
4397
  if (!slidesCount) {
@@ -4508,6 +4525,7 @@ class CarouselContentComponent {
4508
4525
  this.animate.set(true);
4509
4526
  this.trackIndex.update((i) => i + 1);
4510
4527
  this.lockNavigation();
4528
+ this.announceSlideChange();
4511
4529
  }
4512
4530
  prev() {
4513
4531
  if (!this.slides().length || this.locked) {
@@ -4516,8 +4534,9 @@ class CarouselContentComponent {
4516
4534
  this.animate.set(true);
4517
4535
  this.trackIndex.update((i) => i - 1);
4518
4536
  this.lockNavigation();
4537
+ this.announceSlideChange();
4519
4538
  }
4520
- goToIndex(index) {
4539
+ goToIndex(index, options) {
4521
4540
  const slidesCount = this.slides().length;
4522
4541
  if (!slidesCount || this.locked) {
4523
4542
  return;
@@ -4527,6 +4546,26 @@ class CarouselContentComponent {
4527
4546
  const delta = normalized - current;
4528
4547
  this.animate.set(true);
4529
4548
  this.trackIndex.update((i) => i + delta);
4549
+ if (options?.focusSlide) {
4550
+ // Focus after transition completes so DOM positions are stable
4551
+ this.pendingFocus = true;
4552
+ }
4553
+ else {
4554
+ this.announceSlideChange();
4555
+ }
4556
+ }
4557
+ /**
4558
+ * Focuses the currently active slide for screen reader users.
4559
+ * Uses preventScroll to avoid breaking carousel layout.
4560
+ */
4561
+ focusActiveSlide() {
4562
+ setTimeout(() => {
4563
+ const activeIndex = this.renderedActiveIndex();
4564
+ const slideElement = this.slideElements()[activeIndex];
4565
+ if (slideElement) {
4566
+ slideElement.nativeElement.focus({ preventScroll: true });
4567
+ }
4568
+ });
4530
4569
  }
4531
4570
  onTransitionEnd(e) {
4532
4571
  if (e.target !== this.track().nativeElement ||
@@ -4536,13 +4575,29 @@ class CarouselContentComponent {
4536
4575
  }
4537
4576
  this.animate.set(false);
4538
4577
  this.windowBase.set(Math.floor(this.trackIndex()));
4578
+ if (this.pendingFocus) {
4579
+ this.pendingFocus = false;
4580
+ this.focusActiveSlide();
4581
+ }
4539
4582
  }
4540
4583
  lockNavigation() {
4541
4584
  this.locked = true;
4542
4585
  setTimeout(() => (this.locked = false), this.transitionMs());
4543
4586
  }
4587
+ /**
4588
+ * Announces the current slide position to screen readers via LiveAnnouncer.
4589
+ * Called after navigation to inform users of the slide change.
4590
+ */
4591
+ announceSlideChange() {
4592
+ setTimeout(() => {
4593
+ const slideNumber = this.slideIndex() + 1;
4594
+ const totalSlides = this.slides().length;
4595
+ const message = this.translationService.translate("carousel.slide", slideNumber, totalSlides);
4596
+ this.liveAnnouncer.announce(message, "polite");
4597
+ }, 100);
4598
+ }
4544
4599
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: CarouselContentComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4545
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.15", 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 } }, host: { attributes: { "tabindex": "0", "role": "region", "aria-roledescription": "carousel", "aria-live": "off" }, listeners: { "wheel": "onWheel($event)", "keydown": "onKeyDown($event)", "pointerdown": "onPointerDown($event)", "pointermove": "onPointerMove($event)", "pointerup": "onPointerUp()", "pointercancel": "onPointerUp()", "lostpointercapture": "onPointerUp()" }, properties: { "attr.aria-label": "translationService.track('carousel')()", "class": "classes()" } }, queries: [{ propertyName: "slides", predicate: CarouselSlideDirective, isSignal: true }], viewQueries: [{ propertyName: "track", first: true, predicate: ["track"], 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 class=\"tedi-carousel__slide\"\n [attr.role]=\"$index === renderedActiveIndex() ? '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]=\"$index === renderedActiveIndex() ? null : 'true'\"\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;touch-action:pan-y;cursor:grab}.tedi-carousel__content:focus-visible{outline:var(--borders-02) solid var(--tedi-primary-500);outline-offset:calc(-1 * var(--borders-03))}.tedi-carousel__content:active{cursor:grabbing}.tedi-carousel__content--fade-right{mask-image:linear-gradient(to right,black 90%,transparent 100%)}.tedi-carousel__content--fade-x{mask-image:linear-gradient(to right,black 85%,transparent 100%),linear-gradient(to left,black 85%,transparent 100%);mask-composite:intersect}.tedi-carousel__track{display:flex;will-change:transform}.tedi-carousel__slide{-webkit-user-select:none;user-select:none;-webkit-user-drag:none}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
4600
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.15", 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 } }, 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": "translationService.track('carousel')()", "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(--borders-02) solid var(--tedi-primary-500);outline-offset:calc(-1 * var(--borders-03))}.tedi-carousel__content:active{cursor:grabbing}.tedi-carousel__content--fade-right{mask-image:linear-gradient(to right,black 90%,transparent 100%)}.tedi-carousel__content--fade-x{mask-image:linear-gradient(to right,black 85%,transparent 100%),linear-gradient(to left,black 85%,transparent 100%);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(--borders-02) solid var(--tedi-primary-500);outline-offset:calc(-1 * var(--borders-03))}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
4546
4601
  }
4547
4602
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: CarouselContentComponent, decorators: [{
4548
4603
  type: Component,
@@ -4553,8 +4608,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.15", ngImpo
4553
4608
  "[attr.aria-label]": "translationService.track('carousel')()",
4554
4609
  "aria-live": "off",
4555
4610
  "[class]": "classes()",
4556
- }, 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 class=\"tedi-carousel__slide\"\n [attr.role]=\"$index === renderedActiveIndex() ? '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]=\"$index === renderedActiveIndex() ? null : 'true'\"\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;touch-action:pan-y;cursor:grab}.tedi-carousel__content:focus-visible{outline:var(--borders-02) solid var(--tedi-primary-500);outline-offset:calc(-1 * var(--borders-03))}.tedi-carousel__content:active{cursor:grabbing}.tedi-carousel__content--fade-right{mask-image:linear-gradient(to right,black 90%,transparent 100%)}.tedi-carousel__content--fade-x{mask-image:linear-gradient(to right,black 85%,transparent 100%),linear-gradient(to left,black 85%,transparent 100%);mask-composite:intersect}.tedi-carousel__track{display:flex;will-change:transform}.tedi-carousel__slide{-webkit-user-select:none;user-select:none;-webkit-user-drag:none}\n"] }]
4557
- }], propDecorators: { onWheel: [{
4611
+ }, 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(--borders-02) solid var(--tedi-primary-500);outline-offset:calc(-1 * var(--borders-03))}.tedi-carousel__content:active{cursor:grabbing}.tedi-carousel__content--fade-right{mask-image:linear-gradient(to right,black 90%,transparent 100%)}.tedi-carousel__content--fade-x{mask-image:linear-gradient(to right,black 85%,transparent 100%),linear-gradient(to left,black 85%,transparent 100%);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(--borders-02) solid var(--tedi-primary-500);outline-offset:calc(-1 * var(--borders-03))}\n"] }]
4612
+ }], propDecorators: { onScroll: [{
4613
+ type: HostListener,
4614
+ args: ["scroll"]
4615
+ }], onWheel: [{
4558
4616
  type: HostListener,
4559
4617
  args: ["wheel", ["$event"]]
4560
4618
  }], onKeyDown: [{
@@ -4624,7 +4682,7 @@ class CarouselIndicatorsComponent {
4624
4682
  this.carousel.carouselContent().prev();
4625
4683
  }
4626
4684
  handleIndicatorClick(index) {
4627
- this.carousel.carouselContent().goToIndex(index);
4685
+ this.carousel.carouselContent().goToIndex(index, { focusSlide: true });
4628
4686
  }
4629
4687
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: CarouselIndicatorsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4630
4688
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.15", 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(--borders-02) solid var(--tedi-primary-500);outline-offset:var(--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 });