blob: d311e4efa719a42f017dae55c9077b590c88d16c [file] [log] [blame]
/**
* @license
* Copyright 2021 The Go Authors. All rights reserved.
* Use of this source code is governed by a BSD-style
* license that can be found in the LICENSE file.
*/
/**
* Carousel Controller adds event listeners, accessibility enhancements, and
* control elements to a carousel component.
*/
export class CarouselController {
/**
* slides is a collection of slides in the carousel.
*/
private slides: HTMLLIElement[];
/**
* dots is a collection of dot navigation controls, added to the carousel
* by this controller.
*/
private dots: HTMLElement[];
/**
* liveRegion is a visually hidden element that notifies assitive devices
* of visual changes to the carousel. They are added to the carousel by
* this controller.
*/
private liveRegion: HTMLElement;
/**
* activeIndex is the 0-index of the currently active slide.
*/
private activeIndex: number;
constructor(private el: HTMLElement) {
this.slides = Array.from(el.querySelectorAll('.go-Carousel-slide'));
this.dots = [];
this.liveRegion = document.createElement('div');
this.activeIndex = Number(el.getAttribute('data-slide-index') ?? 0);
this.initSlides();
this.initArrows();
this.initDots();
this.initLiveRegion();
}
private initSlides() {
for (const [i, v] of this.slides.entries()) {
if (i === this.activeIndex) continue;
v.setAttribute('aria-hidden', 'true');
}
}
private initArrows() {
const arrows = document.createElement('ul');
arrows.classList.add('go-Carousel-arrows');
arrows.innerHTML = `
<li>
<button class="go-Carousel-prevSlide" aria-label="Go to previous slide">
<img class="go-Icon" height="24" width="24" src="/static/shared/icon/arrow_left_gm_grey_24dp.svg" alt="">
</button>
</li>
<li>
<button class="go-Carousel-nextSlide" aria-label="Go to next slide">
<img class="go-Icon" height="24" width="24" src="/static/shared/icon/arrow_right_gm_grey_24dp.svg" alt="">
</button>
</li>
`;
arrows
.querySelector('.go-Carousel-prevSlide')
?.addEventListener('click', () => this.setActive(this.activeIndex - 1));
arrows
.querySelector('.go-Carousel-nextSlide')
?.addEventListener('click', () => this.setActive(this.activeIndex + 1));
this.el.append(arrows);
}
private initDots() {
const dots = document.createElement('ul');
dots.classList.add('go-Carousel-dots');
for (let i = 0; i < this.slides.length; i++) {
const li = document.createElement('li');
const button = document.createElement('button');
button.classList.add('go-Carousel-dot');
if (i === this.activeIndex) {
button.classList.add('go-Carousel-dot--active');
}
button.innerHTML = `<span class="go-Carousel-obscured">Slide ${i + 1}</span>`;
button.addEventListener('click', () => this.setActive(i));
li.append(button);
dots.append(li);
this.dots.push(button);
}
this.el.append(dots);
}
private initLiveRegion() {
this.liveRegion.setAttribute('aria-live', 'polite');
this.liveRegion.setAttribute('aria-atomic', 'true');
this.liveRegion.setAttribute('class', 'go-Carousel-obscured');
this.liveRegion.textContent = `Slide ${this.activeIndex + 1} of ${this.slides.length}`;
this.el.appendChild(this.liveRegion);
}
private setActive = (index: number) => {
this.activeIndex = (index + this.slides.length) % this.slides.length;
this.el.setAttribute('data-slide-index', String(this.activeIndex));
for (const d of this.dots) {
d.classList.remove('go-Carousel-dot--active');
}
this.dots[this.activeIndex].classList.add('go-Carousel-dot--active');
for (const s of this.slides) {
s.setAttribute('aria-hidden', 'true');
}
this.slides[this.activeIndex].removeAttribute('aria-hidden');
this.liveRegion.textContent = 'Slide ' + (this.activeIndex + 1) + ' of ' + this.slides.length;
};
}