diff --git a/examples/carousel/carousel-1/carousel-1.html b/examples/carousel/carousel-1/carousel-1.html new file mode 100644 index 0000000000..c27fe2faa8 --- /dev/null +++ b/examples/carousel/carousel-1/carousel-1.html @@ -0,0 +1,525 @@ + + + + + Auto-Rotating Image Carousel Example | WAI-ARIA Authoring Practices 1.1 + + + + + + + + + + + + + + + + + + +
+

Auto-Rotating Image Carousel Example

+

+ NOTE: This is a new example as of the January 2019 release of WAI-ARIA Authoring Practices 1.1. + Please provide feedback in + issue 968. +

+

+ The following example implementation of the + carousel design pattern + demonstrates features of the pattern that are essential to accessibility for carousels that automatically start rotating when the page loads. + For instance, rotation stops when users move focus to any control or link in the carousel or hover the mouse over carousel content. + The accessibility features section that follows the example describes these features in detail. +

+ +
+

Example

+ +
+ + +
+ +
+ +
+ +
+

Accessibility Features

+ +
+ +
+

Keyboard Support

+
+

Rotation Control Button

+ + + + + + + + + + + + + + + + + +
KeyFunction
Tab +
    +
  • Moves focus through interactive elements in the carousel.
  • +
  • Rotation control, previous slide, and next slide buttons precede the slide content in the Tab sequence.
  • +
  • The rotation control button moves on screen when it receives keyboard focus and off screen when it is not focused.
  • +
  • Off-screen positioning allows it to be available to screen reader users when not focused.
  • +
+
Enter or SpaceToggle the auto rotation of slides in the carousel.
+
+ +
+

Next and Previous Slide Buttons

+ + + + + + + + + + + + + +
KeyFunction
Enter
Space
+ Display next or previous slide in the carousel. +
+
+
+ +
+

Role, Property, State, and Tabindex Attributes

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
RoleAttributeElementUsage
+ region + + section + +
    +
  • Role region is implied for any section element that has an accessible name.
  • +
  • Defines the carousel and its controls as a land mark region.
  • +
+
+ aria-roledescription="carousel" + + section + +
    +
  • Informs assistive technologies to identify the element as a "carousel" rather than a "region."
  • +
  • Effects how the assistive technology renders the role but does not effect functionality, such as commands for navigating to landmark regions.
  • +
+
+ aria-label="Highlighted television shows" + + section + Provides a label that describes the content in the carousel region.
+ aria-live=off + + div.carousel-items + +
    +
  • Applied to a div element that contains all the slides.
  • +
  • Identifies the container element as a live region that is in the "off" state, meaning assistive technology users are not informed about changes to the region.
  • +
  • The live region is off when the carousel is automatically rotating.
  • +
+
+ aria-live=polite + + div.carousel-items + +
    +
  • Applied to a div element that contains all the slides.
  • +
  • Identifies the container element as a live region in the "polite" state, meaning assistive technology users are informed about changes to the region at the next available opportunity.
  • +
  • This causes screen readers to automatically announce the content of slides when the next and previous slide buttons are activated.
  • +
+
+ group + + div.carousel-item + +
    +
  • Applied to each of the elements that contains the content of a single slide.
  • +
  • Enables assistive technology users to perceive the boundaries of a slide.
  • +
+
+ aria-roledescription="slide" + + div.carousel-item + +
    +
  • Informs assistive technologies to identify the element as a "slide" rather than a "group."
  • +
  • Effects how the assistive technology renders the role but does not remove any assistive technology functions related to group elements.
  • +
+
+ aria-label="N of 6" + + div.carousel-item + +
    +
  • Provides each slide with a distinct label that helps the user understand which of the 6 slides is displayed.
  • +
  • Note: Normally, including set position and size information in an accessible name is not appropriate. An exception is helpful in this implementation because group elements do not support aria-setsize or aria-posinset.
  • +
+
+ aria-disabled="true" + + button + +
    +
  • Applied to the automatic rotation control button when rotation is stopped and conditions that prevent automatic rotation are present.
  • +
  • The button is disabled when a pointer is hovering over the carousel or any element in the carousel has keyboard focus.
  • +
  • The aria-disabled state is used instead of the HTML disabled attribute to keep the button in the Tab sequence.
  • +
  • If hover or focus is removed from the carousel, the aria-disabled attribute is removed from the button.
  • +
