Notes:

  • The carousel is progressively enhanced, with a simple scrollable container for no-js conditions.

  • The arrows are thus hidden by default using the hidden attribute, and only shown in the JS when the JS runs.

  • make sure all data-* attributes are set.

  • class names are required for the styles

  • an out-of-view card needs to be inaccessible by SR and keyboard users. You could use visibility: hidden, but I didn’t like the effect it had on the overall animations, so you can kept it visible and, in the JS:

    • use aria-hidden = "true" to hide it from SRs
    • use inert attribute (with polyfill) to make it keyboard-inaccessible
  • I made it so that the carousel resets to card #1 on screen resize, instead of trying to guess or randomly showing other cards as the number of cars decreases or increases.

<section class="ps ps-slider ps-slider--carousel ps-carousel">
    <div class="ps__wrap">
        <div class="ps__head">
            <header class="ps__header">
                <span class="ps__kicker">The Carousel</span>
                <h2 class="ps__title" aria-level="">It’s more than a budget manager</h2>
            </header>
            <div class="ps__desc">
                <p>
                    This carousel moves by one card at a time when the next and previous arrows are clicked.
                </p>
            </div>
        </div>

        <div class="ps__main grid grid--12">
            <div class="span-1-12">

                <!-- the crousel label should not have word "crousel" in it -->
                <div class="c-carousel" data-carousel data-aria-label="PROVIDE_LABEL">
                    <!-- SR helper will be appended here from the JS to announce to SR users what items are now in view -->

                    <div class="c-carousel__cards-container" data-carousel-cards>
                        <div class="c-carousel__cards-wrapper" data-carousel-cards-wrapper>
                            <div class="c-carousel__card" data-carousel-card>
                                <img class="c-carousel__card__img" src="../../img/carousel-card-icon.svg" alt="icon alt or leave empty if decorational">
                                <h3 class="c-carousel__card__title">Card #1 title</h3>
                                <div class="c-carousel__card__content">
                                    This carousel card contains <a href="#">a link</a>. You can open your financial reports while listening.
                                </div>
                            </div>
                            <div class="c-carousel__card" data-carousel-card>
                                <img class="c-carousel__card__img" src="../../img/carousel-card-icon.svg" alt="icon alt or leave empty if decorational">
                                <h3 class="c-carousel__card__title">Card #2 title</h3>
                                <div class="c-carousel__card__content">
                                    Shorter Card.
                                </div>
                            </div>
                            <div class="c-carousel__card" data-carousel-card>
                                <img class="c-carousel__card__img" src="../../img/carousel-card-icon.svg" alt="icon alt or leave empty if decorational">
                                <h3 class="c-carousel__card__title">Card #3 title</h3>
                                <div class="c-carousel__card__content">
                                    The app doesn't block your music player. You can open your financial reports while listening.
                                </div>
                            </div>
                            <div class="c-carousel__card" data-carousel-card>
                                <img class="c-carousel__card__img" src="../../img/carousel-card-icon.svg" alt="icon alt or leave empty if decorational">
                                <h3 class="c-carousel__card__title">Card #4 title</h3>
                                <div class="c-carousel__card__content">
                                    The app doesn't block your music player. You can open your financial reports while listening.
                                </div>
                            </div>
                            <div class="c-carousel__card" data-carousel-card>
                                <img class="c-carousel__card__img" src="../../img/carousel-card-icon.svg" alt="icon alt or leave empty if decorational">
                                <h3 class="c-carousel__card__title">Card #5 title</h3>
                                <div class="c-carousel__card__content">
                                    This carousel card contains <a href="#">another link</a> to test the inert attribute in all browsers.
                                </div>
                            </div>
                            <div class="c-carousel__card" data-carousel-card>
                                <img class="c-carousel__card__img" src="../../img/carousel-card-icon.svg" alt="icon alt or leave empty if decorational">
                                <h3 class="c-carousel__card__title">Card #6 title</h3>
                                <div class="c-carousel__card__content">
                                    The app doesn't block your music player. You can open your financial reports while listening.
                                </div>
                            </div>
                            <div class="c-carousel__card" data-carousel-card>
                                <img class="c-carousel__card__img" src="../../img/carousel-card-icon.svg" alt="icon alt or leave empty if decorational">
                                <h3 class="c-carousel__card__title">Card #7 title</h3>
                                <div class="c-carousel__card__content">
                                    The app doesn't block your music player. You can open your financial reports while listening.
                                </div>
                            </div>
                            <div class="c-carousel__card" data-carousel-card>
                                <img class="c-carousel__card__img" src="../../img/carousel-card-icon.svg" alt="icon alt or leave empty if decorational">
                                <h3 class="c-carousel__card__title">Card #8 title</h3>
                                <div class="c-carousel__card__content">
                                    The app doesn't block your music player. You can open your financial reports while listening.
                                </div>
                            </div>
                            <div class="c-carousel__card" data-carousel-card>
                                <img class="c-carousel__card__img" src="../../img/carousel-card-icon.svg" alt="icon alt or leave empty if decorational">
                                <h3 class="c-carousel__card__title">Card #9 title</h3>
                                <div class="c-carousel__card__content">
                                    The app doesn't block your music player. You can open your financial reports while listening.
                                </div>
                            </div>

                        </div>
                    </div>

                    <div class="c-carousel__paddleNav" data-carousel-paddlenav hidden>
                        <!-- paddleNav (prev and next buttons) provided here for flexibility (choose and use the icon you want) -->
                        <button class="c-carousel__paddleNav__prev" aria-label="Previous" data-prev>
              <!-- place icon here -->
              <svg width="8" height="12" viewBox="0 0 8 12" aria-hidden="true" focusable="false">
                <g fill="none" fill-rule="evenodd">
                  <path d="M-8-6h24v24H-8z" />
                  <path fill="currentColor" fill-rule="nonzero" d="M7.41 10.59L2.83 6l4.58-4.59L6 0 0 6l6 6z" />
                </g>
              </svg>
            </button>
                        <button class="c-carousel__paddleNav__next" aria-label="Next" data-next>
              <!-- place icon here -->
              <svg width="8" height="12" viewBox="0 0 8 12" aria-hidden="true" focusable="false">
                <g fill="none" fill-rule="evenodd">
                  <path d="M-8-6h24v24H-8z" />
                  <path fill="currentColor" fill-rule="nonzero" d="M.59 10.59L5.17 6 .59 1.41 2 0l6 6-6 6z" />
                </g>
              </svg>
            </button>
                    </div>

                </div>
            </div>

        </div>
    </div>
    </div>
