Keyboard Access APIs

  • 1,008 views
Uploaded on

This presentation covers the fundamentals APIS necessary to provide keyboard access for custom widgets built using JavaScript.

This presentation covers the fundamentals APIS necessary to provide keyboard access for custom widgets built using JavaScript.

  • Full Name Full Name Comment goes here.
    Are you sure you want to
    Your message goes here
  • great article
    Are you sure you want to
    Your message goes here
No Downloads

Views

Total Views
1,008
On Slideshare
0
From Embeds
0
Number of Embeds
0

Actions

Shares
Downloads
15
Comments
1
Likes
1

Embeds 0

No embeds

Report content

Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel
    No notes for slide
  • \n
  • \n
  • First things first. If you are on a Mac, you’ll want to configure it for full keyboard access.\n
  • Use Ctrl + F7 to enable keyboard access for all controls across OS X. This will also enable full keyboard access for Firefox on the Mac.\n
  • Personally, I also like my function keys to operate as standard function keys. To set this go to System Preferences > Keyboard\n
  • In Safari go to Preferences > Advanced and check “Press Tab to highlight each item on a webpage”\n
  • Chrome has a similar preference in “Under the Hood”\n
  • Before we talk about providing keyboard access, let’s begin by understanding who benefits from good keyboard access.\n
  • Users who prefer the keyboard. One example would be software engineers, but many users realize that keyboard shortcuts can greatly improve their efficiency.\n
  • Users who are blind completely rely on the keyboard both to navigate as well as to enter information.\n
  • Users with physical disabilities may not be able to type with a physical keyboard, but can use voice recognition software to speak keyboard shortcuts.\n
  • Similarly, users with physical disabilities can use software keyboards to press keyboard shortcuts.\n
  • \n
  • The more I have studied keyboard shortcuts for widgets, I find widgets fall into three categories. Throughout this presentation we’ll talk about these patterns and how to implement them.\n
  • The first API essential to keyboard access is the tabIndex attribute. tabIndex can be used to make any element in the DOM focusable, and controls how it can be focused. If an element isn’t focusable it can’t fire key-related events.\n
  • Another reason focusability is important for widgets: Screen readers read the currently focused element. So, if a widget isn’t focusable, it will be less discoverable and very likely inoperable to users of screen readers.\n
  • HTML provides a limited set of natively focusable elements—mostly form controls, buttons and links.\n
  • Using tabIndex, any element in the DOM can be focusable and fire key events. tabIndex can be set to one of three values: -1, 0 and an explicit positive index.\n\nA tabIndex of -1 enables an element to be focusable via JavaScript or the mouse. Elements with a tabIndex of -1 are not in the tab flow.\n\nA tabIndex of 0 places the element in the default tab flow; the element is also focusable via JavaScript or the mouse.\n\nLastly, tabIndex can be set to an explicit value.\n
  • \n
  • \n
  • tabIndex can be set declaratively in the markup, or programmatically using JavaScript.\n
  • \n
  • One simple use case for tabIndex 0 is to make custom buttons focusable via the keyboard. JavaScript can be used to enable users to click the button by pressing either the Spacebar or Enter key.\n
  • \n
  • \n
  • \n
  • \n
  • Popup menus are a good use case for tabIndex=-1 because they aren’t in the tab flow but need to be focusable.\n\nHere’s how to set tabIndex on the various elements that compose a popup menu.\n
  • One use for explicitly declaring tabIndex would be to balance visual and functional requirements. For example, in GMail the “Send” button appears before the To, Subject and message body fields. However, when composing an email you’d likely want to tab first to the To field, then Subject, then the message body, and then finally the Send button.\n
  • \n
  • The second piece of API are the focus() and blur() methods.\n
  • \n
  • Stateless containers: widgets like menubars and toolbars. Both manage a collection of like descendant controls, and both maintain a visual focus/selection as the user is navigating. However that selection is not maintained after the control loses focus.\n
  • On the desktop menubar and toolbars are often not in the tab flow; the user moves focus to them via a keyboard shortcut.\n\nOn the web, these controls ARE generally in the tab flow for two reasons:\n\n1) Discoverability—expectation would be that you can use Tab or Shift + Tab to move focus to a control\n\n2) Implementing a keyboard shortcut to move focus to a toolbar might use up a shortcut you need for another user action\n
  • \n
  • Consider a toolbar. When you first move focus to it, the first button is selected.\n
  • As you navigate through the toolbar the currently focused button is highlighted.\n
  • When the toolbar loses focus, the visualization of focus is cleared.\n
  • When the toolbar is re-focused, the user does not resume where they left off, but rather starts back at the beginning.\n
  • To implement this functionality, use tabIndex to remove all but the first button from the tab flow. This will allow the user to use the Tab key to move focus to/from the toolbar in a single keystroke.\n
  • Then, bind a delegated keydown event listener that moves focus to the next or previous button in the toolbar as the user presses the left or right arrow keys.\n
  • \n
  • \n
  • Stateful containers: these are widgets like treeviews, tablists, or listboxes. These widgets manage a collection of like descendant controls, and maintain selection even when they have lost focus.\n
  • Consider a listbox control. The initial state is no selection.\n
  • When a stateful container control initially receives focus, it has no selection, but focus is often indicated by an outline or change of color on the root element. Sometimes an outline around the first item, indicating it is focused but not selected.\n
  • When the user presses the up and down arrow keys, the selection is advanced.\n
  • When the control loses focus, the current selection is maintained, but often grayed out to indicate that it is not active.\n
  • If the control regains focus, the up and down arrow keys should allow the user to pickup where they last left off.\n
  • To implement this pattern, begin by setting the tabIndex of the first element to 0, the rest to -1.\n
  • As the user presses the arrow keys to make a selection, update the tabIndex of the currently selected item to 0 and set the tabIndex of the previously selected item to -1. This will allow there to be a functional representation of the selection; if the control regains focus, the focused item will be the last selected item—allowing the user to pickup where they left off.\n
  • This technique is referred to as the “Roving TabIndex Technique” in the W3C’s ARIA Authoring Practices specification.\n
  • \n
  • \n
  • \n
  • \n
  • When a menu opens, focus should be moved to the menu. It has no initial selection.\n
  • On initial press of the up or down arrow keys, focus is moved into the menu. If the user pressed the down arrow, the first menuitem is selected. If the user pressed the up arrow, the last menuitem is selected.\n
  • On subsequent key press, selection follows the arrow keys.\n
  • Implementing this behavior is easy. The menu container and all of the elements used to create menuitems are given a tabIndex of -1 to make them focusable.\n
  • Focus is set to the menu’s container when the menu is made visible.\n
  • On initial up/down arrow key press, use the :last-child and :first-child selectors are used to select either the first or last menuitem, depending on the key that was pressed.\n
  • On subsequent arrow key presses, just advance the selection using a class name, and then focus the next menuitem.\n
  • \n
  • When a dialog is made visible, focus should be moved into the dialog—to control that represents the next action the user is likely to want to take. This saves keyboard users the work of having to use the Tab key to move focus into the dialog.\n
  • For modal dialogs, modality should be enforced for both the keyboard and the mouse; the user shouldn’t be able to move focus outside of the dialog using Tab or Shift + Tab.\n
  • If the user cancels the dialog (by pressing the Esc key, the close, or Cancel button), focus should be sent back to the element in the DOM that had focus prior to the dialog’s display. This helps keyboard users pickup where they left off.\n
  • Implementing this behavior is easy. When the dialog is made visible, use focus() to move focus to the desired control inside the dialog.\n
  • What about the other behaviors? Like enforcing modality for the keyboard? And, if the dialog is cancelled, moving focus back to the element in the DOM that had focus prior to the dialog’s display?\n\nFor these behaviors we’ll use three other APIs: document.activeElement, the contains() method and the focus and blur events.\n
  • \n
  • \n
  • \n
  • activeElement returns a reference to the currently focused DOM element.\n
  • Use activeElement to capture a reference to the element that had focus prior to a dialog’s display. In this example, the showDialog() function passes the activeElement as a reference to the event listener used to hide the dialog.\n
  • \n
  • This makes it really easy for the event listener to return focus back to the element that had focus prior to the dialog’s display.\n
  • Both jQuery and YUI provide a contains() method—an easy means of determining if one DOM element contains another. This is useful for enforcing modality.\n
  • jQuery and YUI also provide ways to listen for delegated focus and blur events. These are useful as standard DOM focus and blur events do not bubble.\n\nIn jQuery you can use event delegation with the “focus” and “blur” events either by specifying “focusin” and “focusout”, or by using on() with either “focus” and “blur” and providing a delegation selector.\n
  • Putting the two together: To enforce modality, simply bind a document-level, delegated focus event listener. That listener will then use contains() to determine if focus has left the dialog, and restore focus to the dialog if it does lose focus.\n
  • A similar technique can be used to hide popup menus if focus is moved outside of the menu.\n
  • The final API essential for keyboard access is the :focus pseudo class.\n
  • \n
  • Unfortunately IE 6 and 7 do not support the :focus pseudo class.\n
  • \n
  • It easy to supplement the lack of support for the :focus pseudo class in IE using JavaScript.\n
  • Setting the CSS "outline" property to "none" is the primary reason that focus isn't visible in many web sites and applications.\n
  • As it turns out, there is actually a website dedicated being mindful of not setting the CSS "outline" property to "none".\n
  • If you are looking to provide custom focus styles for your site or application, I would recommend not globally removing the browser’s default outline because it is likely there are going to be instances now, and especially later, where you might forget to define a focus style—leaving the user without a visualization of focus.\n\nRather than globally removing the browser’s focus outline, selectively remove it for instances where you are providing custom styles. This will help ensure you have some visualization of focus.\n
  • All of these CSS properties are useful for styling focus as they don't affect box size and therefore the content around the focused element won't reflow.\n
  • Custom focus styling is not possible in IE < 8. But IE does provide a default focus style. In Yahoo! Mail, custom focus styles are considered a Progressive Enhancement.\n
  • In addition to the :focus pseudo class, there is also :active.\n
  • \n
  • Practically speaking the :focus pseudo class is most useful when it comes to styling focus for atomic controls like buttons or links. For all other widgets, :focus isn’t enough as we’ll see in the follow examples.\n
  • For example, consider popup menus and listboxes.\n
  • A popup menu needs to be able to move selection in response to both the keyboard and the mouse. As the user switches between either input mechanism, the selection needs to be able to stay in sync.\n\nThere are two reasons we cannot rely on :focus and :hover:\n\n1) Lack of reliable support in older IE versions.\n\n2) Even if old IE support isn’t an issue, if we were to rely on :focus and :hover alone the user might see two menuitems selected simultaneously if they start with one device (mouse), then move to anounce (keyboard).\n
  • For this reason, using a class is more robust than relying on :focus and :hover. As the user switches between the keyboard and the mouse, the corresponding event listener can stay in sync with the selection made via the previous input method.\n
  • \n
  • Another example: being able to persist selection when a control has lost focus. For example, the selected item in a listbox should still be rendered even when the control has lost focus.\n
  • The currently selected item is marked with a class of “selected” as the user presses the up and down arrow keys.\n
  • \n
  • contains() is used to determine if the listbox has focus. When the listbox has focus, a class of “focus” is added to the root element.\n
  • With a class of “focus” on the root element, the CSS can be structured so that the selected item in the list is rendered blue when the widget has focus, and gray when it does not.\n
  • \n
  • \n
  • As we’ve seen, implementing keyboard navigation across all of the various widget types involves handling a very few number of keys. \n
  • Working examples of all of the widgets from this presentation are available on github.\n

