mirror of
https://github.com/bitwarden/browser
synced 2026-02-28 02:23:25 +00:00
* 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
146 lines
5.0 KiB
TypeScript
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();
|
|
}
|
|
}
|