<section id="my-namespace" class="cwf-carousel" aria-roledescription="carousel" aria-label="Example carousel with navigation" data-navigation="true">
    <div id="my-namespace__wrapper" class="cwf-carousel__wrapper" aria-atomic="false" aria-live="polite">
        <img id="example-carousel__image--1" alt="Random 16:9 image from picsum.photos" src="https://picsum.photos/seed/first/1920/1080" />
        <img id="example-carousel__image--2" alt="Random 16:9 image from picsum.photos" src="https://picsum.photos/seed/second/1920/1080" />
        <img id="example-carousel__image--3" alt="Random 16:9 image from picsum.photos" src="https://picsum.photos/seed/third/1920/1080" />
        <img id="example-carousel__image--4" alt="Random 16:9 image from picsum.photos" src="https://picsum.photos/seed/fourth/1920/1080" />
    </div>
    <nav class="cwf-carousel__controls">
        <div class="cwf-carousel__container">
            <div class="cwf-carousel__pagination"></div>
            <div class="cwf-carousel__navigation">
                <button class="cwf-carousel__button cwf-carousel__button--previous" aria-controls="my-namespace__wrapper" aria-label="Previous slide" title="Previous slide">
                    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512" class="cwf-carousel__chevron cwf-carousel__chevron--left" role="presentation">
                        <!--! Font Awesome Pro 6.1.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->
                        <path fill="currentColor" d="M224 480c-8.188 0-16.38-3.125-22.62-9.375l-192-192c-12.5-12.5-12.5-32.75 0-45.25l192-192c12.5-12.5 32.75-12.5 45.25 0s12.5 32.75 0 45.25L77.25 256l169.4 169.4c12.5 12.5 12.5 32.75 0 45.25C240.4 476.9 232.2 480 224 480z" />
                    </svg>Previous
                </button>
                <button class="cwf-carousel__button cwf-carousel__button--next" aria-controls="my-namespace__wrapper" aria-label="Next slide" title="Next slide">
                    Next<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512" class="cwf-carousel__chevron cwf-carousel__chevron--right" role="presentation">
                        <!--! Font Awesome Pro 6.1.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->
                        <path fill="currentColor" d="M96 480c-8.188 0-16.38-3.125-22.62-9.375c-12.5-12.5-12.5-32.75 0-45.25L242.8 256L73.38 86.63c-12.5-12.5-12.5-32.75 0-45.25s32.75-12.5 45.25 0l192 192c12.5 12.5 12.5 32.75 0 45.25l-192 192C112.4 476.9 104.2 480 96 480z" />
                    </svg> </button>
            </div>
        </div>
    </nav>
</section>
{%- set namespace = 'cwf-carousel' -%}
{%- set id_value = id ?? namespace -%}
{%- set wrapper_id = id_value ~ '__wrapper' -%}
{%- set id_attribute = 'id="' ~ id_value ~ '"' -%}
{%- set classes = [namespace] -%}
{%- set class_attribute = 'class="' ~ (classes|join(' ')) ~ '"' -%}
{%- set aria_roledescription = 'aria-roledescription="carousel"' -%}
{%- set attributes = [id_attribute, class_attribute, aria_roledescription] -%}
{%- if title -%}
    {% set attributes = attributes|merge(['aria-label="' ~ title ~ '"']) %}
{%- endif -%}
{%- set controls = navigation or (autoplay and (slides|length) > 1) -%}
{%- if controls -%}
    {% set attributes = attributes|merge(['data-navigation="true"']) %}
{%- endif -%}
{%- if autoplay -%}
    {% set attributes = attributes|merge(['data-autoplay="true"']) %}
{%- endif -%}
<section {{ attributes|join(' ') }}>
    <div id="{{ wrapper_id }}"
        class="{{ namespace }}__wrapper"
        aria-atomic="false"
        aria-live="{{ autoplay ? 'off' : 'polite' }}">
        {% for slide in slides %}
            {{ slide }}
        {% endfor %}
    </div>
    <nav class="{{ namespace }}__controls">
        <div class="{{ namespace }}__container">
            {%- if controls -%}
                {%- if autoplay -%}
                    <button class="{{ namespace }}__button {{
                        namespace
                        }}__button--autoplay {{ namespace }}__button--play"
                        data-action="pause"
                        aria-label="Pause the carousel"
                        title="Pause the carousel">
                        {% include '../../shared/icons/play-solid.svg' with {
                            class: namespace ~ '__play',
                            role: 'presentation'
                        } %}
                        {% include '../../shared/icons/pause-solid.svg' with {
                            class: namespace ~ '__pause',
                            role: 'presentation'
                        } %}
                        <span class="{{ namespace }}__text">Pause</span>
                    </button>
                {%- endif -%}
                <div class="{{ namespace }}__pagination"></div>
                <div class="{{ namespace }}__navigation">
                    <button class="{{ namespace }}__button {{
                        namespace
                        }}__button--previous"
                        aria-controls="{{ wrapper_id }}"
                        aria-label="Previous slide"
                        title="Previous slide">
                        {% include '../../shared/icons/chevron-left-solid.svg' with {
                            class: namespace ~ '__chevron ' ~ namespace
                                ~ '__chevron--left',
                            role: 'presentation'
                        } %}Previous
                    </button>
                    <button class="{{ namespace }}__button {{
                        namespace
                        }}__button--next"
                        aria-controls="{{ wrapper_id }}"
                        aria-label="Next slide"
                        title="Next slide">
                        Next{% include '../../shared/icons/chevron-right-solid.svg' with {
                            class: namespace ~ '__chevron ' ~ namespace
                                ~ '__chevron--right',
                            role: 'presentation'
                        } %}
                    </button>
                </div>
            {%- else -%}
                <div class="{{ namespace }}__scrollbar"></div>
            {%- endif -%}
        </div>
    </nav>
