Ticket #5210: button_a11y_enhancements.txt

File button_a11y_enhancements.txt, 8.8 KB (added by hhillen, 10 years ago)
Line 
1### Eclipse Workspace Patch 1.0
2#P jQuery
3Index: demos/button/radio.html
4===================================================================
5--- demos/button/radio.html     (revision 3816)
6+++ demos/button/radio.html     (working copy)
7@@ -19,13 +19,13 @@
8 </head>
9 <body>
10 
11-<div class="demo">
12+<div class="demo" role="application">
13 
14-       <form>
15-               <div id="radio1">
16+       <form id="radio1">
17+               <div  title="test options">
18                        <input type="radio" id="radio1" name="radio" /><label for="radio1">Choice 1</label>
19-                       <input type="radio" id="radio2" name="radio" checked="checked" /><label for="radio2">Choice 2</label>
20-                       <input type="radio" id="radio3" name="radio" /><label for="radio3">Choice 3</label>
21+                       <input type="radio" id="radio2" name="radio" /><label for="radio2">Choice 2</label>
22+                       <input type="radio" id="radio3" name="radio" checked="checked" /><label for="radio3">Choice 3</label>
23                </div>
24        </form>
25 
26Index: demos/button/toolbar.html
27===================================================================
28--- demos/button/toolbar.html   (revision 3816)
29+++ demos/button/toolbar.html   (working copy)
30@@ -85,7 +85,7 @@
31 </head>
32 <body>
33 
34-<div class="demo">
35+<div class="demo" role="application">
36 
37        <span id="toolbar" class="ui-widget-header ui-corner-all">
38                <button id="beginning">go to beginning</button>
39@@ -97,7 +97,7 @@
40               
41                <input type="checkbox" id="shuffle" /><label for="shuffle">Shuffle</label>
42               
43-               <span id="repeat">
44+               <span id="repeat" title="Repeat Options">
45                        <input type="radio" id="repeat0" name="repeat" checked="checked" /><label for="repeat0">No Repeat</label>
46                        <input type="radio" id="repeat1" name="repeat" /><label for="repeat1">Once</label>
47                        <input type="radio" id="repeatall" name="repeat" /><label for="repeatall">All</label>
48Index: ui/jquery.ui.button.js
49===================================================================
50--- ui/jquery.ui.button.js      (revision 3828)
51+++ ui/jquery.ui.button.js      (working copy)
52@@ -43,7 +43,7 @@
53 
54                this.buttonElement
55                        .addClass( baseClasses )
56-                       .attr( "role", "button" )
57+                       .attr( "role", this.type === "radio" ? "radio" : "button" )
58                        .bind( "mouseenter.button", function() {
59                                if ( options.disabled ) {
60                                        return;
61@@ -73,36 +73,60 @@
62                                        return;
63                                }
64                                $( this ).toggleClass( "ui-state-active" );
65+                               //this doesn't fire on the actual label, so we'll have to manually toggle the original checkbox first
66+                               self.element[0].checked = !self.element[0].checked;
67                                self.buttonElement.attr( "aria-pressed", self.element[0].checked );
68+                       })
69+                       .bind( "keydown.button", function(event) {
70+                               if ( event.keyCode == $.ui.keyCode.SPACE || event.keyCode == $.ui.keyCode.ENTER ) {
71+                                   self.buttonElement.click();
72+                               }
73                        });
74                } else if ( this.type === "radio") {
75-                       this.buttonElement.bind( "click.button", function() {
76+                       this.buttonElement.bind("keyup.button", function(event) {
77+                               var forward, radios , currentIndex, nextIndex;
78+                               switch(event.keyCode) {
79+                                       case $.ui.keyCode.DOWN:
80+                                       case $.ui.keyCode.RIGHT:
81+                                       case $.ui.keyCode.UP:
82+                                       case $.ui.keyCode.LEFT:
83+                                               forward = event.keyCode == $.ui.keyCode.RIGHT ||
84+                                                       event.keyCode == $.ui.keyCode.DOWN;
85+                                               radios = self._getRadiosInGroup(true);
86+                                               if (!radios)
87+                                                       return;
88+                                               currentIndex = radios.index(self.buttonElement);
89+                                               nextIndex = forward ? currentIndex + 1 : currentIndex -1;
90+                                               nextIndex = nextIndex >= radios.length ? currentIndex :
91+                                                       nextIndex < 0 ? 0 : nextIndex;
92+                                               if (radios.eq(nextIndex).length == 0)                                                   
93+                                                       return;
94+                                               if (event.ctrlKey) // move focus without selecting
95+                                                       radios.eq(nextIndex).focus();
96+                                               else  {
97+                                                   radios.eq(nextIndex).checked = true;
98+                                                       radios.eq(nextIndex).click();
99+                                                       radios.get(nextIndex).focus();
100+                                               }
101+                                               break;
102+                                       case $.ui.keyCode.SPACE: // select focused but unselected radiobutton
103+                                               self.buttonElement.click();
104+                                               break;
105+                               }
106+                       })
107+                       .bind( "click.button", function() {
108                                if ( options.disabled ) {
109                                        return;
110                                }
111                                $( this ).addClass( "ui-state-active" );
112-                               self.buttonElement.attr( "aria-pressed", true );
113+                               self.buttonElement.attr( "aria-checked", true ).attr( "tabindex", "0" );
114 
115-                               var radio = self.element[ 0 ],
116-                                       name = radio.name,
117-                                       form = radio.form,
118-                                       radios;
119-                               if ( name ) {
120-                                       if ( form ) {
121-                                               radios = $( form ).find( "[name=" + name + "]" );
122-                                       } else {
123-                                               radios = $( "[name=" + name + "]", radio.ownerDocument )
124-                                                       .filter(function() {
125-                                                               return !this.form;
126-                                                       });
127-                                       }
128+                               var radios = self._getRadiosInGroup(false);
129+                               if (radios) {
130                                        radios
131-                                               .not( radio )
132-                                               .map(function() {
133-                                                       return $( this ).button( "widget" )[ 0 ];
134-                                               })
135                                                .removeClass( "ui-state-active" )
136-                                               .attr( "aria-pressed", false );
137+                                               .attr( "tabindex", "-1" )
138+                                               .attr( "aria-checked", false );
139                                }
140                        });
141                } else {
142@@ -154,18 +178,48 @@
143                                        : "button";
144 
145                if ( this.type === "checkbox" || this.type === "radio" ) {
146-                       this.buttonElement = $( "[for=" + this.element.attr("id") + "]" );
147+                       this.buttonElement = $( "[for=" + this.element.attr("id") + "]" ).
148+                               wrapInner("<span />").find(":first-child").first();
149+                       // Because otherwise radiobuttons will be announced as "1 of 1":
150+                       this.buttonElement.parent().attr('role', 'presentation');
151                        this.element.hide();
152 
153                        var checked = this.element.is( ":checked" );
154                        if ( checked ) {
155                                this.buttonElement.addClass( "ui-state-active" );
156                        }
157-                       this.buttonElement.attr( "aria-pressed", checked );
158+                       this.buttonElement.attr( this.type === "radio" ? "aria-checked" : "aria-pressed", checked );
159+                       if (this.type === "radio" || this.type === "checkbox") {
160+                               // unchecked radio buttons will be taken out of the tab order later by the buttonset widget 
161+                               this.buttonElement.attr( "tabindex", "0");
162+                       }
163                } else {
164                        this.buttonElement = this.element;
165                }
166        },
167+       
168+       _getRadiosInGroup : function(includeSelf) {
169+               var radio = this.element[ 0 ],
170+                       name = radio.name,
171+                       form = radio.form,
172+                       radios;
173+               if ( name ) {
174+                       if ( form ) {
175+                               radios = $( form ).find( "[name=" + name + "]" );
176+                       } else {
177+                               radios = $( "[name=" + name + "]", radio.ownerDocument )
178+                                       .filter(function() {
179+                                               return !this.form;
180+                                       });
181+                       }
182+                       if (!includeSelf)
183+                               radios = radios.not( this.element[ 0 ] );
184+                       radios = radios.map(function() {
185+                               return $( this ).button( "widget" )[ 0 ];
186+                       });
187+               }
188+               return radios;
189+       },
190 
191        widget: function() {
192                return this.buttonElement;
193@@ -175,14 +229,15 @@
194                this.buttonElement
195                        .removeClass( baseClasses + " " + otherClasses )
196                        .removeAttr( "role" )
197-                       .removeAttr( "aria-pressed" )
198+                       .removeAttr( this.type === "radio" ? "aria-checked" : "aria-pressed" )
199                        .html( this.buttonElement.find(".ui-button-text").html() );
200 
201                if ( !this.hasTitle ) {
202                        this.buttonElement.removeAttr( "title" );
203                }
204-
205+       
206                if ( this.type === "checkbox" || this.type === "radio" ) {
207+                       this.buttonElement.parent().html(this.buttonElement.html()).removeAttr("role");
208                        this.element.show();
209                }
210 
211@@ -210,7 +265,7 @@
212 
213                var icons = this.options.icons,
214                        multipleIcons = icons.primary && icons.secondary;
215-               if ( icons.primary || icons.secondary ) {
216+               if ( !jQuery.ui.highContrastMode() && ( icons.primary || icons.secondary ) ) {
217                        buttonElement.addClass( "ui-button-text-icon" +
218                                ( multipleIcons ? "s" : "" ) );
219                        if ( icons.primary ) {
220@@ -249,6 +304,29 @@
221                                        .addClass( "ui-corner-right" )
222                                .end()
223                        .end();
224+               var radioNames = [], self = this, parent;
225+               //we need to ensure at least one radiobutton in each group is in the tab order
226+               this.element.find(":radio[name]").map(function(){
227+                       name = $(this).attr('name');
228+                       if (jQuery.inArray(name, radioNames) == -1) {
229+                               radioNames.push(name);
230+                               return name;
231+                       }
232+                       return null;
233+               }).each(function(i,name) {
234+                       radios = self.element.find("[name="+ name +"]");
235+                       // if a radiobutton is checked, it is in the tab order. If none are checked, the first is in the tab order
236+                       radios = radios.filter("[checked]").length > 0 ? radios.not("[checked]") : radios.filter(":gt(0)"); 
237+                       radios.each(function(i, e){
238+                               // all radiobuttons initially have tabindex="0", here we remove the unnecessary tab stops
239+                               $( e ).button( "widget" ).attr("tabindex", "-1");
240+                               // radio buttons should be wrapped in an element representing the radiogroup,
241+                               // with a title, aria-label, or aria-labelledby attribute naming the group
242+                               parent = $( e ).parent();
243+                               if ( parent[0] !== self.element && !parent.is("[role='radiogroup']"))
244+                                       parent.attr('role', 'radiogroup');
245+                       })
246+               });
247        },
248 
249        _setOption: function( key, value ) {
250@@ -264,7 +342,7 @@
251                this.buttons
252                        .button( "destroy" )
253                        .removeClass( "ui-corner-left ui-corner-right" );
254-
255+               this.element.find("[role=radiogroup]").removeAttr("role");
256                $.Widget.prototype.destroy.call( this );
257        }
258 });