]> source.dussan.org Git - jquery-ui.git/commitdiff
Accordion: Fixed ARIA support and added proper keyboard support.
authorHans Hillen <hans.hillen@gmail.com>
Tue, 27 Mar 2012 14:19:35 +0000 (10:19 -0400)
committerScott González <scott.gonzalez@gmail.com>
Tue, 27 Mar 2012 14:19:35 +0000 (10:19 -0400)
tests/unit/accordion/accordion_core.js
ui/jquery.ui.accordion.js

index de1b6604654d133c902b4dea7c395f071ae3c022..0d756c97c21a41b730fa9272f59710009e4d1f52 100644 (file)
@@ -25,24 +25,50 @@ test( "handle click on header-descendant", function() {
 });
 
 test( "accessibility", function () {
-       expect( 13 );
-       var element = $( "#list1" ).accordion().accordion( "option", "active", 1 );
+       expect( 37 );
+       var element = $( "#list1" ).accordion({
+               active: 1
+       });
        var headers = element.find( ".ui-accordion-header" );
 
-       equal( headers.eq( 1 ).attr( "tabindex" ), 0, "active header should have tabindex=0" );
-       equal( headers.eq( 0 ).attr( "tabindex" ), -1, "inactive header should have tabindex=-1" );
-       equal( element.attr( "role" ), "tablist", "main role" );
-       equal( headers.attr( "role" ), "tab", "tab roles" );
-       equal( headers.next().attr( "role" ), "tabpanel", "tabpanel roles" );
-       equal( headers.eq( 1 ).attr( "aria-expanded" ), "true", "active tab has aria-expanded" );
-       equal( headers.eq( 0 ).attr( "aria-expanded" ), "false", "inactive tab has aria-expanded" );
-       equal( headers.eq( 1 ).attr( "aria-selected" ), "true", "active tab has aria-selected" );
-       equal( headers.eq( 0 ).attr( "aria-selected" ), "false", "inactive tab has aria-selected" );
+       equal( element.attr( "role" ), "tablist", "element role" );
+       headers.each(function( i ) {
+               var header = headers.eq( i ),
+                       panel = header.next();
+               equal( header.attr( "role" ), "tab", "header " + i + " role" );
+               equal( header.attr( "aria-controls" ), panel.attr( "id" ), "header " + i + " aria-controls" );
+               equal( panel.attr( "role" ), "tabpanel", "panel " + i + " role" );
+               equal( panel.attr( "aria-labelledby" ), header.attr( "id" ), "panel " + i + " aria-labelledby" );
+       });
+
+       equal( headers.eq( 1 ).attr( "tabindex" ), 0, "active header has tabindex=0" );
+       equal( headers.eq( 1 ).attr( "aria-selected" ), "true", "active tab has aria-selected=true" );
+       equal( headers.eq( 1 ).next().attr( "aria-expanded" ), "true", "active tabpanel has aria-expanded=true" );
+       equal( headers.eq( 1 ).next().attr( "aria-hidden" ), "false", "active tabpanel has aria-hidden=false" );
+       equal( headers.eq( 0 ).attr( "tabindex" ), -1, "active header has tabindex=-1" );
+       equal( headers.eq( 0 ).attr( "aria-selected" ), "false", "active tab has aria-selected=false" );
+       equal( headers.eq( 0 ).next().attr( "aria-expanded" ), "false", "active tabpanel has aria-expanded=false" );
+       equal( headers.eq( 0 ).next().attr( "aria-hidden" ), "true", "active tabpanel has aria-hidden=true" );
+       equal( headers.eq( 2 ).attr( "tabindex" ), -1, "active header has tabindex=-1" );
+       equal( headers.eq( 2 ).attr( "aria-selected" ), "false", "active tab has aria-selected=false" );
+       equal( headers.eq( 2 ).next().attr( "aria-expanded" ), "false", "active tabpanel has aria-expanded=false" );
+       equal( headers.eq( 2 ).next().attr( "aria-hidden" ), "true", "active tabpanel has aria-hidden=true" );
+
        element.accordion( "option", "active", 0 );
-       equal( headers.eq( 0 ).attr( "aria-expanded" ), "true", "newly active tab has aria-expanded" );
-       equal( headers.eq( 1 ).attr( "aria-expanded" ), "false", "newly inactive tab has aria-expanded" );
-       equal( headers.eq( 0 ).attr( "aria-selected" ), "true", "active tab has aria-selected" );
-       equal( headers.eq( 1 ).attr( "aria-selected" ), "false", "inactive tab has aria-selected" );
+       equal( headers.eq( 0 ).attr( "tabindex" ), 0, "active header has tabindex=0" );
+       equal( headers.eq( 0 ).attr( "aria-selected" ), "true", "active tab has aria-selected=true" );
+       equal( headers.eq( 0 ).next().attr( "aria-expanded" ), "true", "active tabpanel has aria-expanded=true" );
+       equal( headers.eq( 0 ).next().attr( "aria-hidden" ), "false", "active tabpanel has aria-hidden=false" );
+       equal( headers.eq( 1 ).attr( "tabindex" ), -1, "active header has tabindex=-1" );
+       equal( headers.eq( 1 ).attr( "aria-selected" ), "false", "active tab has aria-selected=false" );
+       equal( headers.eq( 1 ).next().attr( "aria-expanded" ), "false", "active tabpanel has aria-expanded=false" );
+       equal( headers.eq( 1 ).next().attr( "aria-hidden" ), "true", "active tabpanel has aria-hidden=true" );
+       equal( headers.eq( 2 ).attr( "tabindex" ), -1, "active header has tabindex=-1" );
+       equal( headers.eq( 2 ).attr( "aria-selected" ), "false", "active tab has aria-selected=false" );
+       equal( headers.eq( 2 ).next().attr( "aria-expanded" ), "false", "active tabpanel has aria-expanded=false" );
+       equal( headers.eq( 2 ).next().attr( "aria-hidden" ), "true", "active tabpanel has aria-hidden=true" );
 });
 
