Fix for Select Elements with Inaccessible Onchange Events
BACKGROUND
Onchange Event Handler Accessibility Issues
Many tools in Sakai 2 make use of onchange event handlers on select elements (drop-down lists/menus) to automatically process the change of a setting from a drop down menu without requiring the user to press a separate submit button. The result for mouse users is a more efficient and responsive UI interaction and it simplifies the design of the controls layout. Unfortunately it can lead to some accessibility issues for users who are keyboard users or users of adaptive technology that emulate keyboard interactions. Certain browsers (IE is one) will fire the onchange event when the up/down cursor keys are used to scroll to the next/previous option. If the onchange event handler changes focus away from the control the user will be prevented from exploring all of the options in the select menu (they'll probably only be able to access options one off from the default selected value).
In IE there are key combinations which allow exploration of the options without triggering the onchange event (using Alt+Down Arrow to open the list). Unfortunately many users don't know this key combination or are not in the habit of using it. Some alternate input adaptive technologies don't support the Alt+Down arrow key combination.
This issue relates to WCAG 2.0 Success Criterion 3.2.2:
3.2.2 On Input: Changing the setting of any user interface component does not automatically cause a change of context unless the user has been advised of the behavior before using the component. (Level A) - http://www.w3.org/TR/2008/REC-WCAG20-20081211/#consistent-behavior-unpredictable-change
WCAG 2.0 F37 goes further to explain this issue:
"Failure of Success Criterion 3.2.2 due to launching a new window without prior warning when the status of a radio button, check box or select list is changed" - http://www.w3.org/TR/2008/NOTE-WCAG20-TECHS-20081211/F37
The University of Illinois at Urbana/Champaign Campus Information Technologies HTML/XHTML Accessibility Best Practices discusses onChange event accessibility issues more practically here: http://cita.disability.uiuc.edu/html-best-practices/auto/onchange.php
Accessibility Issues With the Use of blur() in Onchange Event Handlers
Unfortunately there is a programming practice that causes a very frustrating keyboard usability issue even in browsers where the interactions that trigger the select element's onchange handler are more accessible (even if the user knows the Alt+Down Arrow key combination). Many select elements in Sakai have blur() in their onchange event handler. The result is that as soon as the selected item is changed, focus leaves the select control. What happens to the focus is browser dependent. In some focus goes to the parent element; in others no element is given focus and the next tab starts over at the front of the tab order or picks back up with the next element in the tab order.
onchange="blur()" was a recommended error proofing / usability fix for mouse users who use the scroll wheel to scroll a Web page. In older browsers, if the select element had focus spinning the scroll wheel would change the setting. By using blur() in the onchange handler, once the select menu option was set, focus is immediately shifted off of the element preventing the scroll wheel from accidentally changing the value when the user attempted to use the wheel to scroll the page.
Unfortunately this fix makes it very frustrating for a keyboard user to fill out forms as for each select element they change the setting of forces them to start navigating the page over again. This is very confusing for blind users who won't immediately realize that the focus has changed inappropriately.
This use of blur() in onchange event handlers could be considered a failure to meet WCAG 2.0 SC 2.4.3:
2.4.3: Focus Order: If a Web page can be navigated sequentially and the navigation sequences affect meaning or operation, focusable components receive focus in an order that preserves meaning and operability. (Level A) - http://www.w3.org/TR/2008/REC-WCAG20-20081211/#navigation-mechanisms-focus-order
Summary of Browser Behavior
OS |
Browser |
Default select+onchange behavior is accessible |
Hot Key(s) that Open Menu |
Mousewheel Changes Setting |
Next Tab After Blur() Focus Behavior |
---|---|---|---|---|---|
OS X |
Firefox 3.6.8 |
Yes |
Option+Up, Option+Down |
No |
Picks up after last tabbed to control, or starts over if tab wasn't previously used |
OS X |
Safari 5.0 |
Yes |
Up / Down keys |
No |
Focus Order Starts Over |
XP |
IE 6 |
No |
Alt+Up, Alt+Down |
Yes |
Focus Order Starts Over |
XP |
IE 7 |
No |
Alt+Up, Alt+Down |
Yes |
Focus Order Starts Over |
XP |
IE 8 |
No |
Alt+Up, Alt+Down |
Yes |
Focus Order Starts Over |
Win7 |
IE 9 Preview 3 |
No |
Alt+Up, Alt+Down |
Yes |
Focus Order Starts Over |
XP |
Firefox 2.0 |
Yes |
Alt+Up, Alt+Down |
No |
Back to Same Control |
XP |
Firefox 3.0 |
Yes |
Alt+Up, Alt+Down |
No |
Back to Same Control |
XP |
Firefox 3.5 |
Yes |
Alt+Up, Alt+Down |
No |
Back to Same Control |
XP |
Firefox 3.6.8 |
Yes |
Alt+Up, Alt+Down |
No |
Next Control in Tab Order |
XP |
Opera 10.5 |
No - Any change submits entire form |
Alt+Down |
No |
Focus Order Starts Over |
XP |
Opera 10.6 |
No |
Alt+Down |
No |
Focus Order Starts Over |
Existing Fix
In some tools, there is hidden text that screen reader users can read located just before the select element that reminds them to open the select menu using Alt+Down. The problem with this is that the message is unavailable to sighted non-mouse users.
Possible Fixes
- Remove blur() from onchange events and replace it with mousewheel="return false;" to disable the mousewheel on select elements in IE. This would require simple changes to most tools. This would improve the situation for users using browsers other than IE.
- Use JavaScript to attach an interaction handler to select elements that makes them as accessible as possible by blocking onchange events that fire when arrow keys are pressed and attempts to filter out onchange events that contain simply "blur()". The interaction handler (code follows) Brian Richwine worked up isn't just a few lines due to the need to handle many keyboard/mouse IU interaction possibilities. (The following code is attached to this page.)
Testing a11ySelectHandler.js
Browsers
- XP: IE 6, IE 7, IE 8
- Win7: IE 8, IE 9 (Preview 3)
Test Suite
A zip file containing a11ySelectHandler.js, jQuery, and an HTML file that loads it is attached. Please see select.a11y.zip
a11ySelectHandler.js
/********************************************************************************** * $URL: $ * $Id: $ *********************************************************************************** * * Copyright (c) 2010 The Sakai Foundation * * Licensed under the Educational Community License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.osedu.org/licenses/ECL-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * *********************************************************************************/ /* * a11ySelectHandler.js -- Brian Richwine, Indiana University * Version 1.0, 2010.October.15 * Enhanced version of code by Cameron Adams from 19.October.2004 as found on at: * http://themaninblue.com/writing/perspective/2004/10/19/ * * Interaction manager for select elements with onchange event handlers that * attempts to ensure their accessibility for keyboard only users using IE. * In Internet Explorer, the onchange event is fired each time the user * uses the up/down arrow keys to scroll through the various options of a * select element. When the select element includes an onchange event handler * that changes the context (i.e.: submits a form, etc.), then the keyboard user * is prevented from exploring all of the options. * * This handler models its user interaction behavior after Mozilla Firefox * 3.6 on the PC. * * Call with the following code from the bottom of the HTML body: * <!--[if IE]> * <script type="text/javascript" defer="defer" src="/library/js/a11ySelectHandler.js"></script> * <![endif]--> * Notes: * - Be sure to load jQuery first! * - This routine is needed only for Internet Explorer. Other supported web browsers (Safari, * Chrome, and Firefox) handle keyboard interactions with select elements in an accessible manner. * - This code adds an "a11ySelect" object to each select element that has an onchange event handler. * - This code assumes existing onchange events are all DOM Level 0 or 1 (only one event handler allowed) * DOM Level 0 events are assigned in the HTML like this: * onchange="alert('hello!');" * DOM Level 1 events are assigned like this: * document.getElementById('selectMenu1').onchange = function() { * alert('Hello!'); * } * * This code has not been tested where DOM Level 2 events are handling the select element's onchange event. * DOM Level 2 events are assigned like this: * document.getElementById('myButton').addEventListener( 'click', function(){ * alert('Hello!'); * }, false); * * * This code and the results of interactions testing performed on 11.August.2010 are * documented on the Sakai Accessibility Working Group confluence space. * * Tested against: * XP: IE 6, IE 7, IE 8, * Win7: IE 9 (Preview 3) */ (function ($) { if ($.browser.msie) { $(document).ready(function () { // Scan document and install handler once document is loaded and ready $("select").each(function (index, objElement) { // "select[onchange]" didn't work, is Sakai using an old version of jQuery? if (objElement && objElement.onchange) { objElement.a11ySelect = { allowOnchangeEvent : false, enterKeyWasPressed : false, initValue : null, lastInteractionWasAClick : true, allowPossibleOpenMenuChange : false, oSelect : null, origOnfocus : null, origOnchange : null, origOnkeydown : null, origOnclick : null, origOnblur : null, handleChanged : function (ob) { var oSelect = (ob && ob.a11ySelect) ? ob : this, a11ySelect = oSelect.a11ySelect; if (!a11ySelect.allowOnchangeEvent && !a11ySelect.allowPossibleOpenMenuChange) { return false; // Abort calling the original onchange event handler } a11ySelect.allowOnchangeEvent = false; // Prevent redundant calls to orig onchange event a11ySelect.initValue = a11ySelect.oSelect.value; // Store current value as init since calling onchange if (a11ySelect.origOnchange) { return a11ySelect.origOnchange(ob); } return false; }, handleClick : function (e) { // Handles clicks on menus opened by keyboard (alt+down arrow) if (this.value !== this.a11ySelect.initValue && !this.a11ySelect.lastInteractionWasAClick) { this.a11ySelect.allowOnchangeEvent = true; this.a11ySelect.handleChanged(this); } this.a11ySelect.lastInteractionWasAClick = true; this.a11ySelect.allowOnchangeEvent = true; this.a11ySelect.enterKeyWasPressed = false; this.a11ySelect.allowPossibleOpenMenuChange = false; if (this.a11ySelect.origOnclick) { return this.a11ySelect.origOnclick(e); } }, handleFocus : function (e) { // Reset everything... this.a11ySelect.initValue = this.value; this.a11ySelect.allowOnchangeEvent = false; this.a11ySelect.enterKeyWasPressed = false; this.a11ySelect.lastInteractionWasAClick = true; this.a11ySelect.allowPossibleOpenMenuChange = false; if (this.a11ySelect.origOnfocus) { return this.a11ySelect.origOnfocus(e); } return true; }, handleBlur : function (e) { // Call orig onChange event when focus is lost if needed if (this.value !== this.a11ySelect.initValue && !this.a11ySelect.enterKeyWasPressed) { this.a11ySelect.allowOnchangeEvent = true; this.a11ySelect.handleChanged(this); } if (this.a11ySelect.origOnblur) { return this.a11ySelect.origOnblur(e); } return true; }, handleKeydown : function (e) { var evt = (e) ? e : window.event, keyCodeEnter = 13, keyCodeEsc = 27; this.a11ySelect.lastInteractionWasAClick = false; this.a11ySelect.allowPossibleOpenMenuChange = false; if (evt.keyCode === keyCodeEnter && this.value !== this.a11ySelect.initValue) { this.a11ySelect.allowOnchangeEvent = true; this.a11ySelect.enterKeyWasPressed = true; this.a11ySelect.handleChanged(this); } else if (evt.keyCode === keyCodeEnter) { // Expanded select menus (alt+down arrow key or by click) don't update value until after keypress event // This allows menus opened by a mouse, but set with an enter key to work this.a11ySelect.allowPossibleOpenMenuChange = true; this.a11ySelect.enterKeyWasPressed = true; } else { this.a11ySelect.enterKeyWasPressed = false; } if (evt.keyCode === keyCodeEsc) { // restore original value when escape is pressed this.value = this.a11ySelect.initValue; } this.a11ySelect.allowOnchangeEvent = false; return true; }, init : function (oSel) { // Keep a reference to the select element object oSel.a11ySelect.oSelect = oSel; oSel.a11ySelect.initValue = oSel.value; oSel.a11ySelect.allowOnchangeEvent = false; oSel.a11ySelect.enterKeyWasPressed = false; oSel.a11ySelect.allowPossibleOpenMenuChange = false; // Store original handlers oSel.a11ySelect.origOnfocus = oSel.onfocus; oSel.a11ySelect.origOnkeydown = oSel.onkeydown; oSel.a11ySelect.origOnclick = oSel.onclick; oSel.a11ySelect.origOnchange = oSel.onchange; oSel.a11ySelect.origOnblur = oSel.onblur; // Install handlers oSel.onfocus = oSel.a11ySelect.handleFocus; oSel.onkeydown = oSel.a11ySelect.handleKeydown; oSel.onclick = oSel.a11ySelect.handleClick; oSel.onchange = oSel.a11ySelect.handleChanged; oSel.onblur = oSel.a11ySelect.handleBlur; $(oSel).css("outline", "1px dotted red"); } }; objElement.a11ySelect.init(objElement); } }); }); } })(jQuery);