Ver Fonte

Add attributes for the "Tabbed carousel" pattern.

NaotoshiFujita há 3 anos atrás
pai
commit
94777673d8

+ 27 - 11
dist/js/splide.js

@@ -686,6 +686,7 @@ function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _d
   var ARIA_PREFIX = "aria-";
   var ARIA_CONTROLS = ARIA_PREFIX + "controls";
   var ARIA_CURRENT = ARIA_PREFIX + "current";
+  var ARIA_SELECTED = ARIA_PREFIX + "selected";
   var ARIA_LABEL = ARIA_PREFIX + "label";
   var ARIA_HIDDEN = ARIA_PREFIX + "hidden";
   var ARIA_ORIENTATION = ARIA_PREFIX + "orientation";
@@ -809,10 +810,17 @@ function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _d
       return [CLASS_ROOT + "--" + options.type, CLASS_ROOT + "--" + options.direction, options.drag && CLASS_ROOT + "--draggable", options.isNavigation && CLASS_ROOT + "--nav", CLASS_ACTIVE];
     }
 
+    function isTab() {
+      return !!(options.pagination || options.isNavigation || Splide2.splides.some(function (target) {
+        return !target.isParent && target.splide.options.isNavigation;
+      }));
+    }
+
     return assign(elements, {
       setup: setup,
       mount: mount,
-      destroy: destroy
+      destroy: destroy,
+      isTab: isTab
     });
   }
 
@@ -833,6 +841,7 @@ function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _d
     var isNavigation = options.isNavigation,
         updateOnMove = options.updateOnMove,
         i18n = options.i18n;
+    var isTab = Components.Elements.isTab;
     var resolve = Components.Direction.resolve;
     var styles = getAttribute(slide, "style");
     var isClone = slideIndex > -1;
@@ -843,7 +852,7 @@ function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _d
     function mount() {
       if (!isClone) {
         slide.id = root.id + "-slide" + pad(index + 1);
-        setAttribute(slide, ROLE, "group");
+        setAttribute(slide, ROLE, isTab() ? "tabpanel" : "group");
         setAttribute(slide, ARIA_ROLEDESCRIPTION, i18n.slide);
         setAttribute(slide, ARIA_LABEL, format(i18n.slideLabel, [index + 1, Splide2.length]));
       }
@@ -872,14 +881,13 @@ function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _d
     }
 
     function initNavigation() {
-      var idx = isClone ? slideIndex : index;
-      var label = format(i18n.slideX, idx + 1);
       var controls = Splide2.splides.map(function (target) {
-        return target.splide.root.id;
+        var Slide2 = target.splide.Components.Slides.getAt(index);
+        return Slide2 ? Slide2.slide.id : "";
       }).join(" ");
-      setAttribute(slide, ARIA_LABEL, label);
+      setAttribute(slide, ARIA_LABEL, format(i18n.slideX, (isClone ? slideIndex : index) + 1));
       setAttribute(slide, ARIA_CONTROLS, controls);
-      setAttribute(slide, ROLE, "menuitem");
+      setAttribute(slide, ROLE, "tab");
       updateActivity(isActive());
     }
 
@@ -904,7 +912,7 @@ function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _d
         toggleClass(slide, CLASS_ACTIVE, active);
 
         if (isNavigation) {
-          setAttribute(slide, ARIA_CURRENT, active || null);
+          setAttribute(slide, ARIA_SELECTED, active || null);
         }
 
         emit(active ? EVENT_ACTIVE : EVENT_INACTIVE, self);
@@ -2477,6 +2485,8 @@ function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _d
       var parent = options.pagination === "slider" && Elements.slider || Elements.root;
       var max = hasFocus() ? length : ceil(length / perPage);
       list = create("ul", classes.pagination, parent);
+      setAttribute(list, ROLE, "tablist");
+      setAttribute(list, ARIA_LABEL, i18n.select);
 
       for (var i = 0; i < max; i++) {
         var li = create("li", null, list);
@@ -2484,9 +2494,14 @@ function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _d
           class: classes.page,
           type: "button"
         }, li);
+        var controls = Slides.getIn(i).map(function (Slide) {
+          return Slide.slide.id;
+        });
         var text = !hasFocus() && perPage > 1 ? i18n.pageX : i18n.slideX;
         bind(button, "click", apply(onClick, i));
