1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-28 02:23:25 +00:00
Files
browser/libs/vault/src/components/carousel/carousel.component.ts
Nick Krantz e26670c029 [PM-14422] Vault Carousel (#12791)
* collect tailwind styles from the `libs/vault/*`

- Some unique styles were not showing in storybook

* initial add of carousel component

* initial add of carousel stories

* move carousel button to a standalone component for organization

* add key manager for carousel buttons

* add tab panel role to slide component

* make carousel slide focusable when it does not contain focusable elements

* add aria live to carousel slides

* add labels for carousel slide buttons

* emit slide change event

* move icons to carousel-icons folder

* add barrel file for carousel

* move protected properties

* remove underscore

* allow differing heights of carousel slides

* update interactive styles for the carousel icons

* allow for focus styled on carousel buttons

* fix tests

* fix imports

* add method to render each slide and get the height of the tallest slide

- This avoids consumers having to pass in a height.
- The height of the tallest slide is needed because it will stop the carousel from jumping around as the user scrolls.

* add comment to content property

* remove rem calculation
2025-01-22 08:45:35 -06:00

146 lines
5.0 KiB
TypeScript

import { FocusKeyManager } from "@angular/cdk/a11y";
import { CdkPortalOutlet } from "@angular/cdk/portal";
import { CommonModule } from "@angular/common";
import {
AfterViewInit,
ChangeDetectorRef,
Component,
ContentChildren,
ElementRef,
EventEmitter,
Input,
Output,
QueryList,
ViewChild,
ViewChildren,
inject,
} from "@angular/core";
import { ButtonModule } from "@bitwarden/components";
import { VaultCarouselButtonComponent } from "./carousel-button/carousel-button.component";
import { VaultCarouselContentComponent } from "./carousel-content/carousel-content.component";
import { VaultCarouselSlideComponent } from "./carousel-slide/carousel-slide.component";
@Component({
selector: "vault-carousel",
templateUrl: "./carousel.component.html",
standalone: true,
imports: [
CdkPortalOutlet,
CommonModule,
ButtonModule,
VaultCarouselContentComponent,
VaultCarouselButtonComponent,
],
})
export class VaultCarouselComponent implements AfterViewInit {
private changeDetectorRef = inject(ChangeDetectorRef);
/**
* Accessible Label for the carousel
*
* @remarks
* The label should not include the word "carousel", `aria-roledescription="carousel"` is already included.
*/
@Input({ required: true }) label = "";
/**
* Emits the index of of the newly selected slide.
*/
@Output() slideChange = new EventEmitter<number>();
/** All slides within the carousel. */
@ContentChildren(VaultCarouselSlideComponent) slides!: QueryList<VaultCarouselSlideComponent>;
/** All buttons that control the carousel */
@ViewChildren(VaultCarouselButtonComponent)
carouselButtons!: QueryList<VaultCarouselButtonComponent>;
/** Wrapping container for the carousel content and buttons */
@ViewChild("container") carouselContainer!: ElementRef<HTMLElement>;
/** Container for the carousel buttons */
@ViewChild("carouselButtonWrapper") carouselButtonWrapper!: ElementRef<HTMLDivElement>;
/** Temporary container containing `tempSlideOutlet` */
@ViewChild("tempSlideContainer") tempSlideContainer!: ElementRef<HTMLDivElement>;
/** Outlet to temporary render each slide within */
@ViewChild(CdkPortalOutlet) tempSlideOutlet!: CdkPortalOutlet;
/** The currently selected index of the carousel. */
protected selectedIndex = 0;
/**
* Slides that have differing heights can cause the carousel controls to jump.
* Set the min height based on the tallest slide.
*/
protected minHeight: `${number}px` | null = null;
/**
* Focus key manager for keeping tab controls accessible.
* https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/tablist_role#keyboard_interactions
*/
protected keyManager: FocusKeyManager<VaultCarouselButtonComponent> | null = null;
/** Set the selected index of the carousel. */
protected selectSlide(index: number) {
this.selectedIndex = index;
this.slideChange.emit(index);
}
ngAfterViewInit(): void {
this.keyManager = new FocusKeyManager(this.carouselButtons)
.withHorizontalOrientation("ltr")
.withWrap()
.withHomeAndEnd();
// Set the first carousel button as active, this avoids having to double tab the arrow keys on initial focus.
this.keyManager.setFirstItemActive();
this.setMinHeightOfCarousel();
}
/**
* Slides of differing height can cause the carousel to jump in height.
* Render each slide in a temporary portal outlet to get the height of each slide
* and store the tallest slide height.
*/
private setMinHeightOfCarousel() {
// Store the height of the carousel button element.
const heightOfButtonsPx = this.carouselButtonWrapper.nativeElement.offsetHeight;
// Get the width of the carousel so we know how much space each slide can render within.
const containerWidth = this.carouselContainer.nativeElement.offsetWidth;
const containerHeight = this.carouselContainer.nativeElement.offsetHeight;
// Set the width of the temp container to render each slide inside of.
this.tempSlideContainer.nativeElement.style.width = `${containerWidth}px`;
// The first slide is already rendered at this point, use the height of the container
// to determine the height of the first slide.
let tallestSlideHeightPx = containerHeight - heightOfButtonsPx;
this.slides.forEach((slide, index) => {
// Skip the first slide, the height is accounted for above.
if (index === this.selectedIndex) {
return;
}
this.tempSlideOutlet.attach(slide.content);
// Store the height of the current slide if is larger than the current stored height;
if (this.tempSlideContainer.nativeElement.offsetHeight > tallestSlideHeightPx) {
tallestSlideHeightPx = this.tempSlideContainer.nativeElement.offsetHeight;
}
// cleanup the outlet
this.tempSlideOutlet.detach();
});
// Set the min height of the entire carousel based on the largest slide.
this.minHeight = `${tallestSlideHeightPx + heightOfButtonsPx}px`;
this.changeDetectorRef.detectChanges();
}
}