From 43e744025ebfadc79617abd0cd7423a7a8e13a38 Mon Sep 17 00:00:00 2001 From: Jon Gunderson Date: Fri, 4 Sep 2020 18:58:24 -0500 Subject: [PATCH] carousel Example with buttons for slide control: Improve High Contrast Support and implementation of APG Programming Practices (pull #1387) Updated the carousel example that uses buttons for slide control as follows: 1. Added view options equivalent to options available for the tabbed carousel example. 2. Improved color contrast support. 3. Improved keyboard focus styling. 4. Updated Javascript to conform with latest APG code style guidelines. 5. Updated documentation. Co-authored-by: Matt King Co-authored-by: Carolyn MacLeod --- ...ousel-1.html => carousel-1-prev-next.html} | 106 +++--- examples/carousel/carousel-2-tablist.html | 14 +- .../css/carousel-1-more-accessible.css | 185 ----------- examples/carousel/css/carousel-1.css | 165 ---------- examples/carousel/css/carousel-prev-next.css | 306 ++++++++++++++++++ examples/carousel/js/carousel-prev-next.js | 286 ++++++++++++++++ examples/carousel/js/carousel.js | 226 ------------- examples/carousel/js/carouselButtons.js | 70 ---- examples/carousel/js/carouselItem.js | 44 --- examples/carousel/js/pauseButton.js | 24 -- ...-1.js => carousel_carousel-1-prev-next.js} | 4 +- 11 files changed, 664 insertions(+), 766 deletions(-) rename examples/carousel/{carousel-1.html => carousel-1-prev-next.html} (78%) delete mode 100644 examples/carousel/css/carousel-1-more-accessible.css delete mode 100644 examples/carousel/css/carousel-1.css create mode 100644 examples/carousel/css/carousel-prev-next.css create mode 100644 examples/carousel/js/carousel-prev-next.js delete mode 100644 examples/carousel/js/carousel.js delete mode 100644 examples/carousel/js/carouselButtons.js delete mode 100644 examples/carousel/js/carouselItem.js delete mode 100644 examples/carousel/js/pauseButton.js rename test/tests/{carousel_carousel-1.js => carousel_carousel-1-prev-next.js} (98%) diff --git a/examples/carousel/carousel-1.html b/examples/carousel/carousel-1-prev-next.html similarity index 78% rename from examples/carousel/carousel-1.html rename to examples/carousel/carousel-1-prev-next.html index cef34d188d..18e6bb60e8 100644 --- a/examples/carousel/carousel-1.html +++ b/examples/carousel/carousel-1-prev-next.html @@ -1,8 +1,9 @@ + - Auto-Rotating Image Carousel Example | WAI-ARIA Authoring Practices 1.2 + Auto-Rotating Image Carousel Example with Buttons for Slide Control | WAI-ARIA Authoring Practices 1.2 @@ -11,20 +12,9 @@ - - - - - - - + + + @@ -37,21 +27,46 @@
-

Auto-Rotating Image Carousel Example

+

Auto-Rotating Image Carousel Example with Buttons for Slide Control

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. + For instance, rotation stops when users either move focus into the carousel or hover the mouse over carousel content, and users can manually control which slide is displayed with previous and next slide buttons. The accessibility features section that follows the example describes these features in detail.

+

Similar examples include:

+ +
-

Example View Options

- +

Example Options

+ + +
@@ -188,7 +203,7 @@

-

8 pm Sunday, March 8, on TV: Sneak peak at the final season.

+

8 pm Sunday, March 8, on TV: Sneak peek at the final season.

@@ -196,7 +211,9 @@

@@ -373,7 +396,7 @@

Role, Property, State, and Tabindex Attributes

  • Role region is implied for any section element that has an accessible name.
  • -
  • Defines the carousel and its controls as a land mark region.
  • +
  • Defines the carousel and its controls as a landmark region.
@@ -388,7 +411,7 @@

Role, Property, State, and Tabindex Attributes

  • 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.
  • +
  • Affects how the assistive technology renders the role but does not affect functionality, such as commands for navigating to landmark regions.
@@ -405,7 +428,7 @@

Role, Property, State, and Tabindex Attributes

- aria-live=off + aria-live="off" div.carousel-items @@ -421,7 +444,7 @@

Role, Property, State, and Tabindex Attributes

- aria-live=polite + aria-live="polite" div.carousel-items @@ -460,7 +483,7 @@

Role, Property, State, and Tabindex Attributes

  • 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.
  • +
  • Affects how the assistive technology renders the role but does not remove any assistive technology functions related to group elements.
@@ -511,11 +534,8 @@

Role, Property, State, and Tabindex Attributes

Javascript and CSS Source Code

diff --git a/examples/carousel/carousel-2-tablist.html b/examples/carousel/carousel-2-tablist.html index 588ca44c81..c41649f6a2 100644 --- a/examples/carousel/carousel-2-tablist.html +++ b/examples/carousel/carousel-2-tablist.html @@ -279,7 +279,7 @@

-

8 pm Sunday, March 8, on TV: Sneak peak at the final season.

+

8 pm Sunday, March 8, on TV: Sneak peek at the final season.

@@ -373,12 +373,12 @@

Controlling Automatic Slide Rotation

  • The rotation control button is the first element in the screen reader reading order.
  • The rotation control button is always visible so it is available to all users whether they are interacting via a mouse, keyboard, assistive technology, or touch.
  • -
  • If the carousel is rotating, its accessible name is Stop Automatic Slide Show, informing screen reader users that the slides are changing in addition to providing a way to stop the changes.
  • +
  • If the carousel is rotating, the button's accessible name is Stop Automatic Slide Show, informing screen reader users that the slides are changing in addition to providing a way to stop the changes.
  • If the carousel is not rotating, the accessible name of the button is Start Automatic Slide Show.
  • -
  • If a user has activated the button to stop the show, the rotation will only restart if the button is activated. Moving keyboard focus or the mouse out of the carousel will not restart rotation.
  • -
  • If keyboard focus is inside the carousel, or if the mouse is hovering over the carousel, the button is disabled; it cannot be used to start rotation.
  • +
  • If a user activates the rotation control button to start rotation it is assumed the user wants auto-rotation to start immediately and focus or hover states within the carousel are ignored.. The rotation will only stop if the button is activated again,
+
  • The example includes an option to completely disable automatic slide rotation. When this option is selected the start/stop button is not rendered and slides can only be rotated through activation of the tab controls.
  • Color Contrast of Text and Rotation Controls

    @@ -551,7 +551,7 @@

    Tabs

    • Role region is implied for any section element that has an accessible name.
    • -
    • Defines the carousel and its controls as a land mark region.
    • +
    • Defines the carousel and its controls as a landmark region.
    @@ -566,7 +566,7 @@

    Tabs

    • Informs assistive technologies to identify the element as a "carousel" rather than as a "region".
    • -
    • Effects how assistive technologies render the role but does not effect functionality, such as commands for navigating to landmark regions.
    • +
    • Affects how assistive technologies render the role but does not affect functionality, such as commands for navigating to landmark regions.
    @@ -744,7 +744,7 @@

    Tabs

    • Informs assistive technologies to identify the element as a "slide" rather than as a "tabpanel".
    • -
    • Effects how assistive technologies render the role but does not effect assistive technology functionality.
    • +
    • Affects how assistive technologies render the role but does not affect assistive technology functionality.
    diff --git a/examples/carousel/css/carousel-1-more-accessible.css b/examples/carousel/css/carousel-1-more-accessible.css deleted file mode 100644 index 4342f03dd6..0000000000 --- a/examples/carousel/css/carousel-1-more-accessible.css +++ /dev/null @@ -1,185 +0,0 @@ -/* .carousel */ - -.carousel .carousel-inner { - display: static; -} - -.carousel .carousel-item { - display: none; - max-width: 900px; - width: 100%; -} - -.carousel .carousel-item.active { - display: block; -} - -/* More like bootstrap, less accessible */ - -/* Shared CSS for Pause, Next and Previous Slide Controls */ - -.carousel .controls { - margin: 0; - padding: 0; - width: 100%; - position: relative; - height: 36px; - background-color: #eee; - border: 4px solid #eee; - border-radius: 5px 5px 0 0; -} - -.carousel .controls button { - position: absolute; - top: 6px; - display: block; - background-color: transparent; - border: none; - outline: none; -} - -.carousel .controls button.previous { - right: 58px; -} - -.carousel .controls button.next { - right: 4px; -} - -.carousel .controls button.rotation { - left: 4px; -} - -.carousel .controls button svg rect.background { - stroke: black; - fill: black; - stroke-width: 1px; - opacity: 0.8; -} - -.carousel .controls button svg rect.border { - fill: transparent; - stroke: transparent; - stroke-width: 2px; -} - -/* Next and Previous Slide Controls */ - -.carousel .controls button svg polygon { - stroke: white; - fill: white; - stroke-width: 2; - opacity: 1; -} - -.carousel .controls button.rotation svg polygon.pause { - stroke-width: 4; - fill: transparent; - stroke: transparent; -} - -.carousel .controls button.rotation svg polygon.play { - stroke-width: 1; - fill: transparent; - stroke: transparent; -} - -.carousel .controls button.rotation.pause svg polygon.pause, -.carousel .controls button.rotation.play svg polygon.play { - fill: white; - stroke: white; -} - -/* Common focus styling for svg buttons */ - -.carousel .controls button:focus rect.background, -.carousel .controls button:hover rect.background, -.carousel .controls button:focus rect.border, -.carousel .controls button:hover rect.border { - fill: #005a9c; - stroke: #005a9c; - opacity: 1; -} - -.carousel .controls button:focus rect.border { - stroke: white; -} - -/* Caption Positioning */ - -.carousel .carousel-items { - width: 100%; - background-color: #eee; - border: solid 4px #eee; - border-radius: 0 0 5px 5px; -} - -.carousel .carousel-items.focus { - border-color: #005a9c; -} - -.carousel .carousel-item .carousel-image { - margin: 0; - padding: 0; - width: 100%; -} - -.carousel .carousel-item .carousel-image a { - margin: 0; - padding: 0; -} - -.carousel .carousel-item .carousel-image a img { - margin: 0; - padding: 0; - display: block; - overflow: hidden; - max-height: 100%; - max-width: 100%; -} - -.carousel .carousel-item .carousel-caption { - margin: 0; - padding: 0.5em; - width: 100%; - height: 3em; - text-align: center; -} - -.carousel .carousel-item .carousel-caption a { - display: inline-block; - background-color: rgba(0, 0, 0, 0); - padding-left: 0.25em; - padding-right: 0.25em; - padding-top: 0.125em; - padding-bottom: 0.125em; - border-radius: 5px; - border: 2px solid transparent; - margin: 0; - text-decoration: underline; -} - -.carousel .carousel-item .carousel-caption h3 { - margin: 0; - padding: 0; - font-weight: bold; -} - -.carousel .carousel-item .carousel-caption h3 a { - color: black; -} - -.carousel .carousel-item .carousel-caption a:hover { - background-color: rgba(0, 0, 0, 0.1); -} - -.carousel .carousel-item .carousel-caption a:focus { - border-color: #005a9c; - background-color: rgba(0, 0, 0, 0.1); - outline: none; -} - -.carousel .carousel-item .carousel-caption p { - margin: 0; - padding: 0; -} diff --git a/examples/carousel/css/carousel-1.css b/examples/carousel/css/carousel-1.css deleted file mode 100644 index 735f61e3db..0000000000 --- a/examples/carousel/css/carousel-1.css +++ /dev/null @@ -1,165 +0,0 @@ - -/* .carousel */ - -.carousel .carousel-inner { - position: relative; -} - -.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-items { - border: solid 2px transparent; -} - -.carousel .carousel-items.focus { - border-color: white; - outline: solid 3px #005a9c; -} - -.carousel .carousel-item .carousel-image a img { - height: 100%; - width: 100%; -} - -.carousel .carousel-item .carousel-caption a { - text-decoration: underline; -} - -.carousel .carousel-item .carousel-caption a, -.carousel .carousel-item .carousel-caption span.contrast { - display: inline-block; - background-color: rgba(0, 0, 0, 0.65); - padding-left: 0.25em; - padding-right: 0.25em; - border-radius: 5px; - border: 2px solid transparent; - margin: 0; -} - -.carousel .carousel-item .carousel-caption h3 a { - color: #fff; - font-weight: 600; -} - -.carousel .carousel-item .carousel-caption a:hover, -.carousel .carousel-item .carousel-caption span.contrast:hover { - background-color: rgba(0, 0, 0, 1); - margin: 0; -} - -.carousel .carousel-item .carousel-caption a:focus { - background-color: rgba(0, 0, 0, 1); - border-color: #fff; - margin: 0; -} - -.carousel .carousel-item .carousel-caption p { - font-size: 1em; - line-height: 1.5; - margin-bottom: 0; -} - -.carousel .carousel-item .carousel-caption { - position: absolute; - right: 15%; - bottom: 0; - left: 15%; - padding-top: 20px; - padding-bottom: 20px; - color: #fff; - text-align: center; -} - -/* Shared CSS for Pause, Next and Previous Slide Controls */ - -.carousel .controls button { - padding: 0; - position: absolute; - top: 5px; - z-index: 10; - background-color: transparent; - border: none; - outline: none; -} - -.carousel .controls button svg rect.background { - stroke: black; - fill: black; - stroke-width: 1px; - opacity: 0.6; -} - -.carousel .controls button svg rect.border { - fill: transparent; - stroke: transparent; - stroke-width: 2px; -} - -/* Next and Previous Slide Controls */ - -.carousel .controls button svg polygon { - stroke: white; - fill: white; - stroke-width: 2; - opacity: 1; -} - -.carousel .controls button.previous { - right: 50px; -} - -.carousel .controls button.next { - right: 6px; -} - -/* Pause Control */ - -.carousel .controls button.rotation { - left: 6px; -} - -.carousel .controls button.rotation svg polygon.pause { - stroke-width: 4; - fill: transparent; - stroke: transparent; -} - -.carousel .controls button.rotation svg polygon.play { - stroke-width: 1; - fill: transparent; - stroke: transparent; -} - -.carousel .controls button.rotation.pause svg polygon.pause, -.carousel .controls button.rotation.play svg polygon.play { - fill: white; - stroke: white; -} - -/* Common focus styling for svg buttons */ - -.carousel .controls button:focus rect.background, -.carousel .controls button:hover rect.background, -.carousel .controls button:focus rect.border, -.carousel .controls button:hover rect.border { - fill: #005a9c; - stroke: #005a9c; - opacity: 1; -} - -.carousel .controls button:focus rect.border { - stroke: white; -} diff --git a/examples/carousel/css/carousel-prev-next.css b/examples/carousel/css/carousel-prev-next.css new file mode 100644 index 0000000000..d85789b047 --- /dev/null +++ b/examples/carousel/css/carousel-prev-next.css @@ -0,0 +1,306 @@ + +/* .carousel */ + +img.reload { + padding: 0.25em; + display: block-inline; + position: relative; + top: 6px; + height: 0.9em; +} + +.carousel { + background-color: #eee; +} + +.carousel .carousel-inner { + position: relative; +} + +.carousel .carousel-items { + padding: 5px; +} + +.carousel .carousel-items.focus { + padding: 2px; + border: solid 3px #005a9c; +} + +.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-item .carousel-image a img { + height: 100%; + width: 100%; +} + +.carousel .carousel-item .carousel-caption a { + cursor: pointer; +} + + +.carousel .carousel-item .carousel-caption a { + text-decoration: underline; + color: #fff; + font-weight: 600; +} + +.carousel .carousel-item .carousel-caption a, +.carousel .carousel-item .carousel-caption span.contrast { + display: inline-block; + margin: 0; + padding: 6px; + display: inline-block; + background-color: rgba(0, 0, 0, 0.65); + border-radius: 5px; + border: 0 solid transparent; +} + +.carousel .carousel-item .carousel-caption a:hover, +.carousel .carousel-item .carousel-caption span.contrast:hover { + background-color: rgba(0, 0, 0, 1); +} + +.carousel .carousel-item .carousel-caption a:focus { + padding: 4px; + border: 2px solid #fff; + background-color: rgba(0, 0, 0, 1); + outline: none; +} + +.carousel .carousel-item .carousel-caption p { + font-size: 1em; + line-height: 1.5; + margin-bottom: 0; +} + +.carousel .carousel-item .carousel-caption { + position: absolute; + right: 15%; + bottom: 0; + left: 15%; + padding-top: 20px; + padding-bottom: 20px; + color: #fff; + text-align: center; +} + +/* Shared CSS for Pause, Previous and Next Buttons */ + +.carousel .controls { + box-sizing: border-box; + position: absolute; + top: 1em; + z-index: 10; + display: flex; + width: 100%; + padding: 0.25em 1.25em 0; +} + +.carousel .controls button { + position: absolute; + z-index: 10; +} + +.carousel .controls button.previous { + right: 70px; +} + +.carousel .controls button.next { + right: 18px; +} + + +/* SVG Controls */ + + +.carousel .controls button { + flex: 0 0 auto; + margin: 0; + padding: 0; + border: none; + background: transparent; + outline: none; + z-index: 10; +} + +.carousel .controls svg .background { + stroke: black; + fill: black; + stroke-width: 1px; + opacity: 0.6; +} + +.carousel .controls svg .border { + fill: transparent; + stroke: transparent; + stroke-width: 2px; +} + +.carousel .controls svg .pause { + stroke-width: 4; + fill: transparent; + stroke: transparent; +} + +.carousel .controls svg .play { + stroke-width: 1; + fill: transparent; + stroke: transparent; +} + +.carousel .controls .pause svg .pause { + fill: white; + stroke: white; +} + +.carousel .controls .play svg .play { + fill: white; + stroke: white; +} + + +.carousel .controls svg polygon { + fill: white; + stroke: white; +} + +.carousel .controls button:focus svg .background, +.carousel .controls button:hover svg .background, +.carousel .controls button:hover svg .border { + fill: #005a9c; + stroke: #005a9c; + opacity: 1; +} + +.carousel .controls button:focus svg .border { + stroke: white; +} + +/* More accessible carousel styles, with caption and controls above/below image */ + +.carousel-moreaccessible { + padding: 0; + margin: 0; + position: relative; + border: #eee solid 4px; + border-radius: 5px; +} + +.carousel.carousel-moreaccessible .controls { + position: static; + height: 36px; +} + + +.carousel.carousel-moreaccessible .controls button.previous { + right: 60px; +} + +.carousel.carousel-moreaccessible .controls button.next { + right: 6px; +} + +.carousel-moreaccessible .carousel-items, +.carousel-moreaccessible .carousel-items.focus { + padding: 0px; + border: none; +} + +.carousel-moreaccessible .carousel-items .carousel-image a { + display: block; + margin: 0; + padding: 5px; + text-decoration: none; + border: none; +} + +.carousel-moreaccessible .carousel-items.focus .carousel-image a { + padding: 2px; + border: 3px solid #005a9c; +} + +/* More accessible caption styling */ + +.carousel-moreaccessible .carousel-item { + padding: 0; + margin: 0; + max-height: none; +} + +.carousel-moreaccessible .carousel-item .carousel-caption { + position: static; + padding: 0; + margin: 0; + height: 60px; + color: black; +} + +.carousel-moreaccessible .carousel-item .carousel-caption a { + display: inline-block; + margin: 0; + padding: 6px; + color: black; + background-color: transparent; + border: none; + border-radius: 5px; +} + +.carousel .carousel-item .carousel-caption a:focus { + padding: 4px; + border-width: 2px solid #fff; + background-color: rgba(0, 0, 0, 1); + color: #fff; + outline: none; +} + + +.carousel-moreaccessible .carousel-item .carousel-caption span.contrast, +.carousel-moreaccessible .carousel-item .carousel-caption span.contrast:hover { + background-color: transparent; +} + +.carousel-moreaccessible .carousel-item .carousel-caption p { + padding: 0; + margin: 0; +} + +.carousel-moreaccessible .carousel-item .carousel-caption h3 { + font-size: 1.1em; + padding: 0; + margin: 0; +} + +.carousel-moreaccessible .carousel-item .carousel-caption a:hover { + background-color: rgba(0, 0, 0, 0.2); +} + +.carousel-moreaccessible .carousel-item .carousel-caption a:focus { + padding: 4px; + border: 2px solid #005a9c; + background-color: transparent; + color: black; + outline: none; +} + +/* Shared CSS for Pause and Tab Controls */ + +.carousel-moreaccessible .controls { + position: relative; + top: 0; + left: 0; + padding: 0.25em 0.25em 0; +} + diff --git a/examples/carousel/js/carousel-prev-next.js b/examples/carousel/js/carousel-prev-next.js new file mode 100644 index 0000000000..67090b2e98 --- /dev/null +++ b/examples/carousel/js/carousel-prev-next.js @@ -0,0 +1,286 @@ +/* +* File: carousel-prev-next.js +* +* Desc: Carousel widget with Previous and Next Buttons that implements ARIA Authoring Practices +* +*/ + +'use strict'; + +var CarouselPreviousNext = function (node, options) { + // merge passed options with defaults + options = Object.assign({ moreaccessible: false, paused: false, norotate: false }, (options || {})); + + // a prefers-reduced-motion user setting must always override autoplay + var hasReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)"); + if (hasReducedMotion.matches) { + options.paused = true; + } + + /* DOM properties */ + this.domNode = node; + + this.carouselItemNodes = node.querySelectorAll('.carousel-item'); + + this.containerNode = node.querySelector('.carousel-items'); + this.liveRegionNode = node.querySelector('.carousel-items'); + this.pausePlayButtonNode = null; + this.previousButtonNode = null; + this.nextButtonNode = null; + + this.playLabel = 'Start automatic slide show'; + this.pauseLabel = 'Stop automatic slide show'; + + /* State properties */ + this.hasUserActivatedPlay = false; // set when the user activates the play/pause button + this.isAutoRotationDisabled = options.norotate // This property for disabling auto rotation + this.isPlayingEnabled = !options.paused; // This property is also set in updatePlaying method + this.timeInterval = 5000; // length of slide rotation in ms + this.currentIndex = 0; // index of current slide + this.slideTimeout = null; // save reference to setTimeout + + // Pause Button + + var elem = document.querySelector('.carousel .controls button.rotation'); + if (elem) { + this.pausePlayButtonNode = elem; + this.pausePlayButtonNode.addEventListener('click', this.handlePausePlayButtonClick.bind(this)); + } + + // Previous Button + + elem = document.querySelector('.carousel .controls button.previous'); + if (elem) { + this.previousButtonNode = elem; + this.previousButtonNode.addEventListener('click', this.handlePreviousButtonClick.bind(this)); + this.previousButtonNode.addEventListener('focus', this.handleFocusIn.bind(this)); + this.previousButtonNode.addEventListener('blur', this.handleFocusOut.bind(this)); + } + + // Next Button + + elem = document.querySelector('.carousel .controls button.next'); + if (elem) { + this.nextButtonNode = elem; + this.nextButtonNode.addEventListener('click', this.handleNextButtonClick.bind(this)); + this.nextButtonNode.addEventListener('focus', this.handleFocusIn.bind(this)); + this.nextButtonNode.addEventListener('blur', this.handleFocusOut.bind(this)); + } + + // Carousel item events + + for (var i = 0; i < this.carouselItemNodes.length; i++ ) { + var caouselItemNode = this.carouselItemNodes[i]; + + // support stopping rotation when any element receives focus in the tabpanel + caouselItemNode.addEventListener('focusin', this.handleFocusIn.bind(this)); + caouselItemNode.addEventListener('focusout', this.handleFocusOut.bind(this)); + + var imageLinkNode = caouselItemNode.querySelector('.carousel-image a'); + + if (imageLinkNode) { + imageLinkNode.addEventListener('focus', this.handleImageLinkFocus.bind(this)); + imageLinkNode.addEventListener('blur', this.handleImageLinkBlur.bind(this)); + } + + } + + // Handle hover events + this.domNode.addEventListener('mouseover', this.handleMouseOver.bind(this)); + this.domNode.addEventListener('mouseout', this.handleMouseOut.bind(this)); + + // initialize behavior based on options + + this.enableOrDisableAutoRotation(options.norotate); + this.updatePlaying(!options.paused && !options.norotate); + this.setAccessibleStyling(options.moreaccessible); + this.rotateSlides(); +} + +/* Public function to disable/enable rotation and if false, hide pause/play button*/ +CarouselPreviousNext.prototype.enableOrDisableAutoRotation = function(disable) { + this.isAutoRotationDisabled = disable; + this.pausePlayButtonNode.hidden = disable; +} + +/* Public function to update controls/caption styling */ +CarouselPreviousNext.prototype.setAccessibleStyling = function(accessible) { + if (accessible) { + this.domNode.classList.add('carousel-moreaccessible'); + } + else { + this.domNode.classList.remove('carousel-moreaccessible'); + } +} + +CarouselPreviousNext.prototype.showCarouselItem = function (index) { + + this.currentIndex = index; + + for(var i = 0; i < this.carouselItemNodes.length; i++) { + var carouselItemNode = this.carouselItemNodes[i]; + if (index === i) { + carouselItemNode.classList.add('active'); + } + else { + carouselItemNode.classList.remove('active'); + } + } +} + +CarouselPreviousNext.prototype.previousCarouselItem = function () { + var nextIndex = this.currentIndex - 1; + if (nextIndex < 0) { + nextIndex = this.carouselItemNodes.length - 1; + } + this.showCarouselItem(nextIndex); +} + +CarouselPreviousNext.prototype.nextCarouselItem = function () { + var nextIndex = this.currentIndex + 1; + if (nextIndex >= this.carouselItemNodes.length) { + nextIndex = 0; + } + this.showCarouselItem(nextIndex); +} + +CarouselPreviousNext.prototype.rotateSlides = function () { + if (!this.isAutoRotationDisabled ) { + if ((!this.hasFocus && + !this.hasHover && + this.isPlayingEnabled) || + this.hasUserActivatedPlay) { + this.nextCarouselItem(); + } + } + + this.slideTimeout = setTimeout(this.rotateSlides.bind(this), this.timeInterval); +} + +CarouselPreviousNext.prototype.updatePlaying = function (play) { + this.isPlayingEnabled = play; + + if (play) { + this.pausePlayButtonNode.setAttribute('aria-label', this.pauseLabel); + this.pausePlayButtonNode.classList.remove('play'); + this.pausePlayButtonNode.classList.add('pause'); + this.liveRegionNode.setAttribute('aria-live', 'off'); + } + else { + this.pausePlayButtonNode.setAttribute('aria-label', this.playLabel); + this.pausePlayButtonNode.classList.remove('pause'); + this.pausePlayButtonNode.classList.add('play'); + this.liveRegionNode.setAttribute('aria-live', 'polite'); + } +} + + /* Event Handlers */ + +CarouselPreviousNext.prototype.handleImageLinkFocus = function () { + this.liveRegionNode.classList.add('focus'); +} + +CarouselPreviousNext.prototype.handleImageLinkBlur = function () { + this.liveRegionNode.classList.remove('focus'); +} + +CarouselPreviousNext.prototype.handleMouseOver = function (event) { + if (!this.pausePlayButtonNode.contains(event.target)) { + this.hasHover = true; + } +} + +CarouselPreviousNext.prototype.handleMouseOut = function () { + this.hasHover = false; +} + + /* EVENT HANDLERS */ + +CarouselPreviousNext.prototype.handlePausePlayButtonClick = function () { + this.hasUserActivatedPlay = !this.isPlayingEnabled; + this.updatePlaying(!this.isPlayingEnabled); +} + +CarouselPreviousNext.prototype.handlePreviousButtonClick = function () { + this.previousCarouselItem(); +} + +CarouselPreviousNext.prototype.handleNextButtonClick = function () { + this.nextCarouselItem(); +} + + /* Event Handlers for carousel items*/ + +CarouselPreviousNext.prototype.handleFocusIn = function () { + this.liveRegionNode.setAttribute('aria-live', 'polite'); + this.hasFocus = true; +} + +CarouselPreviousNext.prototype.handleFocusOut = function () { + if (this.isPlayingEnabled) { + this.liveRegionNode.setAttribute('aria-live', 'off'); + } + this.hasFocus = false; +} + +/* Initialize Carousel and options */ + +window.addEventListener('load', function () { + var carouselEls = document.querySelectorAll('.carousel'); + var carousels = []; + + // set example behavior based on + // default setting of the checkboxes and the parameters in the URL + // update checkboxes based on any corresponding URL parameters + var checkboxes = document.querySelectorAll('.carousel-options input[type=checkbox]'); + var urlParams = new URLSearchParams(location.search); + var carouselOptions = {}; + + // initialize example features based on + // default setting of the checkboxes and the parameters in the URL + // update checkboxes based on any corresponding URL parameters + checkboxes.forEach(function(checkbox) { + var checked = checkbox.checked; + + if (urlParams.has(checkbox.value)) { + var urlParam = urlParams.get(checkbox.value); + if (typeof urlParam === 'string') { + checked = urlParam === 'true'; + checkbox.checked = checked; + } + } + + carouselOptions[checkbox.value] = checkbox.checked; + }); + + carouselEls.forEach(function (node) { + carousels.push(new CarouselPreviousNext(node, carouselOptions)); + }); + + // add change event to checkboxes + checkboxes.forEach(function(checkbox) { + var updateEvent; + switch(checkbox.value) { + case 'moreaccessible': + updateEvent = 'setAccessibleStyling'; + break; + case 'norotate': + updateEvent = 'enableOrDisableAutoRotation'; + break; + } + + // update the carousel behavior and URL when a checkbox state changes + checkbox.addEventListener('change', function(event) { + urlParams.set(event.target.value, event.target.checked + ''); + window.history.replaceState(null, '', window.location.pathname + '?' + urlParams); + + if (updateEvent) { + carousels.forEach(function (carousel) { + carousel[updateEvent](event.target.checked); + }); + } + }); + }); +}, false); + + diff --git a/examples/carousel/js/carousel.js b/examples/carousel/js/carousel.js deleted file mode 100644 index 28c1a37bef..0000000000 --- a/examples/carousel/js/carousel.js +++ /dev/null @@ -1,226 +0,0 @@ -/* -* File: Carousel.js -* -* Desc: Carousel widget that implements ARIA Authoring Practices -* -*/ - -'use strict'; - -/* -* @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.playLabel = 'Start automatic slide show'; - this.pauseLabel = 'Stop automatic slide show'; - - this.rotate = true; - this.hasFocus = false; - this.hasHover = false; - this.isStopped = false; - this.timeInterval = 5000; -}; - -Carousel.prototype.init = function () { - - var elems, elem, button, items, item, imageLinks, i; - - this.liveRegionNode = this.domNode.querySelector('.carousel-items'); - - items = this.domNode.querySelectorAll('.carousel-item'); - - for (i = 0; i < items.length; i++) { - 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; - - 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)); - } - - } - - // Pause, Next Slide and Previous Slide Buttons - - elems = document.querySelectorAll('.carousel .controls button'); - - for (i = 0; i < elems.length; i++) { - elem = elems[i]; - - if (elem.classList.contains('rotation')) { - button = new PauseButton(elem, this); - this.pauseButton = elem; - this.pauseButton.classList.add('pause'); - this.pauseButton.setAttribute('aria-label', this.pauseLabel); - } - else { - button = new CarouselButton(elem, this); - } - - button.init(); - } - - this.currentItem = this.firstItem; - - 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.updateRotation = function () { - - if (!this.hasHover && !this.hasFocus && !this.isStopped) { - this.rotate = true; - this.liveRegionNode.setAttribute('aria-live', 'off'); - } - else { - this.rotate = false; - this.liveRegionNode.setAttribute('aria-live', 'polite'); - } - - if (this.isStopped) { - this.pauseButton.setAttribute('aria-label', this.playLabel); - this.pauseButton.classList.remove('pause'); - this.pauseButton.classList.add('play'); - } - else { - this.pauseButton.setAttribute('aria-label', this.pauseLabel); - this.pauseButton.classList.remove('play'); - this.pauseButton.classList.add('pause'); - } - -}; - -Carousel.prototype.toggleRotation = function () { - if (this.isStopped) { - if (!this.hasHover && !this.hasFocus) { - this.isStopped = false; - } - } - else { - this.isStopped = true; - } - - this.updateRotation(); - -}; - -Carousel.prototype.handleImageLinkFocus = function () { - this.liveRegionNode.classList.add('focus'); -}; - -Carousel.prototype.handleImageLinkBlur = function () { - this.liveRegionNode.classList.remove('focus'); -}; - -Carousel.prototype.handleMouseOver = function (event) { - if (!this.pauseButton.contains(event.target)) { - this.hasHover = true; - } - this.updateRotation(); -}; - -Carousel.prototype.handleMouseOut = function () { - this.hasHover = false; - this.updateRotation(); -}; - -/* Initialize Carousel Tablists */ - -window.addEventListener('load', function () { - 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/js/carouselButtons.js b/examples/carousel/js/carouselButtons.js deleted file mode 100644 index 7590d68216..0000000000 --- a/examples/carousel/js/carouselButtons.js +++ /dev/null @@ -1,70 +0,0 @@ -/* -* File: carouselButton.js -* -* Desc: Carousel Button widget that implements ARIA Authoring Practices -*/ - -'use strict'; - -/* -* @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('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.handleClick = function (event) { - this.changeItem(); -}; - -CarouselButton.prototype.handleFocus = function (event) { - this.carousel.hasFocus = true; - this.domNode.classList.add('focus'); - this.carousel.updateRotation(); -}; - -CarouselButton.prototype.handleBlur = function (event) { - this.carousel.hasFocus = false; - this.domNode.classList.remove('focus'); - this.carousel.updateRotation(); -}; diff --git a/examples/carousel/js/carouselItem.js b/examples/carousel/js/carouselItem.js deleted file mode 100644 index 93a44a1931..0000000000 --- a/examples/carousel/js/carouselItem.js +++ /dev/null @@ -1,44 +0,0 @@ -/* -* File: CarouselItem.js -* -* Desc: Carousel Tab widget that implements ARIA Authoring Practices -*/ - -'use strict'; - -/* -* @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.updateRotation(); -}; - -CarouselItem.prototype.handleFocusOut = function (event) { - this.domNode.classList.remove('focus'); - this.carousel.hasFocus = false; - this.carousel.updateRotation(); -}; diff --git a/examples/carousel/js/pauseButton.js b/examples/carousel/js/pauseButton.js deleted file mode 100644 index b2e4fb5c7e..0000000000 --- a/examples/carousel/js/pauseButton.js +++ /dev/null @@ -1,24 +0,0 @@ -/* -* File: pasueButton.js -* -* Desc: Implements the pause button for the carousel widget -* -*/ - -'use strict'; - -var PauseButton = function (domNode, carouselObj) { - this.domNode = domNode; - - this.carousel = carouselObj; -}; - -PauseButton.prototype.init = function () { - this.domNode.addEventListener('click', this.handleClick.bind(this)); -}; - -/* EVENT HANDLERS */ - -PauseButton.prototype.handleClick = function () { - this.carousel.toggleRotation(); -}; diff --git a/test/tests/carousel_carousel-1.js b/test/tests/carousel_carousel-1-prev-next.js similarity index 98% rename from test/tests/carousel_carousel-1.js rename to test/tests/carousel_carousel-1-prev-next.js index 5562c67d83..5629c964ae 100644 --- a/test/tests/carousel_carousel-1.js +++ b/test/tests/carousel_carousel-1-prev-next.js @@ -8,7 +8,7 @@ const assertAriaLabelExists = require('../util/assertAriaLabelExists'); const assertAriaRoles = require('../util/assertAriaRoles'); const assertTabOrder = require('../util/assertTabOrder'); -const exampleFile = 'carousel/carousel-1.html'; +const exampleFile = 'carousel/carousel-1-prev-next.html'; const ex = { landmarkSelector: '#myCarousel', @@ -58,7 +58,7 @@ ariaTest('slide container have aria-live initially set to off', exampleFile, 'ca // On page load, `aria-level` is `off` await assertAttributeValues(t, ex.slideContainerSelector, 'aria-live', 'off'); - // Focus on the widget, and aria-selected should change to 'polite' + // Focus on the widget, and aria-live should change to 'polite' await t.context.session.findElement(By.css(ex.nextButtonSelector)).sendKeys(Key.ENTER); await assertAttributeValues(t, ex.slideContainerSelector, 'aria-live', 'polite');