</section>
{
  "id": "my-namespace",
  "slides": [
    "<img id=\"example-carousel__image--1\" alt=\"Random 16:9 image from picsum.photos\" src=\"https://picsum.photos/seed/first/1920/1080\" />",
    "<img id=\"example-carousel__image--2\" alt=\"Random 16:9 image from picsum.photos\" src=\"https://picsum.photos/seed/second/1920/1080\" />",
    "<img id=\"example-carousel__image--3\" alt=\"Random 16:9 image from picsum.photos\" src=\"https://picsum.photos/seed/third/1920/1080\" />",
    "<img id=\"example-carousel__image--4\" alt=\"Random 16:9 image from picsum.photos\" src=\"https://picsum.photos/seed/fourth/1920/1080\" />"
  ],
  "navigation": true,
  "title": "Example carousel with navigation"
}
  • Content:
    // Carousel component styles
    
    @use "../../shared/animation";
    @use "../../shared/media";
    @use "../../shared/style";
    @use "../../shared/theme";
    
    // Selector prefix
    $prefix: "cwf" !default;
    
    // Swiper mixins
    
    @mixin android {
        #{if(&, ".swiper-android &", ".swiper-android")} {
            @content;
        }
    }
    
    @mixin pointer-events {
        #{if(&, ".swiper-pointer-events &", ".swiper-pointer-events")} {
            @content;
        }
    }
    
    @mixin fade {
        #{if(&, ".swiper-fade &", ".swiper-fade")} {
            @content;
        }
    }
    
    // Placeholder selectors
    
    %carousel-slide-size {
        flex-shrink: 0;
        width: 100%;
        height: 100%;
        margin-bottom: 0;
    }
    
    %carousel-translate-3d-reset {
        transform: translate3d(0px, 0, 0);
    }
    
    // Carousel
    
    .#{$prefix}-carousel {
        display: flex;
        flex-direction: column;
        background-color: style.color("white-dark");
        @include style.spacing;
        overflow: hidden;
        @include style.z-index("content", "middle");
    
        @mixin within-grid {
            .cwf-grid > .#{$prefix}-carousel & {
                @content;
            }
        }
    
        // Wrapper
    
        &__wrapper {
            position: relative;
            display: flex;
            align-items: center;
            width: 100%;
            height: 100%;
            transition-property: transform;
            @include style.z-index("content", "middle");
            @extend %carousel-translate-3d-reset;
    
            @include style.cursor(grab);
    
            & > * {
                @extend %carousel-slide-size;
                margin-bottom: 0;
            }
        }
    
        // Slide
    
        &__slide {
            @extend %carousel-slide-size;
            transition-property: transform;
    
            @include android {
                @extend %carousel-translate-3d-reset;
            }
    
            @include fade {
                pointer-events: none;
                transition-property: opacity;
            }
    
            &--active {
                @include fade {
                    pointer-events: auto;
                }
            }
        }
    
        // Controls
    
        $controls__padding: 0.5rem;
        $controls__padding--md: 1rem;
    
        &__controls {
            --#{$prefix}-carousel__controls--padding: #{$controls__padding};
            display: flex;
            justify-content: space-between;
            align-items: center;
            width: 100%;
            padding-top: var(--#{$prefix}-carousel__controls--padding);
            background-color: style.color("white");
    
            @include media.breakpoint {
                .#{$prefix}-carousel:is([data-navigation], [data-autoplay]) & {
                    --#{$prefix}-carousel__controls--padding: #{$controls__padding--md};
                }
            }
        }
    
        @mixin controls {
            .#{$prefix}-carousel__controls & {
                @content;
            }
        }
    
        // Container
    
        &__container {
            @include theme.contain;
    
            @include controls {
                display: flex;
                justify-content: space-between;
                align-items: center;
                gap: 0.5rem;
            }
        }
    
        // Scrollbar
    
        $scrollbar__background-color: style.darken("white-dark", 5%) !default;
    
        $scrollbar__background-color--md: style.color("white") !default;
    
        &__scrollbar {
            flex: 1;
            position: relative;
            height: 1rem;
            background-color: $scrollbar__background-color;
    
            @include style.cursor(pointer);
    
            @include media.breakpoint {
                height: 0.5rem;
            }
        }
    
        $handle__background-color: theme.accent--background() !default;
    
        &__handle {
            position: relative;
            left: 0;
            top: 0;
            width: 100%;
            height: 1rem;
            background-color: $handle__background-color;
    
            @include style.cursor(grab);
    
            @include media.breakpoint {
                height: 0.5rem;
            }
        }
    
        // Pagination
    
        $pagination__padding: 0.75rem !default;
        $pagination__padding--md: 1rem !default;
    
        &__pagination {
            --#{$prefix}-carousel__pagination--padding: #{$pagination__padding};
            padding-left: var(--#{$prefix}-carousel__pagination--padding);
            padding-right: var(--#{$prefix}-carousel__pagination--padding);
    
            @include media.breakpoint {
                --#{$prefix}-carousel__pagination--padding: #{$pagination__padding--md};
            }
        }
    
        // Fraction
    
        &__fraction {
            @include media.breakpoint {
                display: none;
    
                @include within-grid {
                    display: block;
                }
            }
        }
    
        // Dots
    
        $dots__display: none !default;
        $dots__display--md: flex !default;
        $dots__display--md--in-grid: none !default;
    
        &__dots {
            --#{$prefix}-carousel__dots--display: #{$dots__display};
            display: var(--#{$prefix}-carousel__dots--display);
    
            @include media.breakpoint {
                --#{$prefix}-carousel__dots--display: #{$dots__display--md};
                align-items: center;
                flex-wrap: wrap;
                gap: 1rem;
    
                @include within-grid {
                    --#{$prefix}-carousel__dots--display: #{$dots__display--md--in-grid};
                }
            }
        }
    
        // Dot
    
        $dot__width: 1.75rem !default;
        $dot__width--ends: 1.5rem !default;
        $dot__height: 0.5rem !default;
        $dot__background-color: style.color("white-darkest") !default;
        $dot__background-color--active: theme.accent--background() !default;
        $dot__scale: 1 !default;
        $dot__scale--active: 1.5 !default;
        $dot__angle: style.angle(
            $width: $dot__width,
            $height: $dot__height,
            $side: "both"
        ) !default;
        $dot__angle--first: style.angle(
            $width: $dot__width--ends,
            $height: $dot__height
        ) !default;
        $dot__angle--last: style.angle(
            $width: $dot__width--ends,
            $height: $dot__height,
            $side: "left"
        ) !default;
    
        &__dot {
            --#{$prefix}-carousel__dot--width: #{$dot__width};
            --#{$prefix}-carousel__dot--height: #{$dot__height};
            --#{$prefix}-carousel__dot--background-color: #{$dot__background-color};
            --#{$prefix}-carousel__dot--scale: #{$dot__scale};
            --#{$prefix}-carousel__dot--angle: #{$dot__angle};
            display: block;
            width: var(--#{$prefix}-carousel__dot--width);
            height: var(--#{$prefix}-carousel__dot--height);
            padding: 0;
            border: none;
            border-radius: 0;
            background-color: var(--#{$prefix}-carousel__dot--background-color);
            transform: scale(var(--#{$prefix}-carousel__dot--scale));
            clip-path: var(--#{$prefix}-carousel__dot--angle);
            @include animation.transition(background-color, transform);
    
            &:first-child {
                --#{$prefix}-carousel__dot--angle: #{$dot__angle--first};
            }
    
            &:first-child,
            &:last-child {
                --#{$prefix}-carousel__dot--width: #{$dot__width--ends};
            }
    
            &:last-child {
                --#{$prefix}-carousel__dot--angle: #{$dot__angle--last};
            }
    
            @include style.cursor(pointer);
    
            &--active {
                --#{$prefix}-carousel__dot--background-color: #{$dot__background-color--active};
                --#{$prefix}-carousel__dot--scale: #{$dot__scale--active};
            }
        }
    
        // Navigation
    
        &__navigation {
            display: flex;
        }
    
        $button__font-size: 1rem !default;
        $button__background-color: transparent !default;
        $button__background-color--interact: style.color("white-dark") !default;
        $button__background-color--disabled: transparent !default;
        $button__color: style.color("gray") !default;
        $button__color--interact: theme.accent--background() !default;
        $button__color--disabled: style.color("gray-lightest") !default;
    
        // Buttons
    
        &__button {
            --#{$prefix}-carousel__button--background-color: #{$button__background-color};
            --#{$prefix}-carousel__button--font-size: #{$button__font-size};
            --#{$prefix}-carousel__button--color: #{$button__color};
            display: flex;
            align-items: center;
            background-color: var(--#{$prefix}-carousel__button--background-color);
            padding: 0.5rem 0.75rem;
            border: none;
            border-radius: 0.25rem;
            font-size: var(--#{$prefix}-carousel__button--font-size);
            color: var(--#{$prefix}-carousel__button--color);
            @include animation.transition(color);
    
            @include style.cursor(pointer);
    
            &:hover,
            &:focus {
                --#{$prefix}-carousel__button--background-color: #{$button__background-color--interact};
                --#{$prefix}-carousel__button--color: #{$button__color--interact};
            }
    
            &--disabled {
                --#{$prefix}-carousel__button--background-color: #{$button__background-color--disabled} !important;
                --#{$prefix}-carousel__button--color: #{$button__color--disabled} !important;
    
                @include style.cursor(not-allowed);
            }
    
            // Play/pause buttons
            &--play .#{$prefix}-carousel__play,
            &--pause .#{$prefix}-carousel__pause {
                display: none;
            }
        }
    
        // Icons
    
        $icon__size--play-pause: var(
            --#{$prefix}-carousel__button--font-size
        ) !default;
        $icon__size--chevron: calc(
            var(--#{$prefix}-carousel__button--font-size) * 1.125
        ) !default;
    
        @mixin icon($size) {
            min-width: $size;
            width: $size;
            min-height: $size;
            height: $size;
        }
    
        &__play,
        &__pause {
            @include icon($icon__size--play-pause);
        }
    
        &__chevron {
            @include icon($icon__size--chevron);
        }
    
        &__play,
        &__pause,
        &__chevron--left {
            margin-right: 0.5rem;
        }
    
        &__chevron--right {
            margin-left: 0.5rem;
        }
    }
    
    // Pointer events
    
    @include pointer-events {
        touch-action: pan-y;
    }
    
  • URL: /components/raw/carousel/_index.scss
  • Filesystem Path: components/carousel/_index.scss
  • Size: 9.7 KB
  • Content:
    // The default component class
    import { Component } from '../../shared/component.js';
    
    // Swiper.js and all needed modules
    import Swiper, {
        Autoplay,
        EffectFade,
        Navigation,
        Pagination,
        Scrollbar
    } from 'swiper';
    
    // Check whether reduced motion is enabled locally or globally
    import { reducedMotion } from '../../shared/media/index.js';
    
    // Find focusable descendants and toggle an element's tab order
    import {
        descendants as getFocusableDescendants,
        toggle as toggleTabOrder,
        remove as removeTabOrder
    } from '../../shared/focus.js';
    
    // Provide functionality to all carousels
    export class Carousel extends Component {
        constructor({
            prefix = 'cwf',
            carousel = 'carousel',
            wrapper = 'carousel__wrapper',
            slide = 'carousel__slide',
            activeSlide = 'carousel__slide--active',
            nextSlide = 'carousel__slide--next',
            previousSlide = 'carousel__slide--previous',
            visibleSlide = 'carousel__slide--visible',
            scrollbar = 'carousel__scrollbar',
            handle = 'carousel__handle',
            pagination = 'carousel__pagination',
            fraction = 'carousel__fraction',
            number = 'carousel__number',
            currentNumber = 'carousel__number--current',
            totalNumber = 'carousel__number--total',
            dots = 'carousel__dots',
            dot = 'carousel__dot',
            activeDot = 'carousel__dot--active',
            nextButton = 'carousel__button--next',
            prevButton = 'carousel__button--previous',
            disabledButton = 'carousel__button--disabled',
            autoplayButton = 'carousel__button--autoplay',
            playButton = 'carousel__button--play',
            pauseButton = 'carousel__button--pause',
            text = 'carousel__text'
        } = {}) {
            super({
                prefix,
                classes: {
                    carousel,
                    wrapper,
                    slide,
                    activeSlide,
                    nextSlide,
                    previousSlide,
                    visibleSlide,
                    scrollbar,
                    handle,
                    pagination,
                    fraction,
                    number,
                    currentNumber,
                    totalNumber,
                    dots,
                    dot,
                    activeDot,
                    nextButton,
                    prevButton,
                    disabledButton,
                    autoplayButton,
                    playButton,
                    pauseButton,
                    text
                }
            });
    
            // Setup the default parameters for all configurations
            this.defaultParameters = {
                keyboard: {
                    enabled: true
                },
                slideActiveClass: this.classes.activeSlide,
                slideClass: this.classes.slide,
                slideNextClass: this.classes.nextSlide,
                slidePrevClass: this.classes.previousSlide,
                wrapperClass: this.classes.wrapper
            };
    
            // Bind "this" to the necessary methods
            this.autoplayButtonOnClick = this.autoplayButtonOnClick.bind(this);
            this.pauseAutoplayOnInteraction =
                this.pauseAutoplayOnInteraction.bind(this);
        }
    
        // Returns a copy of the default parameters
        get parameters() {
            return Object.assign({}, this.defaultParameters);
        }
    
        // Returns whether reduced motion is enabled locally/globally
        get reducedMotion() {
            return reducedMotion();
        }
    
        // Setup a carousel module
        module({ reference, modules, selector, key, config, callbacks, ...other }) {
            // First, store a copy of the default parameters
            let parameters = reference.parameters || this.parameters;
    
            // Next, setup the modules array (it it hasn't been already)...
            if (!parameters.modules) parameters.modules = [];
            // ... and push the provided modules to the corresponding array
            parameters.modules.push(...modules);
    
            // Next, store the config at the given key in the parameters...
            parameters[key] = config;
    
            // If a selector has been specified,...
            if (selector) {
                // Next, if the selector is a string, turn it into a map for a single element
                if (typeof selector === 'string') selector = { el: selector };
    
                // Then, for each element selector,...
                Object.keys(selector).forEach((element) => {
                    // ... provide a target element for it
                    parameters[key][element] = reference.carousel.querySelector(
                        selector[element]
                    );
                });
            }
    
            // If callbacks have been provided, add them to the parameters
            if (callbacks) reference.callbacks = callbacks;
    
            // If other parameter settings have been provided, assign them to the parameters
            if (Object.keys(other).length)
                parameters = Object.assign(parameters, other);
    
            // Finally, add the parameters to the reference (if it doesn't exist)...
            if (!reference.parameters) reference.parameters = parameters;
            // ... and return it
            return reference;
        }
    
        // Setup a carousel's fade effect module
        fadeEffect(reference) {
            return this.module({
                reference,
                modules: [EffectFade],
                key: 'fadeEffect',
                config: {
                    crossFade: true
                },
                effect: 'fade',
                slideVisibleClass: this.classes.visibleSlide
            });
        }
    
        // Find the associated reference of a given element
        findReferenceOfElement(element) {
            return this.references.find(
                ({ autoplay = {}, carousel, slides, wrapper }) =>
                    [autoplay?.button, autoplay?.text, carousel, ...slides, wrapper]
                        .filter(Boolean)
                        .includes(element)
            );
        }
    
        // Update the autoplay button when functionality is changed
        autoplayButtonUpdate(button, running) {
            // Get the associated reference of the autoplay button...
            const reference = this.findReferenceOfElement(button);
            // ... and if it doesn't exist, do nothing else
            if (!reference) return;
    
            // Next, grab the autoplay object...
            const { autoplay } = reference;
            // ... and its text from the reference
            const { text } = autoplay;
    
            // Next, define a text action...
            const action = running ? 'Pause' : 'Play';
            // ... and a message using it
            const message = `${action} the carousel`;
    
            // Set the button's aria-label and title to the message
            button.dataset.action = action.toLowerCase();
            button.setAttribute('aria-label', message);
            button.title = message;
    
            // Toggle the play/pause class based on the state
            button.classList.toggle(this.classes.playButton, running);
            button.classList.toggle(this.classes.pauseButton, !running);
    
            // Set the button text to the action
            text.textContent = action;
        }
    
        // Control autoplay functionality when the autoplay button is clicked
        autoplayButtonOnClick({ currentTarget: autoplayButton }) {
            // Get the associated reference of the autoplay button...
            const reference = this.findReferenceOfElement(autoplayButton);
            // ... and if it doesn't exist, do nothing else
            if (!reference) return;
    
            // Get the button's action...
            const { action } = autoplayButton.dataset;
            // ... and start/stop the carousel based on it
            switch (action) {
                case 'play':
                    return reference.swiper.autoplay.start();
                case 'pause':
                    return reference.swiper.autoplay.stop();
            }
        }
    
        // Setup the autoplay button functionality
        autoplayButtonFunctionality(reference) {
            // Grab the carousel, swiper, and autoplay from the reference...
            const { carousel, swiper, autoplay = {} } = reference;
            // ... and if the autoplay object doesn't already exist, add it to the reference
            if (!Object.keys(autoplay).length) reference.autoplay = {};
    
            // Store the button from the reference or grab it...
            const button =
                autoplay?.button ||
                carousel.querySelector(this.selectors.autoplayButton);
            // ... and if doesn't exist, do nothing else
            if (!button) return;
    
            // Store the button text from the reference or grab it...
            const text =
                autoplay?.text || button.querySelector(this.selectors.text);
            // ... and if it doesn't exist, do nothing else
            if (!text) return;
    
            // Othwerwise, handle the button's click event to control the carousel functionality
            button[this.listener]('click', this.autoplayButtonOnClick);
    
            // If the autoplay button hasn't been saved to the reference, save it
            if (!autoplay.button) reference.autoplay.button = button;
    
            // If the autoplay text hasn't been saved to the reference, save it
            if (!autoplay.text) reference.autoplay.text = text;
    
            // If the component is stopping, reset the autoplay button and do nothing else
            if (!this.run) return this.autoplayButtonUpdate(button, true);
    
            // Otherwise, handle autoplay button updates when the carousel starts...
            swiper.on('autoplayStart', () =>
                this.autoplayButtonUpdate.bind(this)(button, true)
            );
            // ... and stops autoplay functionality
            swiper.on('autoplayStop', () =>
                this.autoplayButtonUpdate.bind(this)(button, false)
            );
        }
    
        // Pause autoplay when the wrapper is hovered
        pauseAutoplayOnInteraction({ currentTarget }) {
            // Grab this current target's associated reference...
            const reference = this.findReferenceOfElement(currentTarget);
            // ... and if doesn't exist, do nothing else
            if (!reference) return;
    
            // Otherwise, stop autoplay
            return reference.swiper.autoplay.stop();
        }
    
        // Handle the autoplay wrapper functionality
        autoplayWrapperFunctionality({ wrapper, swiper }) {
            // If no wrapper is found, do nothing
            if (!wrapper) return;
    
            // When the wrapper is hovered, pause autoplay
            wrapper[this.listener]('mouseenter', this.pauseAutoplayOnInteraction);
    
            // If unmounting, reset the wrapper's aria-live attribute
            if (!this.run) return wrapper.setAttribute('aria-live', 'off');
    
            // Update the wrapper's aria-live attribute when autoplay is started...
            swiper.on('autoplayStart', () =>
                wrapper.setAttribute('aria-live', 'off')
            );
            // ... and stopped
            swiper.on('autoplayStop', () =>
                wrapper.setAttribute('aria-live', 'polite')
            );
        }
    
        // Handle the autoplay slide functionality
        autoplaySlideFunctionality({ slides }) {
            // If no slides are found, do nothing
            if (!slides || !slides.length) return;
    
            // For each slide,...
            slides.forEach((slide) =>
                // ... pause autoplay when focused
                slide[this.listener]('focus', this.pauseAutoplayOnInteraction)
            );
        }
    
        // Setup a carousel's autplay module
        autoplay(reference) {
            return this.module({
                reference,
                modules: [Autoplay],
                key: 'autoplay',
                config: {
                    delay: 5000
                },
                callbacks: [
                    this.autoplayButtonFunctionality,
                    this.autoplayWrapperFunctionality,
                    this.autoplaySlideFunctionality
                ]
            });
        }
    
        // Setup a carousel's navigation module
        navigation(reference) {
            return this.module({
                reference,
                modules: [Navigation],
                selector: {
                    nextEl: this.selectors.nextButton,
                    prevEl: this.selectors.prevButton
                },
                key: 'navigation',
                config: {
                    disabledClass: this.classes.disabledButton
                }
            });
        }
    
        // Render a pagination number
        renderNumber(className, value) {
            // Define an array of classes...
            const classes = [this.classes.number, className];
            // ... and return a span using them and the given value
            return `<span class="${classes.join(' ')}">${value}</span>`;
        }
    
        // Render the pagination fraction
        renderFraction(current, total) {
            // Define the current...
            const currentNumber = this.renderNumber(
                this.classes.currentNumber,
                current
            );
            // ... and total number
            const totalNumber = this.renderNumber(this.classes.totalNumber, total);
    
            // Finally, define a label...
            const label = `On slide ${current} of ${total}`;
            // ... and use it to return the fraction with the current/total numbers wrapped
            return `<div aria-label="${label}" class="${this.classes.fraction}">${currentNumber}/${totalNumber}</div>`;
        }
    
        // Render a pagination dot
        renderDot(active, message, controls) {
            // Define an array of classes...
            const classes = [
                this.classes.dot,
                active ? this.classes.activeDot : false
            ].filter(Boolean);
            // ... and return a button using them, the message, and the controls
            return `<button class="${classes.join(
                ' '
            )}" aria-label="${message}" title="${message}" aria-controls="${controls}"></button>`;
        }
    
        // Render the pagination dots
        renderDots(reference, current, total) {
            // Define an empty array of dots
            let dots = [];
    
            // For each dot,...
            for (let number = 1; number <= total; number++) {
                // ... calculate if it's active,...
                const active = number === current;
                // ... it's message,...
                const message = `Go to slide ${number} of ${total}`;
                // ... it's control ID,...
                const controls = reference.wrapper.id;
                // ... and use them to define the dot...
                const dot = this.renderDot(active, message, controls);
                // ... and push it to the dots array
                dots.push(dot);
            }
    
            // Define a label...
            const label = `On slide ${current} of ${total}`;
            // ... and use it to return the dots wrapped
            return `<div class="${
                this.classes.dots
            }" aria-label="${label}">${dots.join('')}</div>`;
        }
    
        // Render the custom pagination
        renderCustomPagination(reference, swiper, current, total) {
            // Define the fraction...
            const fraction = this.renderFraction(current, total);
            // ... and dots
            const dots = this.renderDots(reference, current, total);
    
            // Finally, return the fraction and dots together
            return fraction + dots;
        }
    
        // Setup a carousel's pagination module
        pagination(reference) {
            return this.module({
                reference,
                modules: [Pagination],
                selector: this.selectors.pagination,
                key: 'pagination',
                config: {
                    bulletActiveClass: this.classes.activeDot,
                    bulletClass: this.classes.dot,
                    clickable: true,
                    renderCustom: this.renderCustomPagination.bind(this, reference),
                    type: 'custom'
                }
            });
        }
    
        // Setup a carousel's scrollbar module
        scrollbar(reference) {
            return this.module({
                reference,
                modules: [Scrollbar],
                selector: this.selectors.scrollbar,
                key: 'scrollbar',
                config: {
                    dragClass: this.classes.handle,
                    draggable: true
                }
            });
        }
    
        // Return an attribute string based off the run flag
        get attribute() {
            return this.run ? 'setAttribute' : 'removeAttribute';
        }
    
        // Update a slide's classes and attributes
        slideUpdate(slide, index, slides) {
            slide.classList.toggle(this.classes.slide, this.run);
            slide[this.attribute]('role', 'group');
            slide[this.attribute]('aria-roledescription', 'slide');
            slide[this.attribute]('aria-label', `${index + 1} of ${slides.length}`);
        }
    
        // Get and setup a carousel's slides
        slides({ carousel, slides: existingSlides }) {
            // Grab the slides...
            const slides =
                existingSlides ||
                Array.from(
                    carousel.querySelectorAll(`${this.selectors.wrapper} > *`)
                );
            // ... and for each, update its classes and atrributes
            slides.forEach(this.slideUpdate.bind(this));
    
            // Finally, return the slides
            return slides;
        }
    
        // Store references to all carousels
        store() {
            // Attempt to grab all carousels...
            const carousels = Array.from(
                document.querySelectorAll(this.selectors.carousel)
            );
            // ... and if none exist, do nothing else
            if (!carousels.length) return;
    
            // Otherwise, store the references...
            this.references = carousels.map((carousel) => {
                // Grab the wrapper...
                const wrapper = carousel.querySelector(this.selectors.wrapper);
                // ... and each slide
                const slides = this.slides({ carousel });
    
                // Store a reference to the carousel as an object
                let reference = { carousel, wrapper, slides };
    
                // If reduced motion is enabled locally/globally, set up the reference's fade effect
                if (this.reducedMotion) reference = this.fadeEffect(reference);
    
                // Pull controls from the carousel's dataset
                const { navigation, autoplay } = carousel.dataset;
    
                // If no controls are specified, return the reference with the scrollbar module
                if (!navigation && !autoplay) return this.scrollbar(reference);
    
                // Otherwise, set up the reference's pagination, navigation,....
                if (navigation) {
                    reference = this.pagination(reference);
                    reference = this.navigation(reference);
                }
                // ... and autoplay modules if defined...
                if (autoplay) reference = this.autoplay(reference);
                // ... and return it
                return reference;
            });
        }
    
        // Update a carousel's slides' and their descendants tab index
        updateSlidesTabIndex({ slides }, { activeIndex = 0 }) {
            // For each slide,...
            return slides.forEach((slide, index) => {
                // ... set the targets as it and its descendants...
                const targets = [slide, ...getFocusableDescendants(slide)];
                // ... and either toggle their tab order or remove it if mounting or not
                return this.run
                    ? toggleTabOrder(activeIndex === index, ...targets)
                    : removeTabOrder(...targets);
            });
        }
    
        // Handle the functionality for each carousel
        functionality(run) {
            // For each reference,...
            this.references.forEach((reference) => {
                // Store swiper as an existing or new instance...
                const swiper =
                    reference.swiper ||
                    new Swiper(reference.carousel, reference.parameters);
                // ... and if it hasn't been saved to the reference, save it
                if (!reference.swiper) reference.swiper = swiper;
    
                // Update this reference's slides' tab index
                this.updateSlidesTabIndex(reference, swiper);
    
                // If unmounting, destroy the swiper instance
                if (!run) swiper.destroy(true, true);
    
                // When the swiper active index changes, update this reference's slides' tab index
                swiper.on(
                    'activeIndexChange',
                    this.updateSlidesTabIndex.bind(this, reference)
                );
    
                // If there are no callbacks, do nothing else...
                if (!reference.callbacks || !reference.callbacks.length) return;
                // ... otherwise, run the callbacks
                return reference.callbacks.forEach((callback) =>
                    callback.bind(this)(reference)
                );
            });
        }
    
        // Mount/unmount carousel functionality
        mount(run) {
            super.mount(run);
    
            // If mounting, store the references
            if (run) this.store();
    
            // Handle the functionality of each carousel
            this.functionality(run);
    
            // If unmounting,...
            if (!run) {
                // Reset all slides...
                this.references.forEach(this.slides.bind(this));
                // ... and then the references
                this.references = [];
            }
        }
    }
    
  • URL: /components/raw/carousel/index.js
  • Filesystem Path: components/carousel/index.js
  • Size: 20.7 KB

Carousel

The carousel component encapsulates multiple other components to create a horizontally scrolling area to cycle through content. This is especially useful to conserve vertical space and showcase items within a contextual group.

Variations

By default, a carousel will be styled as a simple horizontally scrolling area with a stylized scrollbar below it.

By adding a data-navigation="true" attribute to a carousel, it enables more finite controls over the carousel. This removes the scrollbar and in its place adds pagination and previous/next navigation buttons.

Autoplay

By adding a data-autoplay="true" attribute to a carousel in conjunction with a data-navigation="true" attribute, it enables autoplay functionality. This adds a play/pause button to the left of the pagination.

Functionality

Under the hood, the carousel component uses Swiper.js to power its functionality. This includes touch gesture support, generating the pagination buttons, and more.

Autoplay

When autoplay is enabled, there are multiple ways to pause it:

  1. By clicking the play/pause button
  2. By clicking any pagination button or the previous/next buttons.
  3. By hovering over the slides.