+
+ button + + a + Identifies the element as a button.
+ aria-label="LABEL_STRING" + + a + Defines the accessible name for the next and previous slide buttons.
+ aria-controls="IDREF" + + a + +
    +
  • Identifies the content on the page that the button controls.
  • +
  • Refers to the div that contains all the slides.
  • +
+
+
+ +
+

Javascript and CSS Source Code

+ +
+ +
+

HTML Source Code

+ +
+ + +
+ +
+ + + + + + diff --git a/examples/carousel/carousel-1/css/carousel.css b/examples/carousel/carousel-1/css/carousel.css new file mode 100644 index 0000000000..87a14dba0a --- /dev/null +++ b/examples/carousel/carousel-1/css/carousel.css @@ -0,0 +1,178 @@ + +/*.carousel*/ +.carousel-item { + display: none; + max-height: 400px; + max-width: 900px; + position: relative; + overflow: hidden; + width: 100%; +} + +.carousel .carousel-item.active { + display: block; +} + +/* More like bootstrap, less accessible*/ + +.carousel .carousel-inner { + max-width: 900px; + position: relative; +} + +.carousel button.pause { + display: block; + font-size: 20px; + width: auto; + left: -300em; + margin-bottom: 10px; + height: auto; + position: relative; + top: 5px; + right: -20px; + border: thin solid outset; +} + +.carousel button[aria-disabled=true] { + color: #666; +} + +.carousel button.pause:focus{ + display: block; + position: relative; + font-size: 20px; + width: auto; + left: 0px; + margin-bottom: 10px; + height: auto; + position: relative; + top: 5px; + right: -20px; +} + +.carousel .carousel-items { + border: solid 2px transparent; +} + +.carousel .carousel-items.focus { + border-color: white; + outline: solid 3px #005A9C; +} + +.carousel .carousel-inner .carousel-image a img { + height: 100%; + width: 100%; +} + +.carousel .carousel-inner .carousel-caption a { + text-decoration: underline; + border: none; +} + +.carousel .carousel-inner .carousel-caption a:focus, +.carousel .carousel-inner .carousel-caption a:hover { + outline: solid 2px #FFF; + outline-offset: 1px; +} + +.carousel .carousel-inner .carousel-caption h3 a { + color: #fff; + font-weight: 600; +} + +.carousel .carousel-inner .carousel-caption p { + font-size: 1em; + line-height: 1.5; + margin-bottom: 0; +} + +.carousel .carousel-inner .carousel-caption { + bottom: 0; + left: 0; + padding: 3% 3% 50px; + right: 0; + text-shadow: none +} + +.carousel:hover .carousel-inner .carousel-caption, +.carousel .carousel-item.focus .carousel-caption { + background-color: rgba(0, 0, 0, 0.4); +} + +.carousel .carousel-caption { + position: absolute; + right: 15%; + bottom: 0px; + left: 15%; + padding-top: 20px; + padding-bottom: 20px; + color: #fff; + text-align: center; + text-shadow: 0 1px 2px rgba(0,0,0,.6); +} + +.carousel .carousel-inner, +.carousel .carousel-item, +.carousel .carousel-slide { + max-height: 400px; +} + +.carousel .carousel-control { + position: absolute; + top: 0; + z-index: 10; + font-size: 200%; + font-weight: bold; + color: #fff; + text-align: center; + text-shadow: 0 1px 2px rgba(0,0,0,.6); +} + +.carousel a.carousel-control svg { + position: relative; + display: inline-block; + top: 45%; +} + +.carousel a.carousel-control svg polygon { + opacity: 0.7; +} + +.carousel a.carousel-control:focus { + border: 3px solid #FFF; + outline: 1px solid #005A9C; +} + +.carousel a.carousel-control:focus svg polygon, +.carousel a.carousel-control:hover svg polygon { + opacity: 1.0; +} + +.carousel a.carousel-control.previous { + bottom: 0; + width: 15%; + background-image: linear-gradient(to right,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%); +} + +.carousel a.carousel-control.previous:focus, +.carousel a.carousel-control.previous:hover { + bottom: 0; + width: 15%; + background-image: linear-gradient(to right,rgba(0,0,0,.7) 0,rgba(0,0,0,.0001) 100%); +} + +.carousel a.carousel-control.next { + right: 0; + bottom: 0; + width: 15%; + background-image: linear-gradient(to right,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%); +} + +.carousel a.carousel-control.next:focus, +.carousel a.carousel-control.next:hover { + right: 0; + bottom: 0; + width: 15%; + background-image: linear-gradient(to right,rgba(0,0,0,.0001) 0,rgba(0,0,0,.7) 100%); +} + diff --git a/examples/carousel/carousel-1/images/amsterdamslide__800x600.jpg b/examples/carousel/carousel-1/images/amsterdamslide__800x600.jpg new file mode 100644 index 0000000000..a8f78e9751 Binary files /dev/null and b/examples/carousel/carousel-1/images/amsterdamslide__800x600.jpg differ diff --git a/examples/carousel/carousel-1/images/britcomdavidslide__800x600.jpg b/examples/carousel/carousel-1/images/britcomdavidslide__800x600.jpg new file mode 100644 index 0000000000..74bfae55d7 Binary files /dev/null and b/examples/carousel/carousel-1/images/britcomdavidslide__800x600.jpg differ diff --git a/examples/carousel/carousel-1/images/foyleswarslide__800x600.jpg b/examples/carousel/carousel-1/images/foyleswarslide__800x600.jpg new file mode 100644 index 0000000000..83a056a3bc Binary files /dev/null and b/examples/carousel/carousel-1/images/foyleswarslide__800x600.jpg differ diff --git a/examples/carousel/carousel-1/images/lands-endslide__800x600.jpg b/examples/carousel/carousel-1/images/lands-endslide__800x600.jpg new file mode 100644 index 0000000000..7d25fc4b40 Binary files /dev/null and b/examples/carousel/carousel-1/images/lands-endslide__800x600.jpg differ diff --git a/examples/carousel/carousel-1/images/mag800-2__800x600.jpg b/examples/carousel/carousel-1/images/mag800-2__800x600.jpg new file mode 100644 index 0000000000..a0201e1a36 Binary files /dev/null and b/examples/carousel/carousel-1/images/mag800-2__800x600.jpg differ diff --git a/examples/carousel/carousel-1/images/trustslide-2__800x600.jpg b/examples/carousel/carousel-1/images/trustslide-2__800x600.jpg new file mode 100644 index 0000000000..fb260a8f49 Binary files /dev/null and b/examples/carousel/carousel-1/images/trustslide-2__800x600.jpg differ diff --git a/examples/carousel/carousel-1/js/carousel.js b/examples/carousel/carousel-1/js/carousel.js new file mode 100644 index 0000000000..9e3071e921 --- /dev/null +++ b/examples/carousel/carousel-1/js/carousel.js @@ -0,0 +1,220 @@ +/* +* File: Carousel.js +* +* Desc: Carousel widget that implements ARIA Authoring Practices +* +*/ + +/* +* @constructor CarouselTablist +* +* +*/ +var Carousel = function (domNode) { + this.domNode = domNode; + + this.items = []; + + this.firstItem = null; + this.lastItem = null; + this.currentDomNode = null; + this.liveRegionNode = null; + this.currentItem = null; + this.pauseButton = null; + + this.startLabel = 'Start automatic slide show'; + this.stopLabel = 'Stop automatic slide show'; + + this.rotate = true; + this.hasFocus = false; + this.hasHover = false; + this.isStopped = false; + this.timeInterval = 5000; +}; + +Carousel.prototype.init = function () { + + this.liveRegionNode = this.domNode.querySelector('.carousel-items'); + + var items = this.domNode.querySelectorAll('.carousel-item'); + + for (var i = 0; i < items.length; i++) { + var item = new CarouselItem(items[i], this); + + item.init(); + this.items.push(item); + + if (!this.firstItem) { + this.firstItem = item; + this.currentDomNode = item.domNode; + } + this.lastItem = item; + + var imageLinks = items[i].querySelectorAll('.carousel-image a'); + + if (imageLinks && imageLinks[0]) { + imageLinks[0].addEventListener('focus', this.handleImageLinkFocus.bind(this)); + imageLinks[0].addEventListener('blur', this.handleImageLinkBlur.bind(this)); + } + + } + + // Next Slide and Previous Slide Buttons + + var elems = document.querySelectorAll('.carousel a.carousel-control'); + + for (var i = 0; i < elems.length; i++) { + if (elems[i].tagName.toLowerCase() == 'a') { + var button = new CarouselButton(elems[i], this); + + button.init(); + } + } + + this.currentItem = this.firstItem; + + this.pauseButton = this.domNode.parentNode.parentNode.querySelector('button.pause'); + if (this.pauseButton) { + var button = new PauseButton(this.pauseButton, this); + button.init(); + this.pauseButton.innerHTML = this.stopLabel; + } + + this.domNode.addEventListener('mouseover', this.handleMouseOver.bind(this)); + this.domNode.addEventListener('mouseout', this.handleMouseOut.bind(this)); + + // Start rotation + setTimeout(this.rotateSlides.bind(this), this.timeInterval); +}; + +Carousel.prototype.setSelected = function (newItem, moveFocus) { + if (typeof moveFocus != 'boolean') { + moveFocus = false; + } + + for (var i = 0; i < this.items.length; i++) { + this.items[i].hide(); + } + + this.currentItem = newItem; + this.currentItem.show(); + + if (moveFocus) { + this.currentItem.domNode.focus(); + } +}; + +Carousel.prototype.setSelectedToPreviousItem = function (currentItem, moveFocus) { + if (typeof moveFocus != 'boolean') { + moveFocus = false; + } + + var index; + + if (typeof currentItem !== 'object') { + currentItem = this.currentItem; + } + + if (currentItem === this.firstItem) { + this.setSelected(this.lastItem, moveFocus); + } + else { + index = this.items.indexOf(currentItem); + this.setSelected(this.items[index - 1], moveFocus); + } +}; + +Carousel.prototype.setSelectedToNextItem = function (currentItem, moveFocus) { + if (typeof moveFocus != 'boolean') { + moveFocus = false; + } + + var index; + + if (typeof currentItem !== 'object') { + currentItem = this.currentItem; + } + + if (currentItem === this.lastItem) { + this.setSelected(this.firstItem, moveFocus); + } + else { + index = this.items.indexOf(currentItem); + this.setSelected(this.items[index + 1], moveFocus); + } +}; + +Carousel.prototype.rotateSlides = function () { + if (this.rotate) { + this.setSelectedToNextItem(); + } + setTimeout(this.rotateSlides.bind(this), this.timeInterval); +}; + +Carousel.prototype.startRotation = function () { + if (!this.hasHover && !this.hasFocus && !this.isStopped) { + this.rotate = true; + this.liveRegionNode.setAttribute('aria-live', 'off'); + this.pauseButton.innerHTML = this.stopLabel; + } + this.disablePauseButton(); +}; + +Carousel.prototype.stopRotation = function () { + this.rotate = false; + this.liveRegionNode.setAttribute('aria-live', 'polite'); + this.pauseButton.innerHTML = this.startLabel; + this.disablePauseButton(); +}; + +Carousel.prototype.disablePauseButton = function () { + if (this.hasHover || this.hasFocus) { + this.pauseButton.setAttribute('aria-disabled', 'true'); + } + else { + this.pauseButton.removeAttribute('aria-disabled'); + } +}; + +Carousel.prototype.toggleRotation = function () { + if (this.isStopped) { + if (this.pauseButton.getAttribute('aria-disabled') !== 'true') { + this.isStopped = false; + this.startRotation(); + } + } + else { + this.isStopped = true; + this.stopRotation(); + } +}; + +Carousel.prototype.handleImageLinkFocus = function () { + this.liveRegionNode.classList.add('focus'); +}; + +Carousel.prototype.handleImageLinkBlur = function () { + this.liveRegionNode.classList.remove('focus'); +}; + +Carousel.prototype.handleMouseOver = function () { + this.hasHover = true; + this.stopRotation(); +}; + +Carousel.prototype.handleMouseOut = function () { + this.hasHover = false; + this.startRotation(); +}; + +/* Initialize Carousel Tablists */ + +window.addEventListener('load', function (event) { + var carousels = document.querySelectorAll('.carousel'); + + for (var i = 0; i < carousels.length; i++) { + var carousel = new Carousel(carousels[i]); + carousel.init(); + } +}, false); + diff --git a/examples/carousel/carousel-1/js/carouselButtons.js b/examples/carousel/carousel-1/js/carouselButtons.js new file mode 100644 index 0000000000..22bde956e9 --- /dev/null +++ b/examples/carousel/carousel-1/js/carouselButtons.js @@ -0,0 +1,89 @@ +/* +* File: carouselButton.js +* +* Desc: Carousel Button widget that implements ARIA Authoring Practices +*/ + +/* +* @constructor CarouselButton +* +* +*/ +var CarouselButton = function (domNode, carouselObj) { + this.domNode = domNode; + + this.carousel = carouselObj; + + this.direction = 'previous'; + + if (this.domNode.classList.contains('next')) { + this.direction = 'next'; + } + + this.keyCode = Object.freeze({ + 'RETURN': 13, + 'SPACE': 32, + 'END': 35, + 'HOME': 36, + 'LEFT': 37, + 'UP': 38, + 'RIGHT': 39, + 'DOWN': 40 + }); +}; + +CarouselButton.prototype.init = function () { + this.domNode.addEventListener('keydown', this.handleKeydown.bind(this)); + this.domNode.addEventListener('click', this.handleClick.bind(this)); + this.domNode.addEventListener('focus', this.handleFocus.bind(this)); + this.domNode.addEventListener('blur', this.handleBlur.bind(this)); +}; + +CarouselButton.prototype.changeItem = function () { + if (this.direction === 'previous') { + this.carousel.setSelectedToPreviousItem(); + } + else { + this.carousel.setSelectedToNextItem(); + } +}; + + +/* EVENT HANDLERS */ + +CarouselButton.prototype.handleKeydown = function (event) { + var flag = false; + + switch (event.keyCode) { + case this.keyCode.SPACE: + case this.keyCode.RETURN: + this.changeItem(); + this.domNode.focus(); + flag = true; + break; + + default: + break; + } + + if (flag) { + event.stopPropagation(); + event.preventDefault(); + } +}; + +CarouselButton.prototype.handleClick = function (event) { + this.changeItem(); +}; + +CarouselButton.prototype.handleFocus = function (event) { + this.carousel.hasFocus = true; + this.domNode.classList.add('focus'); + this.carousel.stopRotation(); +}; + +CarouselButton.prototype.handleBlur = function (event) { + this.carousel.hasFocus = false; + this.domNode.classList.remove('focus'); + this.carousel.startRotation(); +}; diff --git a/examples/carousel/carousel-1/js/carouselItem.js b/examples/carousel/carousel-1/js/carouselItem.js new file mode 100644 index 0000000000..7ed5417f63 --- /dev/null +++ b/examples/carousel/carousel-1/js/carouselItem.js @@ -0,0 +1,42 @@ +/* +* File: CarouselItem.js +* +* Desc: Carousel Tab widget that implements ARIA Authoring Practices +*/ + +/* +* @constructor CarouselItem +* +* +*/ +var CarouselItem = function (domNode, carouselObj) { + this.domNode = domNode; + this.carousel = carouselObj; +}; + +CarouselItem.prototype.init = function () { + this.domNode.addEventListener('focusin', this.handleFocusIn.bind(this)); + this.domNode.addEventListener('focusout', this.handleFocusOut.bind(this)); +}; + +CarouselItem.prototype.hide = function () { + this.domNode.classList.remove('active'); +}; + +CarouselItem.prototype.show = function () { + this.domNode.classList.add('active'); +}; + +/* EVENT HANDLERS */ + +CarouselItem.prototype.handleFocusIn = function (event) { + this.domNode.classList.add('focus'); + this.carousel.hasFocus = true; + this.carousel.stopRotation(); +}; + +CarouselItem.prototype.handleFocusOut = function (event) { + this.domNode.classList.remove('focus'); + this.carousel.hasFocus = false; + this.carousel.startRotation(); +}; diff --git a/examples/carousel/carousel-1/js/pauseButton.js b/examples/carousel/carousel-1/js/pauseButton.js new file mode 100644 index 0000000000..ebd875ee56 --- /dev/null +++ b/examples/carousel/carousel-1/js/pauseButton.js @@ -0,0 +1,38 @@ +/* +* File: pasueButton.js +* +* Desc: Implements the pause button for the carousel widget +* +*/ + +var PauseButton = function (domNode, carouselObj) { + this.domNode = domNode; + + this.carousel = carouselObj; +}; + +var StartButton = function (domNode, carouselObj) { + this.domNode = domNode; + + this.carousel = carouselObj; +}; + +PauseButton.prototype.init = function () { + this.domNode.addEventListener('click', this.handleClick.bind(this)); + this.domNode.addEventListener('focus', this.handleFocus.bind(this)); + this.domNode.addEventListener('blur', this.handleBlur.bind(this)); +}; + +/* EVENT HANDLERS */ + +PauseButton.prototype.handleClick = function (event) { + this.carousel.toggleRotation(); +}; + +PauseButton.prototype.handleFocus = function (event) { + this.domNode.classList.add('focus'); +}; + +PauseButton.prototype.handleBlur = function (event) { + this.domNode.classList.remove('focus'); +}; diff --git a/test/tests/carousel-1.js b/test/tests/carousel-1.js new file mode 100644 index 0000000000..1ef6cc95c8 --- /dev/null +++ b/test/tests/carousel-1.js @@ -0,0 +1,76 @@ +'use strict'; + +const { ariaTest } = require('..'); +const { By, Key } = require('selenium-webdriver'); +const assertAttributeValues = require('../util/assertAttributeValues'); +const assertAriaControls = require('../util/assertAriaControls'); +const assertAriaLabelledby = require('../util/assertAriaLabelledby'); +const assertAriaDescribedby = require('../util/assertAriaDescribedby'); +const assertAriaLabelExists = require('../util/assertAriaLabelExists'); +const assertAriaRoles = require('../util/assertAriaRoles'); +const assertTabOrder = require('../util/assertTabOrder'); + + +const exampleFile = 'carousel/carousel-1/carousel-1.html'; + +const ex = { + landmarkSelector: '#myCarousel', + previousNextButtonSelector: '#ex1 .carousel-control', + slideContainerSelector: '#ex1 .carousel-items', + slideSelector: '#ex1 .carousel-item' +}; + + +// Attributes + +ariaTest('Carousel 1: section has aria-label', exampleFile, 'carousel-region-role', async (t) => { + t.plan(1); + await assertAriaLabelExists(t, ex.landmarkSelector); +}); + +ariaTest('Carousel 1: section has aria-roledescription set to carousel', exampleFile, 'carousel-region-aria-roledescription', async (t) => { + t.plan(1); + + // check the aria-roledescrption set to carousel + await assertAttributeValues(t, ex.landmarkSelector, 'aria-roledescription', 'carousel'); +}); + +ariaTest('Carousel 1: previous and next buttons have role button', exampleFile, 'carousel-button-role-link', async (t) => { + t.plan(1); + await assertAriaRoles(t, 'myCarousel', 'button', 2, 'a'); +}); + +ariaTest('Carousel 1: previous and next buttons have aria-label', exampleFile, 'carousel-button-aria-label-next-previous', async (t) => { + t.plan(1); + await assertAriaLabelExists(t, ex.previousNextButtonSelector); +}); + +ariaTest('Carousel 1: previous and next buttons have aria-controls', exampleFile, 'carousel-button-aria-controls', async (t) => { + t.plan(1); + await assertAriaControls(t, ex.previousNextButtonSelector); +}); + +ariaTest('Carousel 1: slide container have aria-live initially set to off', exampleFile, 'carousel-group-aria-roledescription', async (t) => { + t.plan(1); + + // check the aria-roledescrption set to carousel + await assertAttributeValues(t, ex.slideContainerSelector, 'aria-live', 'off'); +}); + +ariaTest('Carousel 1: slides have role group', exampleFile, 'carousel-group-role', async (t) => { + t.plan(1); + await assertAriaRoles(t, 'myCarousel', 'group', 6, 'div'); +}); + +ariaTest('Carousel 1: slides have aria-label', exampleFile, 'carousel-group-aria-label', async (t) => { + t.plan(1); + await assertAriaLabelExists(t, ex.slideSelector); +}); + +ariaTest('Carousel 1: slides have aria-roledescription set to slide', exampleFile, 'carousel-group-aria-roledescription', async (t) => { + t.plan(1); + + // check the aria-roledescrption set to carousel + await assertAttributeValues(t, ex.slideSelector, 'aria-roledescription', 'slide'); +}); +