/** * @license * Copyright 2019 Google LLC * SPDX-License-Identifier: BSD-3-Clause */ import {LitElement, html, css, unsafeCSS} from 'lit'; import {customElement, property} from 'lit/decorators.js'; import { breakpoints } from '../../breakpoints'; import Swiper from 'swiper'; import { EffectFade, Navigation, Pagination } from 'swiper/modules'; import swiperStyles from 'swiper/css/bundle?raw'; import { unsafeHTML } from 'lit/directives/unsafe-html.js'; @customElement('iu-carousel') export class Carousel extends LitElement { private swiper?: Swiper; static override styles = [ css`${unsafeCSS(swiperStyles)}`, css` .img{ aspect-ratio: 3/2; height: auto; } .img img{ width: 100%; height: 100%; object-fit: cover; } .caption p{ font: var(--iu-f-0); background: #fff; margin-bottom: 0; margin-top: 1.75rem; height: 100%; } :host .swiper-button-prev, :host .swiper-button-next{ width: 50%; height: var(--carousel-img-height); top: 0; margin-top: 0; } :host .swiper-button-prev::after, :host .swiper-button-next::after{ content: none; } :host .swiper-pagination{ display: flex; gap: 0.625rem; position: absolute; top: calc(var(--carousel-img-height) + 0.625rem); } :host .swiper-pagination .swiper-pagination-bullet{ width:100%; border-radius: 0; height: 1px; margin: 0; background: var(--iu-color-grey-300); opacity: 1; } :host .swiper-pagination .swiper-pagination-bullet-active{ background: var(--iu-color-black); height: 3px; } :host .swiper-slide{ opacity: 0; } :host .swiper-slide-active, :host .swiper.slide-duplicate-active{ opacity: 1; } ` ]; @property({ type: Array }) items: Array<{ path: string; caption?: string }> = []; override firstUpdated() { const swiperEl = this.shadowRoot?.querySelector('.swiper') as HTMLElement; const paginationEl = this.shadowRoot?.querySelector('.swiper-pagination') as HTMLElement; const nextEl = this.shadowRoot?.querySelector('.swiper-button-next') as HTMLElement; const prevEl = this.shadowRoot?.querySelector('.swiper-button-prev') as HTMLElement; if (!swiperEl || !paginationEl || !nextEl || !prevEl) { console.error('Required Swiper elements not found'); return; } // Initialize Swiper once the component is rendered this.swiper = new Swiper(swiperEl, { modules: [EffectFade, Navigation, Pagination], loop: true, effect: 'fade', pagination: { el: paginationEl, clickable: true, renderBullet: (index, className) => { return `<button class="${className}" role="tab" aria-label="Go to slide ${index + 1}" aria-selected="false"></button>`; } }, navigation: { nextEl: nextEl, prevEl: prevEl }, on: { slideChange: () => { // Update aria-selected state for pagination bullets const bullets = paginationEl.querySelectorAll('button'); bullets.forEach((bullet, index) => { bullet.setAttribute('aria-selected', index === this.swiper?.realIndex ? 'true' : 'false' ); }); } } }); requestAnimationFrame(() => { const height = Math.floor(this.clientWidth / 1.5) this.style.setProperty('--carousel-img-height', `${height}px`); }); } override render() { return html` <div class="swiper" role="region" aria-roledescription="carousel" aria-label="Image carousel"> <div class="swiper-wrapper" role="presentation"> ${this.items.map( (item, index) => html` <div class="swiper-slide" role="group" aria-roledescription="slide" aria-label="${index + 1} of ${this.items.length}"> <div class="img"> <img src="${item.path}" alt="${item.caption || 'Image'}" /> ${item.caption ? html` <div class="caption"><p>${unsafeHTML(item.caption)}</p></div>` : '' } </div> </div> ` )} </div> <div class="swiper-pagination" role="tablist" aria-label="Carousel slides"></div> <div class="swiper-button-prev" role="button" aria-label="Previous slide"></div> <div class="swiper-button-next" role="button" aria-label="Next slide"></div> </div> `; } } declare global { interface HTMLElementTagNameMap { 'iu-carousel': Carousel; } }