+// TODO: keyboard support
+
 }( jQuery ) );
index 2200732ac90f93fb95714b90c66336c1c6851146..2e68889ef7bc836856c43574b032eaf2d059f87f 100644 (file)
@@ -12,6 +12,7 @@
  *     jquery.ui.widget.js
  */
 (function( $, undefined ) {
+       var uid = 0;
 
 $.widget( "ui.accordion", {
        version: "@VERSION",
@@ -33,7 +34,9 @@ $.widget( "ui.accordion", {
        },
 
        _create: function() {
-               var options = this.options;
+               var accordionId = this.accordionId = "ui-accordion-" +
+                               (this.element.attr( "id" ) || ++uid),
+                       options = this.options;
 
                this.prevShow = this.prevHide = $();
                this.element.addClass( "ui-accordion ui-widget ui-helper-reset" );
@@ -68,18 +71,36 @@ $.widget( "ui.accordion", {
 
                this.headers
                        .attr( "role", "tab" )
+                       .each(function( i ) {
+                               var header = $( this ),
+                                       headerId = header.attr( "id" ),
+                                       panel = header.next(),
+                                       panelId = panel.attr( "id" );
+                               if ( !headerId ) {
+                                       headerId = accordionId + "-header-" + i;
+                                       header.attr( "id", headerId );
+                               }
+                               if ( !panelId ) {
+                                       panelId = accordionId + "-panel-" + i;
+                                       panel.attr( "id", panelId );
+                               }
+                               header.attr( "aria-controls", panelId );
+                               panel.attr( "aria-labelledby", headerId );
+                       })
                        .next()
                                .attr( "role", "tabpanel" );
 
                this.headers
                        .not( this.active )
                        .attr({
-                               // TODO: document support for each of these
-                               "aria-expanded": "false",
                                "aria-selected": "false",
                                tabIndex: -1
                        })
                        .next()
+                               .attr({
+                                       "aria-expanded": "false",
+                                       "aria-hidden": "true"
+                               })
                                .hide();
 
                // make sure at least one header is in the tab order
@@ -87,10 +108,14 @@ $.widget( "ui.accordion", {
                        this.headers.eq( 0 ).attr( "tabIndex", 0 );
                } else {
                        this.active.attr({
-                               "aria-expanded": "true",
                                "aria-selected": "true",
                                tabIndex: 0
-                       });
+                       })
+                       .next()
+                               .attr({
+                                       "aria-expanded": "true",
+                                       "aria-hidden": "false"
+                               });
                }
 
                this._setupEvents( options.event );
@@ -124,6 +149,8 @@ $.widget( "ui.accordion", {
        },
 
        _destroy: function() {
+               var accordionId = this.accordionId;
+
                // clean up main element
                this.element
                        .removeClass( "ui-accordion ui-widget ui-helper-reset" )
@@ -131,19 +158,31 @@ $.widget( "ui.accordion", {
 
                // clean up headers
                this.headers
-                       .unbind( ".accordion" )
                        .removeClass( "ui-accordion-header ui-accordion-header-active ui-helper-reset ui-state-default ui-corner-all ui-state-active ui-state-disabled ui-corner-top" )
                        .removeAttr( "role" )
-                       .removeAttr( "aria-expanded" )
                        .removeAttr( "aria-selected" )
-                       .removeAttr( "tabIndex" );
+                       .removeAttr( "aria-controls" )
+                       .removeAttr( "tabIndex" )
+                       .each(function() {
+                               if ( /^ui-accordion/.test( this.id ) ) {
+                                       this.removeAttribute( "id" );
+                               }
+                       });
                this._destroyIcons();
 
                // clean up content panels
                var contents = this.headers.next()
                        .css( "display", "" )
                        .removeAttr( "role" )
-                       .removeClass( "ui-helper-reset ui-widget-content ui-corner-bottom ui-accordion-content ui-accordion-content-active ui-state-disabled" );
+                       .removeAttr( "aria-expanded" )
+                       .removeAttr( "aria-hidden" )
+                       .removeAttr( "aria-labelledby" )
+                       .removeClass( "ui-helper-reset ui-widget-content ui-corner-bottom ui-accordion-content ui-accordion-content-active ui-state-disabled" )
+                       .each(function() {
+                               if ( /^ui-accordion/.test( this.id ) ) {
+                                       this.removeAttribute( "id" );
+                               }
+                       });
                if ( this.options.heightStyle !== "content" ) {
                        this.element.css( "height", this.originalHeight );
                        contents.css( "height", "" );
@@ -208,6 +247,13 @@ $.widget( "ui.accordion", {
                        case keyCode.SPACE:
                        case keyCode.ENTER:
                                this._eventHandler( event );
+                               break;
+                       case keyCode.HOME:
+                               toFocus = this.headers[ 0 ];
+                               break;
+                       case keyCode.END:
+                               toFocus = this.headers[ length - 1 ];
+                               break;
                }
 
                if ( toFocus ) {
@@ -218,6 +264,12 @@ $.widget( "ui.accordion", {
                }
        },
 
+       _panelKeyDown : function( event ) {
+               if ( event.keyCode === $.ui.keyCode.UP && event.ctrlKey ) {
+                       $( event.currentTarget ).prev().focus();
+               }
+       },
+
        refresh: function() {
                var heightStyle = this.options.heightStyle,
                        parent = this.element.parent(),
@@ -305,6 +357,9 @@ $.widget( "ui.accordion", {
                        });
                }
                this._bind( this.headers, events );
+               this._bind( this.headers.next(), {
+                       keydown: "_panelKeyDown"
+               });
        },
 
        _eventHandler: function( event ) {
@@ -382,21 +437,26 @@ $.widget( "ui.accordion", {
                        this._toggleComplete( data );
                }
 
-               // TODO assert that the blur and focus triggers are really necessary, remove otherwise
-               toHide.prev()
+               toHide
                        .attr({
                                "aria-expanded": "false",
-                               "aria-selected": "false",
-                               tabIndex: -1
+                               "aria-hidden": "true"
                        })
-                       .blur();
-               toShow.prev()
+                       .prev()
+                               .attr({
+                                       "aria-selected": "false",
+                                       tabIndex: -1
+                               });
+               toShow
                        .attr({
                                "aria-expanded": "true",
-                               "aria-selected": "true",
-                               tabIndex: 0
+                               "aria-hidden": "false"
                        })
-                       .focus();
+                       .prev()
+                               .attr({
+                                       "aria-selected": "true",
+                                       tabIndex: 0
+                               });
        },
 
        _animate: function( toShow, toHide, data ) {