</section>

<!-- required for the carousel -->
<script src="../../js/inert-polyfill.js"></script>
<script src="../../js/intersectionObserver-polyfill.js"></script>
<script src="../../js/hammer.min.js"></script>
  • Content:
    
    "use strict";
    // polyfill prepend method for IE
    (function (arr) {
      arr.forEach(function (item) {
        if (item.hasOwnProperty('prepend')) {
          return;
        }
        Object.defineProperty(item, 'prepend', {
          configurable: true,
          enumerable: true,
          writable: true,
          value: function prepend() {
            var argArr = Array.prototype.slice.call(arguments),
              docFrag = document.createDocumentFragment();
    
            argArr.forEach(function (argItem) {
              var isNode = argItem instanceof Node;
              docFrag.appendChild(isNode ? argItem : document.createTextNode(String(argItem)));
            });
    
            this.insertBefore(docFrag, this.firstChild);
          }
        });
      });
    })([Element.prototype, Document.prototype, DocumentFragment.prototype]);
    
    if (typeof Object.assign != "function") {
      // Must be writable: true, enumerable: false, configurable: true
      Object.defineProperty(Object, "assign", {
        value: function assign(target, varArgs) {
          // .length of function is 2
    
          if (target == null) {
            // TypeError if undefined or null
            throw new TypeError(
              "Cannot convert undefined or null to object"
            );
          }
    
          var to = Object(target);
    
          for (var index = 1; index < arguments.length; index++) {
            var nextSource = arguments[index];
    
            if (nextSource != null) {
              // Skip over if undefined or null
              for (var nextKey in nextSource) {
                // Avoid bugs when hasOwnProperty is shadowed
                if (
                  Object.prototype.hasOwnProperty.call(
                    nextSource,
                    nextKey
                  )
                ) {
                  to[nextKey] = nextSource[nextKey];
                }
              }
            }
          }
          return to;
        },
        writable: true,
        configurable: true
      });
    }
    var util = {
      keyCodes: {
        UP: 38,
        DOWN: 40,
        LEFT: 37,
        RIGHT: 39,
        HOME: 36,
        END: 35,
        ENTER: 13,
        SPACE: 32,
        DELETE: 46,
        TAB: 9
      },
    
      generateID: function (base) {
        return base + Math.floor(Math.random() * 999);
      }
    };
    
    
    
    (function (w, doc, undefined) {
    
      var ARIAcarouselOptions = {
    
      }
    
      var ARIAcarousel = function (inst, options) {
        const _options = Object.assign(ARIAcarouselOptions, options);
        const el = inst;
        const carouselContainer = el.querySelector("[data-carousel-cards]");
        const cardsWrapper = el.querySelector("[data-carousel-cards-wrapper]");
        const cards = Array.from(el.querySelectorAll("[data-carousel-card]"));
        const paddleNav = el.querySelector("[data-carousel-paddleNav]");
        const prevButton = paddleNav.querySelector('[data-prev]');
        const nextButton = paddleNav.querySelector('[data-next]');
        const carouselID = util.generateID('c-carousel-');
        let itemsShowing = [];
    
        let cardWidth = cards[0].offsetWidth;
        let containerWidth = carouselContainer.offsetWidth;
        let itemsInView = Math.round(containerWidth / cardWidth); // Math.round() instead of Math.floor() because FF sometimes errs on a couple of pixels, leading to a value of 3.99 instead of 4.something, so the number is set to 3, which is wrong. To avoid that, round up.
        let itemsOutOfView = cards.length - itemsInView;
        let rightCounter = itemsOutOfView;
        let leftCounter = 0; // reset it
    
    
        var init = function () {
          el.setAttribute('id', carouselID);
          el.classList.add('js-carousel');
          // set up carousel a11y attributes
          el.setAttribute('role', 'group'); // or region
          el.setAttribute('aria-roledescription', 'Carousel');
          el.setAttribute('aria-label', el.getAttribute('data-aria-label'));
          // show Next and Prev Buttons
          paddleNav.removeAttribute('hidden');
          // add the SR announcement span
          initHelper();
    
          // handle carousel on window resize 
          let timeout = false, // holder for timeout id
            delay = 300, // delay after event is "complete" to run callback
            calls = 0;
    
          window.addEventListener("resize", function () {
            clearTimeout(timeout);
            timeout = setTimeout(updateState, delay);
          });
          updateState();
    
          function updateState() {
            cardWidth = cards[0].offsetWidth;
            containerWidth = carouselContainer.offsetWidth;
            itemsInView = Math.round(containerWidth / cardWidth);
            itemsOutOfView = cards.length - itemsInView;
            rightCounter = itemsOutOfView;
            leftCounter = 0; // reset it
    
            // console.log('---');
            // console.log('card width: ' + cardWidth);
            // console.log('Items in view: ' + itemsInView);
            // console.log('Items out of view: ' + itemsOutOfView);
            // console.log('right counter: ' + rightCounter);
            // console.log('left counter: ' + leftCounter);
    
            handlePaddleButtonsState();
            updateHelper();
            slideCards();
          }
    
          // initialize elements
          initCards();
          initPaddleNav();
    
          // add touch swipe support
          enableTouchSwipes();
        };
    
        var initCards = function () {
          // set up IO on the cards
          let options = {
            root: carouselContainer,
            rootMargin: '-10px', // set to a negative value so that FF doesn't consider an element intersecting when its boundary is just touching that of the root
            threshold: 0.75
          }
          let observer = new IntersectionObserver(a11ifyCards, options);
          cards.forEach(card => observer.observe(card));
    
          // change ARIA attributes based on whether the cards are in view or not
          function a11ifyCards(entries, observer) {
            entries.forEach(entry => {
              if (entry.intersectionRatio >= 0.75) { // if you use isIntersecting or intersectionRatio == 1, FF will resolve to true even when it isn't; it's about 1px in FF. Annoying!
                entry.target.classList.add('is-visible');
                // unhide item from SRs
                entry.target.setAttribute('aria-hidden', 'false');
                // re-enable keyboard interactions
                entry.target.removeAttribute('inert');
              }
              else {
                entry.target.classList.remove('is-visible');
                // hide item from SRs (the polyfill used does not hide the element from SRs, so you should do it)
                entry.target.setAttribute('aria-hidden', 'true');
                // prevent keyboard interactions
                entry.target.setAttribute('inert', '');
              }
            });
            // announce which elements are in view
            updateHelper();
          }
        }
    
        // requires Hammer.js
        var enableTouchSwipes = function () {
          var mc = new Hammer(cardsWrapper, { threshold: 500 });
    
          mc.on("swipeleft", function (e) {
            slideForward();
          });
    
          mc.on("swiperight", function (e) {
            slideBack();
          });
        }
    
        var initHelper = function () {
          let helper = document.createElement('span');
          helper.setAttribute('aria-live', 'polite');
          helper.setAttribute('id', carouselID + '__SRHelper');
          helper.classList.add('sr-only');
          helper.classList.add('c-carousel__SRHelper');
    
          el.prepend(helper);
          updateHelper();
        }
    
        var updateHelper = function () {
          let visibleItems = Array.from(el.querySelectorAll('.c-carousel__card.is-visible'));
          let cardNumbers = [];
          let helper = el.querySelector('.c-carousel__SRHelper');
    
          // get which items are in view
          visibleItems.forEach(item => {
            let number = cards.indexOf(item);
            cardNumbers.push(number + 1);
          });
          // announce them in the SR helper
          helper.innerHTML = 'Showing carousel items ' + cardNumbers.toString() + ' of ' + cards.length;
        }
    
        // initialize prev and next arrows
        var initPaddleNav = function () {
          prevButton.addEventListener('keydown', (e) => {
            paddleKeyboardRespond(e);
          }, false);
    
          nextButton.addEventListener('keydown', (e) => {
            paddleKeyboardRespond(e);
          }, false);
    
          prevButton.addEventListener('click', function (e) {
            slideBack();
          });
    
          nextButton.addEventListener('click', function (e) {
            slideForward(e);
          });
    
          handlePaddleButtonsState();
        }
    
        // enable/disable prev and next arrows
        var handlePaddleButtonsState = function () {
          if (rightCounter == 0) {
            nextButton.setAttribute('aria-disabled', 'true');
            nextButton.setAttribute('tabindex', '-1');
          }
          else if (rightCounter > 0) {
            nextButton.removeAttribute('aria-disabled');
            nextButton.removeAttribute('tabindex');
          }
    
          if (leftCounter == 0) {
            prevButton.setAttribute('aria-disabled', 'true');
            prevButton.setAttribute('tabindex', '-1');
          }
          else if (leftCounter > 0) {
            prevButton.removeAttribute('aria-disabled');
            prevButton.removeAttribute('tabindex');
          }
        }
    
    
        var slideCards = function () {
          // var translateValue = leftCounter * cardWidth * -1;
          // instead of using the element width as above, I'm using percentages based on number of items in view (basically same as widths set in the CSS media queries on .c-carousel__card)
          // because of the way FF calculates item widths and how it differs from Chrome and Safari, thus resulting in inaccurate translate values (so portions of some cards would be visible when they shouldn't)
          var translateValue = leftCounter * (100 / itemsInView) * -1;
          cardsWrapper.style.transform = 'translateX(' + translateValue + '%)';
        }
    
        var incrementRightCounter = function () {
          if (rightCounter < itemsOutOfView) {
            return ++rightCounter;
          }
          else return;
        }
    
        var decrementRightCounter = function () {
          if (rightCounter > 0) {
            return --rightCounter;
          }
          else return;
        }
    
        var incrementLeftCounter = function () {
          if (leftCounter < itemsOutOfView) {
            return ++leftCounter;
          }
          else return;
        }
    
        var decrementLeftCounter = function () {
          if (leftCounter > 0) {
            return --leftCounter;
          }
          else return;
        }
    
        var slideBack = function (e) {
          incrementRightCounter();
          decrementLeftCounter();
          slideCards();
          handlePaddleButtonsState();
    
          // console.log('---');
          // console.log('card width: ' + cardWidth);
          // console.log('Items in view: ' + itemsInView);
          // console.log('Items out of view: ' + itemsOutOfView);
          // console.log('right counter: ' + rightCounter);
          // console.log('left counter: ' + leftCounter);
        }
    
        var slideForward = function (e) {
          decrementRightCounter();
          incrementLeftCounter();
          slideCards();
          handlePaddleButtonsState();
    
          // console.log('---');
          // console.log('card width: ' + cardWidth);
          // console.log('Items in view: ' + itemsInView);
          // console.log('Items out of view: ' + itemsOutOfView);
          // console.log('right counter: ' + rightCounter);
          // console.log('left counter: ' + leftCounter);
        }
    
    
        var paddleKeyboardRespond = function (e) {
          var keyCode = e.keyCode || e.which;
    
          switch (keyCode) {
            case util.keyCodes.LEFT:
              prevButton.focus();
              slideBack(e);
              break;
    
            case util.keyCodes.RIGHT:
              nextButton.focus();
              slideForward(e);
              break;
    
            case util.keyCodes.ENTER:
            case util.keyCodes.SPACE:
              break;
    
    
            case util.keyCodes.TAB:
              break;
          }
        }
    
        init.call(this);
        return this;
      }; // ARIAcarousel()
    
      w.ARIAcarousel = ARIAcarousel;
    
    })(window, document);
    
    
    
    
    var carouselInstance = "[data-carousel]";
    var els = document.querySelectorAll(carouselInstance);
    var allcarousel = [];
    
    // Generate all carousel instances
    for (var i = 0; i < els.length; i++) {
      var ncarousel = new ARIAcarousel(els[i]); // if manual is set to false, the carousel open on focus without needing an ENTER or SPACE press
      allcarousel.push(ncarousel);
    }
    
    
  • URL: /components/raw/slider-carousel/slider-carousel.js
  • Filesystem Path: src/components/slider-carousel/slider-carousel.js
  • Size: 12 KB
  • Content:
    /***************************************************************\
     * Slider | Cards (a.k.a. Carousel) component-specific styles.
     ***************************************************************/
    
    .c-carousel {
      position: relative;
    }
    
    .c-carousel__cards-container {
      overflow-x: auto;
      overflow-y: hidden;
      -webkit-overflow-scrolling: touch;
    
      .js-carousel & {
        overflow: hidden;
      }
    }
    
    .c-carousel__cards-wrapper {
      white-space: nowrap;
      transition: transform .4s cubic-bezier(.39, .03, .56, .57);
    }
    
    .c-carousel__card {
      width: 100%;
      text-align: center;
      margin-bottom: 1rem; // for no-js
      margin-right: 1.6rem; // for no-js
      white-space: normal;
      display: inline-block;
    
      @media all and (min-width: 640px) {
        width: 50%;
      }
    
      @media all and (min-width: 900px) {
        width: 33.3333%;
      }
    
      @media all and (min-width: 1200px) {
        width: 25%;
      }
    
      padding: 2.5rem 1.6rem;
      position: relative;
    
      &::after {
        content: "";
        display: block;
        position: absolute;
        top: 0;
        height: 100%;
        left: .8rem;
        width: calc(100% - 1.6rem);
        z-index: -1;
        background-color: var(--color--secondary);
        border-radius: 8px;
      }
    
    }
    
    .js-carousel {
      .c-carousel__container {
        overflow: hidden;
      }
    
      .c-carousel__cards-wrapper {
        width: 100%;
        display: flex;
        align-items: center;
        transition: transform .3s linear;
      }
    
      .c-carousel__card {
        margin: 0;
        flex-shrink: 0; // to make sure slides inside the flex container take up 100% of the container and don't shrink to their content size.
      }
    }
    
    .c-carousel__card__img {
      margin-bottom: var(--v-margin);
    }
    
    .c-carousel__card__title {
      font-size: 1rem;
    }
    
    
    .c-carousel__paddleNav {
      margin-top: 1.5rem;
      text-align: center;
    
    
      .js-carousel & {
    
        display: flex; // only override hidden attribute when JS is enabled, otherwise next/prev buttons are useless
        align-items: center;
        justify-content: center;
    
        @media all and (min-width: 50em) {
          margin-top: 0;
        }
      }
    
    
    
      .c-carousel__paddleNav__prev,
      .c-carousel__paddleNav__next {
        width: 2.75rem;
        height: 2.75rem;
        padding: .5rem;
        z-index: 2;
        top: 50%;
    
        border: 2px dotted transparent;
        border-radius: 50%;
    
        line-height: 0;
    
        @media all and (min-width: 64em) {
          position: absolute;
          margin-top: -1.375rem;
        }
    
        svg {
          width: 50%;
          height: auto;
          color: #000;
          transition: color .1s linear;
        }
    
        &[aria-disabled="true"] {
          svg {
            color: #ccc;
          }
        }
    
        svg {
          display: inline;
        }
    
        &:hover {
          svg {
            color: var(--color--primary);
          }
        }
    
        &[aria-disabled="true"]:hover {
          svg {
            color: #ccc;
          }
        }
    
        &:focus,
        &:active {
          outline: none;
          border-color: currentColor;
        }
    
        &[aria-disabled="true"]:focus {
          border-color: #ccc;
        }
    
        &:focus:not(:focus-visible) {
          border-color: transparent;
        }
    
        .js-focus-visible &:focus:not(.focus-visible) {
          border-color: transparent;
        }
      }
    
      .c-carousel__paddleNav__prev {
        left: -3.5rem;
      }
    
      .c-carousel__paddleNav__next {
        right: -3.5rem;
      }
    }
  • URL: /components/raw/slider-carousel/slider-carousel.scss
  • Filesystem Path: src/components/slider-carousel/slider-carousel.scss
  • Size: 3.2 KB
  • Content:
    title: Slider | Testimonials
    # notes: "You can have a note here; it will override the README.md notes"
    context:
      text: 
    
    
    
  • URL: /components/raw/slider-carousel/slider-carousel.yml
  • Filesystem Path: src/components/slider-carousel/slider-carousel.yml
  • Size: 123 Bytes
  • Content:
    
    "use strict";
    
    (function (arr) {
      arr.forEach(function (item) {
        if (item.hasOwnProperty('prepend')) {
          return;
        }
        Object.defineProperty(item, 'prepend', {
          configurable: true,
          enumerable: true,
          writable: true,
          value: function prepend() {
            var argArr = Array.prototype.slice.call(arguments),
              docFrag = document.createDocumentFragment();
    
            argArr.forEach(function (argItem) {
              var isNode = argItem instanceof Node;
              docFrag.appendChild(isNode ? argItem : document.createTextNode(String(argItem)));
            });
    
            this.insertBefore(docFrag, this.firstChild);
          }
        });
      });
    })([Element.prototype, Document.prototype, DocumentFragment.prototype]);
    
    // Production steps of ECMA-262, Edition 6, 22.1.2.1
    if (!Array.from) {
      Array.from = (function () {
        var toStr = Object.prototype.toString
        var isCallable = function (fn) {
          return typeof fn === 'function' || toStr.call(fn) === '[object Function]'
        }
        var toInteger = function (value) {
          var number = Number(value)
          if (isNaN(number)) {
            return 0
          }
          if (number === 0 || !isFinite(number)) {
            return number
          }
          return (number > 0 ? 1 : -1) * Math.floor(Math.abs(number))
        }
        var maxSafeInteger = Math.pow(2, 53) - 1
        var toLength = function (value) {
          var len = toInteger(value)
          return Math.min(Math.max(len, 0), maxSafeInteger)
        }
    
        // The length property of the from method is 1.
        return function from(arrayLike /*, mapFn, thisArg */) {
          // 1. Let C be the this value.
          var C = this
    
          // 2. Let items be ToObject(arrayLike).
          var items = Object(arrayLike)
    
          // 3. ReturnIfAbrupt(items).
          if (arrayLike == null) {
            throw new TypeError(
              'Array.from requires an array-like object - not null or undefined'
            )
          }
    
          // 4. If mapfn is undefined, then let mapping be false.
          var mapFn = arguments.length > 1 ? arguments[1] : void undefined
          var T
          if (typeof mapFn !== 'undefined') {
            // 5. else
            // 5. a If IsCallable(mapfn) is false, throw a TypeError exception.
            if (!isCallable(mapFn)) {
              throw new TypeError(
                'Array.from: when provided, the second argument must be a function'
              )
            }
    
            // 5. b. If thisArg was supplied, let T be thisArg; else let T be undefined.
            if (arguments.length > 2) {
              T = arguments[2]
            }
          }
    
          // 10. Let lenValue be Get(items, "length").
          // 11. Let len be ToLength(lenValue).
          var len = toLength(items.length)
    
          // 13. If IsConstructor(C) is true, then
          // 13. a. Let A be the result of calling the [[Construct]] internal method
          // of C with an argument list containing the single item len.
          // 14. a. Else, Let A be ArrayCreate(len).
          var A = isCallable(C) ? Object(new C(len)) : new Array(len)
    
          // 16. Let k be 0.
          var k = 0
          // 17. Repeat, while k < len… (also steps a - h)
          var kValue
          while (k < len) {
            kValue = items[k]
            if (mapFn) {
              A[k] =
                typeof T === 'undefined'
                  ? mapFn(kValue, k)
                  : mapFn.call(T, kValue, k)
            } else {
              A[k] = kValue
            }
            k += 1
          }
          // 18. Let putStatus be Put(A, "length", len, true).
          A.length = len
          // 20. Return A.
          return A
        }
      })()
    }
    
    
    
    
    if (typeof Object.assign != "function") {
      // Must be writable: true, enumerable: false, configurable: true
      Object.defineProperty(Object, "assign", {
        value: function assign(target, varArgs) {
          // .length of function is 2
    
          if (target == null) {
            // TypeError if undefined or null
            throw new TypeError(
              "Cannot convert undefined or null to object"
            );
          }
    
          var to = Object(target);
    
          for (var index = 1; index < arguments.length; index++) {
            var nextSource = arguments[index];
    
            if (nextSource != null) {
              // Skip over if undefined or null
              for (var nextKey in nextSource) {
                // Avoid bugs when hasOwnProperty is shadowed
                if (
                  Object.prototype.hasOwnProperty.call(
                    nextSource,
                    nextKey
                  )
                ) {
                  to[nextKey] = nextSource[nextKey];
                }
              }
            }
          }
          return to;
        },
        writable: true,
        configurable: true
      });
    }
    var util = {
      keyCodes: {
        UP: 38,
        DOWN: 40,
        LEFT: 37,
        RIGHT: 39,
        HOME: 36,
        END: 35,
        ENTER: 13,
        SPACE: 32,
        DELETE: 46,
        TAB: 9
      },
    
      generateID: function (base) {
        return base + Math.floor(Math.random() * 999);
      }
    };
    
    
    
    (function (w, doc, undefined) {
    
      var ARIAcarouselOptions = {
    
      }
    
      var ARIAcarousel = function (inst, options) {
        const _options = Object.assign(ARIAcarouselOptions, options);
        const el = inst;
        const carouselContainer = el.querySelector("[data-carousel-cards]");
        const cardsWrapper = el.querySelector("[data-carousel-cards-wrapper]");
        const cards = Array.from(el.querySelectorAll("[data-carousel-card]"));
        const paddleNav = el.querySelector("[data-carousel-paddleNav]");
        const prevButton = paddleNav.querySelector('[data-prev]');
        const nextButton = paddleNav.querySelector('[data-next]');
        const carouselID = util.generateID('c-carousel-');
        let itemsShowing = [];
    
        let cardWidth = cards[0].offsetWidth;
        let containerWidth = carouselContainer.offsetWidth;
        let itemsInView = Math.round(containerWidth / cardWidth); // Math.round() instead of Math.floor() because FF sometimes errs on a couple of pixels, leading to a value of 3.99 instead of 4.something, so the number is set to 3, which is wrong. To avoid that, round up.
        let itemsOutOfView = cards.length - itemsInView;
        let rightCounter = itemsOutOfView;
        let leftCounter = 0; // reset it
    
    
        var init = function () {
          el.setAttribute('id', carouselID);
          el.classList.add('js-carousel');
          // set up carousel a11y attributes
          el.setAttribute('role', 'group'); // or region
          el.setAttribute('aria-roledescription', 'Carousel');
          el.setAttribute('aria-label', el.getAttribute('data-aria-label'));
          // show Next and Prev Buttons
          paddleNav.removeAttribute('hidden');
          // add the SR announcement span
          initHelper();
    
          // handle carousel on window resize 
          let timeout = false, // holder for timeout id
            delay = 300, // delay after event is "complete" to run callback
            calls = 0;
    
          window.addEventListener("resize", function () {
            clearTimeout(timeout);
            timeout = setTimeout(updateState, delay);
          });
          updateState();
    
          function updateState() {
            cardWidth = cards[0].offsetWidth;
            containerWidth = carouselContainer.offsetWidth;
            itemsInView = Math.round(containerWidth / cardWidth);
            itemsOutOfView = cards.length - itemsInView;
            rightCounter = itemsOutOfView;
            leftCounter = 0; // reset it
    
            // console.log('---');
            // console.log('card width: ' + cardWidth);
            // console.log('Items in view: ' + itemsInView);
            // console.log('Items out of view: ' + itemsOutOfView);
            // console.log('right counter: ' + rightCounter);
            // console.log('left counter: ' + leftCounter);
    
            handlePaddleButtonsState();
            updateHelper();
            slideCards();
          }
    
          // initialize elements
          initCards();
          initPaddleNav();
    
          // add touch swipe support
          enableTouchSwipes();
        };
    
        var initCards = function () {
          // set up IO on the cards
          let options = {
            root: carouselContainer,
            rootMargin: '-10px', // set to a negative value so that FF doesn't consider an element intersecting when its boundary is just touching that of the root
            threshold: 0.75
          }
          let observer = new IntersectionObserver(a11ifyCards, options);
          cards.forEach(function (card) { observer.observe(card) });
    
          // change ARIA attributes based on whether the cards are in view or not
          function a11ifyCards(entries, observer) {
            entries.forEach(function (entry) {
              if (entry.intersectionRatio >= 0.75) { // if you use isIntersecting or intersectionRatio == 1, FF will resolve to true even when it isn't; it's about 1px in FF. Annoying!
                entry.target.classList.add('is-visible');
                // unhide item from SRs
                entry.target.setAttribute('aria-hidden', 'false');
                // re-enable keyboard interactions
                entry.target.removeAttribute('inert');
              }
              else {
                entry.target.classList.remove('is-visible');
                // hide item from SRs (the polyfill used does not hide the element from SRs, so you should do it)
                entry.target.setAttribute('aria-hidden', 'true');
                // prevent keyboard interactions
                entry.target.setAttribute('inert', '');
              }
            });
            // announce which elements are in view
            updateHelper();
          }
        }
    
        // requires Hammer.js
        var enableTouchSwipes = function () {
          var mc = new Hammer(cardsWrapper, { threshold: 500 });
    
          mc.on("swipeleft", function (e) {
            slideForward();
          });
    
          mc.on("swiperight", function (e) {
            slideBack();
          });
        }
    
        var initHelper = function () {
          let helper = document.createElement('span');
          helper.setAttribute('aria-live', 'polite');
          helper.setAttribute('id', carouselID + '__SRHelper');
          helper.classList.add('sr-only');
          helper.classList.add('c-carousel__SRHelper');
    
          el.prepend(helper);
          updateHelper();
        }
    
        var updateHelper = function () {
          let visibleItems = Array.from(el.querySelectorAll('.c-carousel__card.is-visible'));
          let cardNumbers = [];
          let helper = el.querySelector('.c-carousel__SRHelper');
    
          // get which items are in view
          visibleItems.forEach(function (item) {
            let number = cards.indexOf(item);
            cardNumbers.push(number + 1);
          });
          // announce them in the SR helper
          helper.innerHTML = 'Showing carousel items ' + cardNumbers.toString() + ' of ' + cards.length;
        }
    
        // initialize prev and next arrows
        var initPaddleNav = function () {
          prevButton.addEventListener('keydown', function (e) {
            paddleKeyboardRespond(e);
          }, false);
    
          nextButton.addEventListener('keydown', function (e) {
            paddleKeyboardRespond(e);
          }, false);
    
          prevButton.addEventListener('click', function (e) {
            slideBack();
          });
    
          nextButton.addEventListener('click', function (e) {
            slideForward(e);
          });
    
          handlePaddleButtonsState();
        }
    
        // enable/disable prev and next arrows
        var handlePaddleButtonsState = function () {
          if (rightCounter == 0) {
            nextButton.setAttribute('aria-disabled', 'true');
            nextButton.setAttribute('tabindex', '-1');
          }
          else if (rightCounter > 0) {
            nextButton.removeAttribute('aria-disabled');
            nextButton.removeAttribute('tabindex');
          }
    
          if (leftCounter == 0) {
            prevButton.setAttribute('aria-disabled', 'true');
            prevButton.setAttribute('tabindex', '-1');
          }
          else if (leftCounter > 0) {
            prevButton.removeAttribute('aria-disabled');
            prevButton.removeAttribute('tabindex');
          }
        }
    
    
        var slideCards = function () {
          // var translateValue = leftCounter * cardWidth * -1;
          // instead of using the element width as above, I'm using percentages based on number of items in view (basically same as widths set in the CSS media queries on .c-carousel__card)
          // because of the way FF calculates item widths and how it differs from Chrome and Safari, thus resulting in inaccurate translate values (so portions of some cards would be visible when they shouldn't)
          var translateValue = leftCounter * (100 / itemsInView) * -1;
          cardsWrapper.style.transform = 'translateX(' + translateValue + '%)';
        }
    
        var incrementRightCounter = function () {
          if (rightCounter < itemsOutOfView) {
            return ++rightCounter;
          }
          else return;
        }
    
        var decrementRightCounter = function () {
          if (rightCounter > 0) {
            return --rightCounter;
          }
          else return;
        }
    
        var incrementLeftCounter = function () {
          if (leftCounter < itemsOutOfView) {
            return ++leftCounter;
          }
          else return;
        }
    
        var decrementLeftCounter = function () {
          if (leftCounter > 0) {
            return --leftCounter;
          }
          else return;
        }
    
        var slideBack = function (e) {
          incrementRightCounter();
          decrementLeftCounter();
          slideCards();
          handlePaddleButtonsState();
    
          // console.log('---');
          // console.log('card width: ' + cardWidth);
          // console.log('Items in view: ' + itemsInView);
          // console.log('Items out of view: ' + itemsOutOfView);
          // console.log('right counter: ' + rightCounter);
          // console.log('left counter: ' + leftCounter);
        }
    
        var slideForward = function (e) {
          decrementRightCounter();
          incrementLeftCounter();
          slideCards();
          handlePaddleButtonsState();
    
          // console.log('---');
          // console.log('card width: ' + cardWidth);
          // console.log('Items in view: ' + itemsInView);
          // console.log('Items out of view: ' + itemsOutOfView);
          // console.log('right counter: ' + rightCounter);
          // console.log('left counter: ' + leftCounter);
        }
    
    
        var paddleKeyboardRespond = function (e) {
          var keyCode = e.keyCode || e.which;
    
          switch (keyCode) {
            case util.keyCodes.LEFT:
              prevButton.focus();
              slideBack(e);
              break;
    
            case util.keyCodes.RIGHT:
              nextButton.focus();
              slideForward(e);
              break;
    
            case util.keyCodes.ENTER:
            case util.keyCodes.SPACE:
              break;
    
    
            case util.keyCodes.TAB:
              break;
          }
        }
    
        init.call(this);
        return this;
      }; // ARIAcarousel()
    
      w.ARIAcarousel = ARIAcarousel;
    
    })(window, document);
    
    
    
    
    var carouselInstance = "[data-carousel]";
    var els = document.querySelectorAll(carouselInstance);
    var allcarousel = [];
    
    // Generate all carousel instances
    for (var i = 0; i < els.length; i++) {
      var ncarousel = new ARIAcarousel(els[i]); // if manual is set to false, the carousel open on focus without needing an ENTER or SPACE press
      allcarousel.push(ncarousel);
    }
    
    
  • URL: /components/raw/slider-carousel/unes6ed-js.js
  • Filesystem Path: src/components/slider-carousel/unes6ed-js.js
  • Size: 14.7 KB