-        setAttribute(button, ARIA_CONTROLS, Components2.Elements.list.id);
+        setAttribute(li, ROLE, "none");
+        setAttribute(button, ROLE, "tab");
+        setAttribute(button, ARIA_CONTROLS, controls.join(" "));
         setAttribute(button, ARIA_LABEL, format(text, i + 1));
         items.push({
           li: li,
@@ -2513,12 +2528,12 @@ function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _d
 
       if (prev) {
         removeClass(prev.button, CLASS_ACTIVE);
-        removeAttribute(prev.button, ARIA_CURRENT);
+        removeAttribute(prev.button, ARIA_SELECTED);
       }
 
       if (curr) {
         addClass(curr.button, CLASS_ACTIVE);
-        setAttribute(curr.button, ARIA_CURRENT, true);
+        setAttribute(curr.button, ARIA_SELECTED, true);
       }
 
       emit(EVENT_PAGINATION_UPDATED, {
@@ -2708,6 +2723,7 @@ function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _d
     pause: "Pause autoplay",
     carousel: "carousel",
     slide: "slide",
+    select: "Select slide to show",
     slideLabel: "%s of %s"
   };
   var DEFAULTS = {

+ 16 - 0
src/js/components/Elements/Elements.ts

@@ -63,6 +63,7 @@ export interface ElementCollection {
  * @since 3.0.0
  */
 export interface ElementsComponent extends BaseComponent, ElementCollection {
+  isTab(): boolean;
 }
 
 /**
@@ -211,9 +212,24 @@ export function Elements( Splide: Splide, Components: Components, options: Optio
     ];
   }
 
+  /**
+   * Indicates whether the slide should be implemented as "Tabbed Carousel" or not.
+   * The last condition checks if one of sync targets behaves as a navigation for this slider or not.
+   *
+   * @todo hasNavigation
+   *
+   * @return `true` if the slider should be tabbed, or otherwise `false`.
+   */
+  function isTab(): boolean {
+    return !! ( options.pagination || options.isNavigation || Splide.splides.some( target => {
+      return ! target.isParent && target.splide.options.isNavigation;
+    } ) );
+  }
+
   return assign( elements, {
     setup,
     mount,
     destroy,
+    isTab,
   } );
 }

+ 18 - 10
src/js/components/Pagination/Pagination.ts

@@ -1,4 +1,4 @@
-import { ARIA_CONTROLS, ARIA_CURRENT, ARIA_LABEL } from '../../constants/attributes';
+import { ARIA_CONTROLS, ARIA_LABEL, ARIA_SELECTED, ROLE } from '../../constants/attributes';
 import { CLASS_ACTIVE } from '../../constants/classes';
 import {
   EVENT_MOVE,
@@ -12,7 +12,8 @@ import { EventInterface } from '../../constructors';
 import { Splide } from '../../core/Splide/Splide';
 import { BaseComponent, Components, Options } from '../../types';
 import {
-  addClass, apply,
+  addClass,
+  apply,
   ceil,
   create,
   empty,
@@ -58,20 +59,22 @@ export interface PaginationItem {
 }
 
 /**
- * The component for handling previous and next arrows.
+ * The component for the pagination UI (a slide picker).
  *
+ * @link https://www.w3.org/TR/2021/NOTE-wai-aria-practices-1.2-20211129/#grouped-carousel-elements
  * @since 3.0.0
  *
  * @param Splide     - A Splide instance.
  * @param Components - A collection of components.
  * @param options    - Options.
  *
- * @return A Arrows component object.
+ * @return A Pagination component object.
  */
 export function Pagination( Splide: Splide, Components: Components, options: Options ): PaginationComponent {
   const { on, emit, bind, unbind } = EventInterface( Splide );
   const { Slides, Elements, Controller } = Components;
   const { hasFocus, getIndex } = Controller;
+  const { isTab } = Elements;
 
   /**
    * Stores all pagination items.
@@ -127,15 +130,20 @@ export function Pagination( Splide: Splide, Components: Components, options: Opt
     const max    = hasFocus() ? length : ceil( length / perPage );
 
     list = create( 'ul', classes.pagination, parent );
+    setAttribute( list, ROLE, 'tablist' );
+    setAttribute( list, ARIA_LABEL, i18n.select );
 
     for ( let i = 0; i < max; i++ ) {
-      const li     = create( 'li', null, list );
-      const button = create( 'button', { class: classes.page, type: 'button' }, li );
-      const text   = ! hasFocus() && perPage > 1 ? i18n.pageX : i18n.slideX;
+      const li       = create( 'li', null, list );
+      const button   = create( 'button', { class: classes.page, type: 'button' }, li );
+      const controls = Slides.getIn( i ).map( Slide => Slide.slide.id );
+      const text     = ! hasFocus() && perPage > 1 ? i18n.pageX : i18n.slideX;
 
       bind( button, 'click', apply( onClick, i ) );
 
-      setAttribute( button, ARIA_CONTROLS, Components.Elements.list.id );
+      setAttribute( li, ROLE, 'none' );
+      setAttribute( button, ROLE, 'tab' );
+      setAttribute( button, ARIA_CONTROLS, controls.join( ' ' ) );
       setAttribute( button, ARIA_LABEL, format( text, i + 1 ) );
 
       items.push( { li, button, page: i } );
@@ -177,12 +185,12 @@ export function Pagination( Splide: Splide, Components: Components, options: Opt
 
     if ( prev ) {
       removeClass( prev.button, CLASS_ACTIVE );
-      removeAttribute( prev.button, ARIA_CURRENT );
+      removeAttribute( prev.button, ARIA_SELECTED );
     }
 
     if ( curr ) {
       addClass( curr.button, CLASS_ACTIVE );
-      setAttribute( curr.button, ARIA_CURRENT, true );
+      setAttribute( curr.button, ARIA_SELECTED, true );
     }
 
     emit( EVENT_PAGINATION_UPDATED, { list, items }, prev, curr );

+ 13 - 9
src/js/components/Slides/Slide.ts

@@ -1,9 +1,10 @@
 import {
   ALL_ATTRIBUTES,
   ARIA_CONTROLS,
-  ARIA_CURRENT,
   ARIA_HIDDEN,
-  ARIA_LABEL, ARIA_ROLEDESCRIPTION,
+  ARIA_LABEL,
+  ARIA_ROLEDESCRIPTION,
+  ARIA_SELECTED,
   ROLE,
   TAB_INDEX,
 } from '../../constants/attributes';
@@ -86,6 +87,7 @@ export function Slide( Splide: Splide, index: number, slideIndex: number, slide:
   const { on, emit, bind, destroy: destroyEvents } = EventInterface( Splide );
   const { Components, root, options } = Splide;
   const { isNavigation, updateOnMove, i18n } = options;
+  const { isTab } = Components.Elements;
   const { resolve } = Components.Direction;
   const styles         = getAttribute( slide, 'style' );
   const isClone        = slideIndex > -1;
@@ -103,7 +105,8 @@ export function Slide( Splide: Splide, index: number, slideIndex: number, slide:
   function mount( this: SlideComponent ): void {
     if ( ! isClone ) {
       slide.id = `${ root.id }-slide${ pad( index + 1 ) }`;
-      setAttribute( slide, ROLE, 'group' );
+
+      setAttribute( slide, ROLE, isTab() ? 'tabpanel' : 'group' );
       setAttribute( slide, ARIA_ROLEDESCRIPTION, i18n.slide );
       setAttribute( slide, ARIA_LABEL, format( i18n.slideLabel, [ index + 1, Splide.length ] ) );
     }
@@ -142,13 +145,14 @@ export function Slide( Splide: Splide, index: number, slideIndex: number, slide:
    * Initializes slides as navigation.
    */
   function initNavigation(): void {
-    const idx      = isClone ? slideIndex : index;
-    const label    = format( i18n.slideX, idx + 1 );
-    const controls = Splide.splides.map( target => target.splide.root.id ).join( ' ' );
+    const controls = Splide.splides.map( target => {
+      const Slide = target.splide.Components.Slides.getAt( index );
+      return Slide ? Slide.slide.id : '';
+    } ).join( ' ' );
 
-    setAttribute( slide, ARIA_LABEL, label );
+    setAttribute( slide, ARIA_LABEL, format( i18n.slideX, ( isClone ? slideIndex : index ) + 1 ) );
     setAttribute( slide, ARIA_CONTROLS, controls );
-    setAttribute( slide, ROLE, 'menuitem' );
+    setAttribute( slide, ROLE, 'tab' )
 
     updateActivity( isActive() );
   }
@@ -187,7 +191,7 @@ export function Slide( Splide: Splide, index: number, slideIndex: number, slide:
       toggleClass( slide, CLASS_ACTIVE, active );
 
       if ( isNavigation ) {
-        setAttribute( slide, ARIA_CURRENT, active || null );
+        setAttribute( slide, ARIA_SELECTED, active || null );
       }
 
       emit( active ? EVENT_ACTIVE : EVENT_INACTIVE, self );

+ 1 - 0
src/js/constants/attributes.ts

@@ -5,6 +5,7 @@ export const DISABLED  = 'disabled';
 export const ARIA_PREFIX          = 'aria-';
 export const ARIA_CONTROLS        = `${ ARIA_PREFIX }controls`;
 export const ARIA_CURRENT         = `${ ARIA_PREFIX }current`;
+export const ARIA_SELECTED        = `${ ARIA_PREFIX }selected`;
 export const ARIA_LABEL           = `${ ARIA_PREFIX }label`;
 export const ARIA_HIDDEN          = `${ ARIA_PREFIX }hidden`;
 export const ARIA_ORIENTATION     = `${ ARIA_PREFIX }orientation`;

+ 1 - 0
src/js/constants/i18n.ts

@@ -14,5 +14,6 @@ export const I18N = {
   pause     : 'Pause autoplay',
   carousel  : 'carousel',
   slide     : 'slide',
+  select    : 'Select slide to show',
   slideLabel: '%s of %s', // [ slide number ] / [ slide size ]
 };