Transcript

  • 1. Keyboard Access Todd Kloots @todd
  • 2. Goals1. Understand the essential APIs for providing keyboard access for custom widgets2. Understand the common, expected patterns for widgets
  • 3. Configuring the Machttp://yaccessibilityblog.com/library/full-keyboard-access-mac.html
  • 4. OS X Preferences
  • 5. OS X Preferences
  • 6. Safari Preferences
  • 7. Chrome Preferences
  • 8. Who benefits?
  • 9. * Users of screen readers
  • 10. http://youtu.be/ReED-LpciYI
  • 11. http://youtu.be/tdEaeULKkLQ
  • 12. Who BenefitsUsers who prefer/love the keyboardUsers who are blindUsers with physical disabilities
  • 13. Widget Keyboard Patterns Atomic widgets (e.g. buttons, links, text boxes) Container widgets (e.g. listboxes, toolbars, trees) Overlay widgets (e.g. dialogs, menus, tooltips)
  • 14. tabIndex
  • 15. Screen ReadersRead the code (HTML)Read the focused elementWhat is read: Element/Control type Label Properties State changes
  • 16. Focusable Elements<a><area><button><input><select><textarea><object>
  • 17. tabIndex<li tabindex="-1" /><li tabindex="0" /><li tabindex="1" />
  • 18. Natively Focusable Elements<a href="#">Label</a><button type="button">Label</button><input type="button" value="Label">Convey interactivityIn the default tab flowFire click event in response to the mouseand keyboard
  • 19. Using tabIndex<li tabindex="0" />Convey interactivityFocusableCan be in placed in the tab flowFire click event in response to the mouseBUT NOT the keyboard
  • 20. <span class="btn" tabindex="0">OK</span>// Native DOMnode.tabIndex = 0;// jQuerynode.attr("tabIndex", 0);// YUInode.set("tabIndex", 0);
  • 21. tabIndex=0
  • 22. tabIndex=0 Use Case<span class="btn" tabindex="0">OK</span>$(".btn").on("keydown", function (e) { var keyCode = e.keyCode; if (keyCode === 13 || keyCode === 32) { $(this).trigger($.Event("click")); }});
  • 23. tabIndex=0 ReviewPlaces the element in the default tab flowFocusable via keyboard, mouse or JavaScriptDefault browser focus outline onFocusClick not fired via Enter or Spacebar
  • 24. tabIndex=-1
  • 25. tabIndex=-1Removes the element from the tab flowFocusable via the mouse or JavaScriptNo browser focus outline in IEClick not fired via Enter or Spacebar
  • 26. tabIndex=-1 Use CasesPopup MenusStateless Container WidgetsStateful Container Widgets
  • 27. Popup Menu Use Case<ul class="menu hidden" tabindex="-1"> <li tabindex="-1">Inbox</li> <li tabindex="-1">Archive</li> <li tabindex="-1">Trash</li></ul>
  • 28. Explicit tabIndex Use Case 4 1 23
  • 29. tabIndex ReviewtabIndex=0tabIndex=-1tabIndex=1
  • 30. focus() and blur()
  • 31. Use CasesOverlay WidgetsStateless Container WidgetsStateful Container Widgets
  • 32. Stateless Containers
  • 33. Web vs. Desktop
  • 34. Web vs. Desktop
  • 35. Initial Focus
  • 36. Right/Left Arrows
  • 37. Blur
  • 38. Subsequent Focus
  • 39. 0 -1 -1 -1 -1<div class="toolbar"> <button type="button" tabindex="0" class="prnt">Print</button> <button type="button" tabindex="-1" class="find">Find</button> <button type="button" tabindex="-1" class="save">Save</button> <button type="button" tabindex="-1" class="sets">Settings</button> <button type="button" tabindex="-1" class="info">Info</button></div>
  • 40. toolbar.on("keydown", "button", function (e) { var keyCode = e.keyCode, button = $(this), next; if (keyCode === 37) { // Left next = "prev"; } else if (keyCode === 39) { // Right next = "next"; } if (next) { button[next]().focus(); }});
  • 41. Toolbar Exercise
  • 42. Stateless Container PatternUse with the following ARIA widget roles toolbar menubar
  • 43. Stateful Containers
  • 44. Initial State: No Selection
  • 45. Initial Focus
  • 46. Up/Down Arrows
  • 47. Blur
  • 48. Subsequent Focus
  • 49. <ul id="listbox"> <li tabindex="0">Activism</li> <li tabindex="-1">Baking</li> <li tabindex="-1">Cooking</li> <li tabindex="-1">Dancing</li> <li tabindex="-1">Fine Art</li> <li tabindex="-1">Ice Skating</li> <li tabindex="-1">Music</li> <li tabindex="-1">Politics</li> <li tabindex="-1">Sports</li> <li tabindex="-1">Travel</li> <li tabindex="-1">Technology</li></ul>
  • 50. Up/Down Arrowslistbox.on("keydown", "li", function (e) { var keyCode = e.keyCode, li = $(this), next, nextItem; if (keyCode === 38) { // UP next = "prev"; } else if (keyCode === 40) { // DOWN next = "next"; } if (next) { // On initial arrow key press, dont advance focus, // just synchronize focus and selection if (!li.hasClass(selected)) { li.addClass("selected"); } else if ((nextItem = li[next]()) && nextItem[0]) { makeSelection(nextItem); nextItem.focus(); } }});
  • 51. Roving tabIndexfunction makeSelection(li) { listbox.find("li.selected").attr(tabIndex, -1).removeClass("selected"); li.attr(tabIndex, 0).addClass("selected");} http://www.w3.org/TR/2009/WD-wai-aria-practices-20090224/#focus_tabindex
  • 52. Roving tabIndex Demo
  • 53. Roving tabIndex PatternUse with the following ARIA widget roles listbox grid tree & treegrid tablist radiogroup
  • 54. ReviewOne tab stop per widgetTwo patterns Stateless Container Stateful Container / Roving tabIndex
  • 55. Menus
  • 56. Initial Focus: No Selection
  • 57. Initial Up/Down Arrow
  • 58. Up/Down Arrows
  • 59. -1 -1 -1 -1<ul class="menu hidden" tabindex="-1"> <li tabindex="-1">Inbox</li> <li tabindex="-1">Archive</li> <li tabindex="-1">Trash</li></ul>
  • 60. Initial Focusfunction showMenu() { menu.find(".selected").removeClass("selected"); menu.removeClass("hidden"); var position = button.position(); menu.css({ top: (position.top + button.outerHeight()), left: position.left }); menu.focus();}
  • 61. Initial Up/Down Arrowif (target === menu[0]) { if ((selected = menu.find(".selected")) && selected[0]) { event = $.Event("keydown", { keyCode: keyCode }); selected.trigger(event); } else { if (keyCode === 38) { // UP selector = "li:last-child"; } else if (keyCode === 40) { // DOWN selector = "li:first-child"; } menu.find(selector).addClass("selected").focus(); }}
  • 62. Up/Down Arrowsswitch (e.keyCode) { case 38: // UP next = "prev"; break; case 40: // Down next = "next"; break; case 13: // Enter case 32: // Space selected.trigger($.Event("click")); selected.blur(); break;}if (next && (nextItem = li[next]()) && nextItem[0]) { li.removeClass("selected"); nextItem.addClass("selected").focus();}
  • 63. Dialogs
  • 64. Initial Focus
  • 65. Tab or Shift + Tab
  • 66. Cancel
  • 67. Initial Focusfunction showDialog() { var activeEl = document.activeElement; dialog.removeClass("hidden"); createModalMask(); positionDialog(); $(#ok-button).focus();}
  • 68. What About?Modality?Canceling?
  • 69. document.activeElement
  • 70. contains()
  • 71. onFocus & onBlur
  • 72. document.activeElement
  • 73. function showDialog() { var activeEl = document.activeElement; dialog.removeClass("hidden"); createModalMask(); positionDialog(); $(#ok-button).focus(); $(document).on("focusin.dialog", enforceModality); $(window).on(resize.dialog, function () { sizeModalMask(); positionDialog(); }); dialog.on("click.dialog", "input", activeEl, hideDialog); ...}
  • 74. dialog.on("keydown.dialog", function (e) { var keyCode = e.keyCode; if (keyCode === 27) { // Esc hideDialog(activeEl); } else if (keyCode === 9 && !e.shiftKey && e.target === $("#cancel-button")[0]) { e.preventDefault(); }});
  • 75. function hideDialog(activeEl) { dialog.addClass("hidden"); removeModalMask(); dialog.off(".dialog"); $(window).off(".dialog"); activeEl.focus();}
  • 76. contains(haystack, needle)http://api.jquery.com/jQuery.contains/http://yuilibrary.com/yui/docs/api/classes/Node.html
  • 77. $(document).on("focusin", onDocFocus);$(document).on("focusout", onDocFocus);$(dialog).on("focus", "input", onDialogFocus);$(dialog).on("blur", "input", onDialogFocus);
  • 78. function enforceModality(e) { if (!$.contains(dialog[0], e.target)) { dialog.find(#ok-button).focus(); }}$(document).on("focusin.dialog", enforceModality);
  • 79. Hiding Menusmenu.on("focusout", function (e) { setTimeout(function () { if (!$.contains(menu[0], document.activeElement)) { menu.addClass("hidden"); } }, 0);});
  • 80. :focus
  • 81. :focusDevice independentAll browsers implement a defaultfocus outline
  • 82. :focus Limitations in IENot supported in IE < 8IE < 8 does provide :active, but only for linksIE doesnt render default outline whentabIndex is set to -1Cannot remove default outline using CSS inIE < 8
  • 83. Supplementing :focus In IEif (browser.msie && browser.version < 8) { toolbar.on("focus", "button", function () { $(this).addClass("focus"); }); toolbar.on("blur", "button", function () { $(this).removeClass("focus"); });}
  • 84. :focus { outline: none;}
  • 85. Recommendation/* BAD: Dont globally remove the outline property. */:focus { outline: none;}/* GOOD: Instead, selectively remove outline in rules that provide an alternative. */.tab:focus { outline: none; background-color: blue;}
  • 86. Useful PropertiesOutlineBackgroundText colorBox ShadowUnderline
  • 87. Custom Focus in Y! Mail IE 6 and 7 IE 8+
  • 88. :active
  • 89. :activeAlso device independentPrecedes :focuskeydown state/mousedown state
  • 90. :focus Use CasesAtomic controls(e.g. buttons and links)
  • 91. :focus Not Useful With
  • 92. Serving Two Masters KeyboardMouse
  • 93. Initial Up/Down Arrowif (target === menu[0]) { if ((selected = menu.find(".selected")) && selected[0]) { event = $.Event("keydown", { keyCode: keyCode }); selected.trigger(event); } else { if (keyCode === 38) { // UP selector = "li:last-child"; } else if (keyCode === 40) { // DOWN selector = "li:first-child"; } menu.find(selector).addClass("selected").focus(); }}
  • 94. Up/Down Arrowsswitch (e.keyCode) { case 38: // UP next = "prev"; break; case 40: // Down next = "next"; break; case 13: // Enter case 32: // Space selected.trigger($.Event("click")); selected.blur(); break;}if (next && (nextItem = li[next]()) && nextItem[0]) { li.removeClass("selected"); nextItem.addClass("selected").focus();}
  • 95. Maintaining State
  • 96. listbox.on("keydown", "li", function (e) { var keyCode = e.keyCode, li = $(this), next, nextItem; if (keyCode === 38) { // UP next = "prev"; } else if (keyCode === 40) { // DOWN next = "next"; } if (next) { // On initial arrow key press, dont advance focus, // just synchronize focus and selection if (!li.hasClass(aria-selected)) { li.addClass("selected"); } else if ((nextItem = li[next]()) && nextItem[0]) { makeSelection(nextItem); nextItem.focus(); } }});
  • 97. function makeSelection(li) { listbox.find("li.selected").attr(tabIndex, -1).removeClass("selected"); li.attr(tabIndex, 0).addClass("selected");}
  • 98. listbox.on("focusin", function (e) { if (!listbox.hasClass("focus")) { listbox.addClass("focus"); }});listbox.on("focusout", function (e) { setTimeout(function () { if (!$.contains(listbox[0], document.activeElement)) { listbox.removeClass("focus"); } }, 0);});
  • 99. li.selected { background: #ccc;}li:focus { outline: none;}ul.focus { border-color: #3875D7;}ul.focus li.selected { background: #3875D7; color: #fff;}
  • 100. Review
  • 101. APIstabIndexfocus(), blur()document.activeElementcontains()focus and blur events:focus, :active
  • 102. Widget Keyboard PatternsTask KeyFocus Next or Previous Tab / Shift + Tab / ArrowsChange selection →, ←, ↑, ↓Expand / Collapse →, ←, ↑, ↓Exit EscClick SpacebarDefault Action Enter
  • 103. Code Exampleshttps://gist.github.com/kloots