├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── assets ├── css │ ├── --demo--.css │ └── tabs.css └── js │ └── .gitkeep ├── index.html ├── index.js ├── package.json └── tests ├── disabled-tab.html ├── focus-test.html ├── has-toc.html ├── inject-tabs-into-widget.html ├── manual.html ├── multiple-instances.html ├── nested.html ├── tab-panel-wrapper.html ├── vertical-orientation.html └── windows-7-tabs.html /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | ## [Unreleased] 5 | - [Removable `tab`/`tabpanel` functionality](https://github.com/scottaohara/a11y_tab_widget/issues/6). 6 | - [Open a `tabpanel` from URL hash](https://github.com/scottaohara/a11y_tab_widget/issues/8). 7 | - Additional documentation about UX for Tab Widgets. 8 | 9 | ## [2.1.4] - 2021-09-20 10 | ### Removed 11 | Removed function to re-focus active tab, which was originally put in place to work around buggy JAWS behavior. The function was causing VO focus to constantly re-shift to active tab when attempting to swipe through tabs, due to changes in how VO focus works on iOS. 12 | 13 | ## [2.1.3] - 2019-08-06 14 | ### Added 15 | - Open tabpanel from URL hash via [PR 18](https://github.com/scottaohara/a11y_tab_widget/pull/18). 16 | 17 | ## [2.1.2] - 2019-01-17 18 | ### Added 19 | - New `data-atabs-panel-wrap` attribute allows for a wrapping container for the `tabpanel`s. 20 | 21 | ## [2.1.1] - 2018-11-16 22 | ### Fixed 23 | - [Home/End keys don't auto activate `tab`s](https://github.com/scottaohara/a11y_tab_widget/issues/14). If statement added to activate a `tab` when navigating with these keys, unless the Tab Widget is set to manual activation. 24 | 25 | 26 | ## [2.1.0] - 2018-11-15 27 | ### Fixed 28 | - [Finished version documentation](https://github.com/scottaohara/a11y_tab_widget/issues/10) 29 | - [Support for IE11](https://github.com/scottaohara/a11y_tab_widget/issues/9) 30 | - Missing `role=tabpanel` on panels. Oops. 31 | 32 | ### Added 33 | - Updated and added missing documentation for new 2.1 version of this script. 34 | - Various example/test files. 35 | - [Work around for NVDA + JAWS focus bug](https://github.com/scottaohara/a11y_tab_widget/commit/7bda439de03d09c2a472a6ee5d95c2a4b663d679). [NVDA filed bug](https://github.com/nvaccess/nvda/issues/8906), [JAWS filed bug](https://github.com/FreedomScientific/VFO-standards-support). 36 | 37 | ### Changed 38 | - `tabpanel`s can only receive keyboard focus on initial Tab away from the `tablist`. Once the `tabpanel` loses focus, the `tabindex` is removed. `tabpanel`s no longer behave as 'clickable' elements when navigating by touch. 39 | - Up, down, left, and right arrow keys all work to navigate between `tab`s within a `tablist`, regardless of orientation. This is due to the fact that `aria-orientation` has poor support and user feedback all requested this feature. 40 | - Change Delete keycode to "delete" and not "Backspace" key (for future release). 41 | - Generate `span`s instead of `button`s. Add in functionality for Space and Enter keys. This was to attempt to fix strange behavior that didn't actually have anything to do with the `button`s... but since `button`s !== `tab`s, no real reason to change back... 42 | - `aria-controls` is dynamically added to only the active `tab` so that JAWS won't announce key commands to move to an element that is inaccessible, if virtual cursor is on inactive `tab`s. 43 | 44 | ### Removed 45 | - `:focus-visible` and `.focus-visible` classes. Not really necessary after various updates to the script since the previous release. 46 | 47 | 48 | ## [2.0.1] - 2018-09-20 49 | ### Added 50 | - Use `:focus-visible` and `.focus-visible` class in demonstration page. However this polyfill was not added as a script dependency. 51 | ### Fixed 52 | - [Make vertical tablists responsive](https://github.com/scottaohara/a11y_tab_widget/issues/7) update to CSS. 53 | 54 | ## [2.0.0] - 2018-09-19 55 | ### Changed 56 | - Rewrote the entire script from scratch into vanilla JavaScript. 57 | - Revised base markup pattern. 58 | - Revised CSS classes. 59 | - Allow for external elements to be added to a Tab Widget, generating both the new `tabpanel` and associated `tab`. 60 | - Options for non-default `aria-orientation=vertial` and automatic activation of `tab`s on focus. 61 | ### Removed 62 | - Non-ARIA Tab Widget version. 63 | 64 | 65 | ## [1.0.0] - 2015-09-30 to [1.1.1] - 2016-12-19 66 | 1.0.0 to 1.1.1 were a jQuery based version of Tab Widgets. 67 | This series of versions allowed for ARIA vs non-ARIA versions of the Tab Widget. 68 | 69 | This version of the script also called for a different version of the expected baseline markup, and hooks to make the script work. 70 | 71 | 72 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 - 2018 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Accessible Tabbed Interfaces 2 | A script to progressively enhance sectioned content into an accessible tabbed interface. 3 | 4 | 5 | ## How to use 6 | To help facilitate the simplest integration with your code base, the necessary markup has been boiled down to a wrapping element with a `data-atabs` attribute to serve as the Tab Widget container. Additionally, `tab`s, and `tabpanel`s can be designated via different markup patterns to help suit your needs. 7 | 8 | ### Example Setup 9 | ```html 10 |
11 | 12 |
14 | 15 |
16 | 17 | 18 |
19 | 20 | 24 | 25 |
26 | 27 |
28 | 29 | 30 | 39 | ``` 40 | 41 | ### Injecting content into the Tab Widget 42 | Once a new instance of a Tab Widget has been created, the `addTab` function can be called from outside the script. Using this function, you can reference elements in the DOM (by `id`) that are not in the `data-atabs` wrapping element, and move them into the Tab Widget, creating a `tab` and `tabpanel` for the content. This may be useful if you need to create a Tab Widget, but you may not have full control over the HTML of your document. 43 | 44 | For instance: 45 | ```html 46 | 47 | 68 | ``` 69 | 70 | ### Tab Widget attributes & options 71 | The Tab Widget script runs through the markup looking for specific `data-atabs-*` attributes to use as hooks to modify the original markup and generate the final component. Here are the `data` attributes that are recognized by this script. 72 | 73 | * `data-atabs` 74 | The primary hook. This necessary attribute is used to contain the final Tab Widget. 75 | * `data-atabs-panel` 76 | Designates that an element should serve as a `tabpanel`. If given the value of "default", the script will set this `tabpanel` and associated `tab` to be active, instead of automatically first `tab` and `tabpanel`. If multiple `data-atabs-panel` attributes have the value of "default", only the first one will be respected. 77 | * `data-atabs-tab-label` 78 | Also used on the element that will be a `tabpanel`, this attribute indicates that the generated `tab` should use its value as the `tab`'s label. The value of `data-atabs-tab-label` takes precedents over using the content of the `tabpanel`'s heading when generating the `tab`. 79 | * `data-atabs-heading` 80 | Place this attribute on the element that serves as the heading within the `tabpanel`. Unless a `data-atabs-tab-label` is used on the `tabpanel`, this heading will serve as the accessible name for the generated `tab`. By default, elements with the `data-atabs-heading` attribute will be removed after their content has been used for the generated `tab`, unless the value of "keep" is set, e.g. `data-atabs-heading="keep`. Only the first instance of an element with this attribute will be recognized by the script. 81 | * `data-atabs-toc` 82 | Without JavaScript, a table of contents (TOC) can provide easy access to different sections of a document that would have otherwise been part of the Tab Widget. With JavaScript available, the TOC isn't as necessary. Providing this attribute with the `id` of the TOC parent element will remove the TOC from the DOM. 83 | * `data-atabs-manual` 84 | By default, when keyboard focus is set to a `tab`, its associated `tabpanel` will open by default, and the `tab` will be set to the selected state. 85 | 86 | If this attribute is set to the `data-atabs` wrapper of *any* Tab Widget in a document, it will make **all** Tab Widgets require manual activation of a `tab` to reveal its associated `tabpanel`. The reason this globally affects Tab Widgets is to mitigate any possibility of an inconsistent user experience between different Tab Widgets in the same document. 87 | * `data-atabs-orientation` 88 | If this attribute is set to the `data-atabs` wrapper element, and it's value is set to "vertical", then it will add `aria-orientation="vertical"` to the `tablist`. As `aria-orientation` is not well supported across all screen readers, this does not presently have any effect on the current functionality of the Tab Widget. 89 | * `data-atabs-panel-wrap` 90 | Optional: use this attribute on a wrapping element for all of the `tabpanel`s. Useful for styling a single wrapper for each `tabpanel`. 91 | 92 | ## User Experience 93 | Tab Widgets are a type of show/hide component, and the manner in which you interact with a Tab Widget will dictate the experience. 94 | 95 | For mouse and touch users, the show/hide functionality is initiated by interacting with the `tab`s within the `tablist`. Activating a `tab` will reveal the `tabpanel` its associated with, while also deactivating the previously active `tab`, and hiding its `tabpanel`. 96 | 97 | For keyboard users, and screen reader users in forms mode, a Tab Widget will automatically select and reveal the contents of the focused `tab` as a user navigates the `tablist` with arrow keys. 98 | 99 | A option is available (`data-atabs-manual`) to require manual activation of `tab`s. This is not necessarily preferred behavior of a Tab Widget, but there may be instances where it's required due to performance issues. 100 | 101 | The [ARIA Authoring Practices](https://www.w3.org/TR/wai-aria-practices-1.2/#tabpanel) notes: 102 | >It is recommended that tabs activate automatically when they receive focus as long as their associated tab panels are displayed without noticeable latency. This typically requires tab panel content to be preloaded. Otherwise, automatic activation slows focus movement, which significantly hampers users' ability to navigate efficiently across the tab list. 103 | 104 | This script has been tested to work with mouse, touch, and keyboard devices. Each input device has also been re-tested while running various browser and screen reader pairings. 105 | 106 | **FYI: More information concerning expected functionality and screen reader announcements will be linked to from here.** 107 | 108 | ## Dependencies and known issues 109 | There are no dependencies for this script. Any necessary polyfill (for IE11) is included in the JavaScript. 110 | 111 | There are some issues with how screen readers interact with Tab Widgets. 112 | * [NVDA: Unexpected focusing of inactive `tab`s in `tablist`](https://github.com/nvaccess/nvda/issues/8906) 113 | * [JAWS: Unexpected focusing of inactive `tab`s in `tablist`](https://github.com/FreedomScientific/VFO-standards-support/issues/132) 114 | * JAWS + Windows 7 Firefox: 115 | Navigating out of a `tablist` by use of Tab key, focus will be moved to the exposed `tabpanel`. JAWS will not leave forms mode as expected, and will require that the user do so manually to navigate the contents of the `tabpanel`. 116 | 117 | 118 | ## Additional Reading 119 | * [ARIA Specification: Tab Role](https://www.w3.org/TR/wai-aria-1.2/#tab) 120 | * [ARIA Specification: Tablist Role](https://www.w3.org/TR/wai-aria-1.2/#tablist) 121 | * [Aria Specification: Tabpanel Role](https://www.w3.org/TR/wai-aria-1.2/#tabpanel) 122 | * [WAI-ARIA Authoring Practices: Tab Widgets](https://www.w3.org/TR/wai-aria-practices-1.2/#tabpanel) 123 | 124 | 125 | ## License, Thank yous & Such 126 | This script was written by Scott O'Hara: [Website](https://www.scottohara.me), [Twitter](https://twitter.com/scottohara). 127 | 128 | It has an [MIT](https://github.com/scottaohara/accessible-components/blob/master/LICENSE.md) license. 129 | 130 | Special thanks to [Josh Drumm](https://github.com/wwnjp) and [Chris Ferdinandi](https://gomakethings.com/) for helping me with some JavaScript refactoring and helper functions. 131 | 132 | Thanks to [Léonie Watson](https://tink.uk/), Adam Campfield, Ryan Adams, and many others who provided excellent user feedback. 133 | 134 | Use it, modify it, contribute to it to help make your project more accessible :) 135 | -------------------------------------------------------------------------------- /assets/css/--demo--.css: -------------------------------------------------------------------------------- 1 | *, 2 | *:before, 3 | *:after { 4 | box-sizing: border-box; 5 | } 6 | 7 | body{overflow-x: hidden;} 8 | body,html{background:#fafafa;color:#450c13;font-family: arial;line-height:1.3;margin:0;padding:0;} 9 | .page{padding:20px;margin:auto; max-width:990px;} 10 | 11 | header { 12 | border-bottom: 1px solid #999; 13 | padding-bottom: 1.5em; 14 | } 15 | 16 | header nav a { 17 | display: inline-block; 18 | margin-right: 1em; 19 | } 20 | 21 | header nav a:first-of-type { 22 | border-right: 1px solid; 23 | padding-right: 1em; 24 | } 25 | 26 | a[href]:focus { 27 | outline: 2px solid; 28 | outline-offset: 2px; 29 | } 30 | -------------------------------------------------------------------------------- /assets/css/tabs.css: -------------------------------------------------------------------------------- 1 | .atabs { 2 | background: #fff; 3 | position: relative; 4 | } 5 | 6 | .atabs__list:not([aria-orientation="vertical"]) { 7 | border-left: 1px solid #999; 8 | border-right: 1px solid #999; 9 | border-top: 1px solid #999; 10 | display: flex; 11 | overflow: auto; 12 | white-space: nowrap; 13 | } 14 | 15 | [data-atabs-orientation="vertical"] { 16 | border-top: 1px solid; 17 | display: flex; 18 | flex-wrap: wrap; 19 | } 20 | 21 | 22 | .atabs__list[aria-orientation="vertical"] { 23 | width: 100% 24 | } 25 | 26 | .atabs__list[aria-orientation="vertical"] .atabs__list__tab { 27 | border-right: 0; 28 | display: inline-block; 29 | text-align: left; 30 | width: 100%; 31 | } 32 | 33 | [data-atabs-orientation="vertical"] .atabs__panel { 34 | width: 100%; 35 | } 36 | 37 | [data-atabs-orientation="vertical"] [role="tablist"] { 38 | border-left: 1px solid; 39 | border-right: 1px solid; 40 | } 41 | 42 | @media screen and ( min-width: 34em ) { 43 | [data-atabs-orientation="vertical"] .atabs__panel { 44 | width: calc(100% - 12em); 45 | } 46 | 47 | .atabs__list[aria-orientation="vertical"] { 48 | width: 12em; 49 | } 50 | 51 | .atabs__list[aria-orientation="vertical"] .atabs__list__tab:last-of-type { 52 | border-bottom: 0; 53 | } 54 | 55 | [data-atabs-orientation="vertical"] [role="tablist"] { 56 | border-bottom: 1px solid; 57 | border-left: 1px solid; 58 | border-right: 0; 59 | } 60 | 61 | [data-atabs-orientation="vertical"] [role="tablist"] { 62 | border-left: 1px solid; 63 | } 64 | } 65 | 66 | .atabs__list[hidden] { 67 | display: none; 68 | } 69 | 70 | /** 71 | * Just to make sure invalid children of a 72 | * tablist are not visible/accessible. 73 | * these should also be removed from the DOM 74 | */ 75 | .atabs__list > :not([role="tab"]) { 76 | display: none; 77 | } 78 | 79 | .atabs__list__tab { 80 | -webkit-appearance: none; /* btn */ 81 | appearance: none; /* btn */ 82 | background: #fff; 83 | border: 0; /* btn */ 84 | border-bottom: 1px solid; 85 | flex-grow: 1; 86 | font: inherit; /* btn */ 87 | margin: 0; /* btn */ 88 | padding: .825em 1em; 89 | position: relative; 90 | text-decoration: none; /* if */ 91 | } 92 | 93 | .atabs__list__tab:not(:last-of-type) { 94 | border-right: 1px solid; 95 | } 96 | 97 | .atabs__list__tab:active, 98 | .atabs__list__tab:hover, 99 | .atabs__list__tab:focus { 100 | background: #4464c2; 101 | color: #fff; 102 | outline: 2px solid #4464c2; 103 | outline-offset: -2px; 104 | } 105 | 106 | .atabs__list__tab[aria-disabled="true"] { 107 | cursor: not-allowed; 108 | opacity: 0.5; 109 | } 110 | 111 | .atabs__list__tab > span, 112 | .atabs__list__tab > svg, 113 | .atabs__list__tab > img { 114 | pointer-events: none; 115 | } 116 | 117 | .atabs__list__tab[aria-selected="true"] { 118 | background: #213469; 119 | border-bottom: 3px solid #213469; /* high contrast vid */ 120 | color: #fff; 121 | } 122 | 123 | .atabs__list__tab[aria-selected="true"]:hover, 124 | .atabs__list__tab[aria-selected="true"]:focus { 125 | box-shadow: inset 0 1px 0 4px #fff; 126 | } 127 | 128 | .atabs__panel[hidden] { 129 | display: none; 130 | } 131 | 132 | .atabs__panel { 133 | border: 1px solid; 134 | border-top: 0; 135 | padding: 1.25em; 136 | } 137 | 138 | .atabs__panel:focus-visible { 139 | box-shadow: inset 0 0 1px #222; 140 | outline: 2px solid; 141 | outline-offset: -2px; 142 | } 143 | 144 | .atabs__panel:focus, 145 | .atabs__panel.focus-visible { 146 | box-shadow: inset 0 0 1px #0072e4; 147 | outline: 4px solid #0072e4; 148 | outline-offset: -3px; 149 | } 150 | 151 | .atabs__panel > :first-child { 152 | margin-top: 0; 153 | } 154 | 155 | .atabs__panel > :last-child { 156 | margin-bottom: 0; 157 | } 158 | -------------------------------------------------------------------------------- /assets/js/.gitkeep: -------------------------------------------------------------------------------- 1 | #hi 2 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ARIA Tabs 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 |
16 |

17 | Tab Widget tests 18 |

19 |

20 | Examples of different configurations to generate Tab Widgets. 21 |

22 |

23 | Review 24 | 25 | the code on GitHub. 26 |

27 |
28 | 29 |
30 |
31 |

Tab Widget Example

32 | 33 |
34 |
35 |

36 | Apples 37 |

38 |

39 | No explicit default was set, so per standard functionality 40 | this first tabpanel will be shown by default. 41 |

42 |

43 | Here's a link just to test keyboard tabbing. 44 | (it's odd talking about tabs, and tabs.) 45 |

46 |
47 |
48 |

49 | Bananas 50 |

51 |

52 | This is an example of a heading being used to populate the 53 | tab's label, while also not removing heading 54 | from the tabpanel. This is done by adding 55 | the value "keep" to data-atabs-heading. 56 |

57 |
58 |
59 |

60 | There are two headings here 61 |

62 |

63 | The tab is getting its label from the 64 | data-atabs-tab-label. The heading is 65 | kept in the tabpanel because it does 66 | not have the data-atabs-heading 67 | attribute. 68 |

69 |

It wouldn't make much sense

70 |

71 | If the contents of a tabpanel contain 72 | additional sub headings, it wouldn't make sense to remove 73 | the initial heading from the tabpanel. 74 | Doing so would create a gap in the heading structure. 75 |

76 |
77 |
78 |
79 | 80 |
81 |

More demos

82 | 93 |
94 | 95 |
96 |
97 | 98 | 99 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | if ( typeof Object.assign != 'function' ) { 4 | // Must be writable: true, enumerable: false, configurable: true 5 | Object.defineProperty(Object, "assign", { 6 | value: function assign( target, varArgs ) { // .length of function is 2 7 | 8 | if ( target == null ) { // TypeError if undefined or null 9 | throw new TypeError('Cannot convert undefined or null to object'); 10 | } 11 | 12 | var to = Object(target); 13 | 14 | for ( var index = 1; index < arguments.length; index++ ) { 15 | var nextSource = arguments[index]; 16 | 17 | if ( nextSource != null ) { // Skip over if undefined or null 18 | for ( var nextKey in nextSource ) { 19 | // Avoid bugs when hasOwnProperty is shadowed 20 | if ( Object.prototype.hasOwnProperty.call(nextSource, nextKey) ) { 21 | to[nextKey] = nextSource[nextKey]; 22 | } 23 | } 24 | } 25 | } 26 | return to; 27 | }, 28 | writable: true, 29 | configurable: true 30 | }); 31 | } 32 | 33 | if ( Element && !Element.prototype.matches ) { 34 | var proto = Element.prototype; 35 | proto.matches = proto.matchesSelector || 36 | proto.mozMatchesSelector || proto.msMatchesSelector || 37 | proto.oMatchesSelector || proto.webkitMatchesSelector; 38 | } 39 | 40 | // add utilities 41 | var util = { 42 | keyCodes: { 43 | UP: 38, 44 | DOWN: 40, 45 | LEFT: 37, 46 | RIGHT: 39, 47 | HOME: 36, 48 | END: 35, 49 | ENTER: 13, 50 | SPACE: 32, 51 | DELETE: 46, 52 | TAB: 9 53 | }, 54 | 55 | generateID: function ( base ) { 56 | return base + Math.floor(Math.random() * 999); 57 | }, 58 | 59 | getDirectChildren: function ( elm, selector ) { 60 | return Array.prototype.filter.call(elm.children, function ( child ) { 61 | return child.matches(selector); 62 | }); 63 | }, 64 | 65 | getUrlHash: function () { 66 | return window.location.hash.replace('#', ''); 67 | }, 68 | 69 | /** 70 | * Use history.replaceState so clicking through Tabs 71 | * does not create dozens of new history entries. 72 | * Browser back should navigate to the previous page 73 | * regardless of how many Tabs were activated. 74 | * 75 | * @param {string} hash 76 | */ 77 | setUrlHash: function( hash ) { 78 | if ( history.replaceState ) { 79 | history.replaceState(null, '', '#' + hash); 80 | } else { 81 | location.hash = hash; 82 | } 83 | }, 84 | 85 | /** 86 | * Prevent focus on event target by blurring and resetting 87 | * the focus to the previous focus target if possible. 88 | * 89 | * @param event 90 | */ 91 | preventFocus: function (event) { 92 | event.preventDefault(); 93 | 94 | var currentFocusTarget = event.currentTarget; 95 | var previousFocusTarget = event.relatedTarget; 96 | 97 | // Try to remove the focus from this element. 98 | // This is important to always perform, since just focusing the previously focused element won't work in Edge/FF, 99 | // if that element is unable to actually get the focus back (became invisible, etc.): the focus would stay on the 100 | // current element in such a case 101 | if (currentFocusTarget && typeof currentFocusTarget.blur === 'function') { 102 | currentFocusTarget.blur(); 103 | } 104 | 105 | if (previousFocusTarget && typeof previousFocusTarget.focus === 'function') { 106 | // Revert focus back to previous blurring element 107 | event.relatedTarget.focus(); 108 | } 109 | } 110 | }; 111 | 112 | (function ( w, doc, undefined ) { 113 | /** 114 | * ARIA Tabbed Interface 115 | * Creates a tab list to toggle the visibility of 116 | * different subsections of a document. 117 | * 118 | * Author: Scott O'Hara 119 | * Version: 2.1.4 120 | * License: https://github.com/scottaohara/a11y_tab_widget/blob/master/LICENSE 121 | */ 122 | var ARIAtabsOptions = { 123 | baseID: 'atab_', 124 | defaultTabLabel: 'Tab ', 125 | elClass: 'atabs', 126 | customTabClassAttribute: 'data-atabs-tab-class', 127 | tabLabelAttribute: 'data-atabs-tab-label', 128 | headingAttribute: 'data-atabs-heading', 129 | defaultOrientation: 'horizontal', 130 | orientationAttribute: 'data-atabs-orientation', 131 | panelWrapper: 'data-atabs-panel-wrap', 132 | disabledAttribute: 'data-atabs-disabled', 133 | panelClass: 'atabs__panel', 134 | panelSelector: '[data-atabs-panel]', 135 | tabClass: 'atabs__list__tab', 136 | tabListClass: 'atabs__list', 137 | tablistSelector: '[data-atabs-list]', 138 | manualAttribute: 'data-atabs-manual', 139 | manual: false 140 | }; 141 | 142 | 143 | var ARIAtabs = function ( inst, options ) { 144 | var _options = Object.assign(ARIAtabsOptions, options); 145 | var orientation = _options.defaultOrientation; 146 | var _tabListContainer; 147 | var _tabs = []; 148 | var activeIndex = 0; 149 | var defaultPanel = 0; 150 | var selectedTab = activeIndex; 151 | var el = inst; 152 | var hasPanelWrapper = el.querySelector('[' + _options.panelWrapper + ']'); 153 | var elID; 154 | var headingSelector = '[' + _options.headingAttribute + ']'; 155 | 156 | var init = function () { 157 | elID = el.id || util.generateID(_options.baseID); 158 | 159 | if ( el.getAttribute(_options.orientationAttribute) === 'vertical' ) { 160 | orientation = 'vertical'; 161 | } 162 | 163 | if ( el.hasAttribute(_options.manualAttribute) ) { 164 | _options.manual = true; 165 | } 166 | 167 | el.classList.add(_options.elClass); 168 | 169 | // find or create the tabList 170 | _tabListContainer = generateTablistContainer(); 171 | 172 | // create the tabs and setup the panels 173 | buildTabs.call( this ); 174 | 175 | // If there's a table of contents for no-js sections, 176 | // that won't be needed anymore. Remove it. 177 | deleteTOC(); 178 | 179 | if ( activeIndex > -1 ) { 180 | activateTab(); 181 | } 182 | }; // init() 183 | 184 | 185 | var generateTablistContainer = function () { 186 | var tabListContainer = el.querySelector(_options.tablistSelector) || doc.createElement('div'); 187 | tabListContainer.setAttribute('role', 'tablist'); 188 | tabListContainer.classList.add(_options.tabListClass); 189 | tabListContainer.id = elID + '_list'; 190 | tabListContainer.innerHTML = ''; // clear out anything that shouldn't be there 191 | if ( orientation === 'vertical' ) { 192 | tabListContainer.setAttribute('aria-orientation', orientation); 193 | } 194 | el.insertBefore(tabListContainer, el.querySelector(':first-child')); 195 | 196 | return tabListContainer; 197 | }; // generateTablistContainer() 198 | 199 | 200 | this.addTab = function ( panel, label, customClass ) { 201 | var customClass = customClass || panel.getAttribute(_options.customTabClassAttribute); 202 | var disabled = panel.hasAttribute(_options.disabledAttribute); 203 | 204 | var generateTab = function ( index, id, tabPanel, customClass ) { 205 | var newTab = doc.createElement('span'); 206 | newTab.id = elID + '_tab_' + index; 207 | newTab.tabIndex = -1; 208 | newTab.setAttribute('role', 'tab'); 209 | newTab.setAttribute('aria-selected', activeIndex === index); 210 | if ( activeIndex === index ) { 211 | newTab.setAttribute('aria-controls', id); 212 | } 213 | newTab.setAttribute('data-controls', id); 214 | newTab.innerHTML = tabPanel; 215 | newTab.classList.add(_options.tabClass); 216 | if ( customClass ) { 217 | newTab.classList.add(customClass); 218 | } 219 | if ( disabled ) { 220 | newTab.setAttribute('aria-disabled', true); 221 | newTab.addEventListener('focus', util.preventFocus.bind(this)); 222 | } else { 223 | newTab.addEventListener('click', function () { 224 | onClick.call(this, index); 225 | this.focus(); 226 | updateUrlHash(); 227 | }, false); 228 | 229 | newTab.addEventListener('keydown', tabElementPress.bind(this), false); 230 | //newTab.addEventListener('focus', function () { 231 | // checkYoSelf.call(this, index); 232 | //}, false); 233 | } 234 | 235 | return newTab; 236 | }; 237 | 238 | var newPanel = panel; 239 | var i = _tabs.length; 240 | 241 | if ( !newPanel ) { 242 | return; 243 | } 244 | 245 | var panelHeading = newPanel.querySelector(headingSelector); 246 | var finalLabel = [ 247 | label, 248 | newPanel.getAttribute(_options.tabLabelAttribute), 249 | panelHeading && panelHeading.textContent, 250 | _options.defaultTabLabel + (i + 1) 251 | ] 252 | .filter( function ( l ) { 253 | return l && l !== ''; 254 | })[0]; 255 | 256 | var newId = newPanel.id || elID + '_panel_' + i; 257 | var t = generateTab(i, newId, finalLabel, customClass); 258 | 259 | _tabListContainer.appendChild(t); 260 | newPanel.id = newId; 261 | newPanel.setAttribute('role', 'tabpanel'); 262 | newPanel.setAttribute('aria-labelledby', elID + '_tab_' + i); 263 | newPanel.classList.add(_options.panelClass); 264 | newPanel.hidden = true; 265 | 266 | if ( !el.contains(panel) ) { 267 | el.appendChild(panel); 268 | } 269 | 270 | if ( newPanel.getAttribute('id') === util.getUrlHash()) { 271 | activeIndex = i; 272 | } else if ( defaultPanel === 0 && newPanel.getAttribute('data-atabs-panel') === 'default' ) { 273 | activeIndex = i; 274 | defaultPanel = activeIndex; 275 | } 276 | 277 | if ( panelHeading ) { 278 | if ( panelHeading.getAttribute(_options.headingAttribute) !== 'keep' ) { 279 | panelHeading.parentNode.removeChild(panelHeading); 280 | } 281 | } 282 | 283 | if ( !disabled ) { 284 | newPanel.addEventListener('keydown', panelElementPress.bind(this), false); 285 | newPanel.addEventListener('blur', removePanelTabindex, false); 286 | 287 | _tabs.push({ 288 | tab: t, 289 | panel: newPanel 290 | }); 291 | } 292 | }; // this.addTab 293 | 294 | 295 | var buildTabs = function () { 296 | var tabs; 297 | 298 | /** 299 | * Typically tab panels should be direct children of the main tab widget 300 | * wrapper. This is necessary so that the script can appropriately associate 301 | * each tablist with the appropriate tabpanels, allowing for nested tab widgets. 302 | * 303 | * If a wrapper for the tabpanels is necessary, for styling/other reasons, then 304 | * this if statement will look to see if the appropriate panel wrapper div is in 305 | * place, and if so, adjust the element used to look for the direct children. 306 | */ 307 | if ( hasPanelWrapper ) { 308 | tabs = util.getDirectChildren(hasPanelWrapper, _options.panelSelector); 309 | } 310 | else { 311 | tabs = util.getDirectChildren(el, _options.panelSelector); 312 | } 313 | 314 | 315 | for ( var i = 0; i < tabs.length; i++ ) { 316 | this.addTab(tabs[i]); 317 | } 318 | }; // buildTabs() 319 | 320 | 321 | var deleteTOC = function () { 322 | if ( el.getAttribute('data-atabs-toc') ) { 323 | var toc = doc.getElementById(el.getAttribute('data-atabs-toc')); 324 | // safety check to make sure a TOC isn't set to be deleted 325 | // after it's already deleted. e.g. if there are two 326 | // dat-atabs-toc that equal the same ID. 327 | if ( toc ) { 328 | toc.parentNode.removeChild(toc); 329 | } 330 | } 331 | }; // deleteTOC() 332 | 333 | 334 | var incrementActiveIndex = function () { 335 | if ( activeIndex < _tabs.length - 1 ) { 336 | return ++activeIndex; 337 | } 338 | else { 339 | activeIndex = 0; 340 | return activeIndex; 341 | } 342 | }; // incrementActiveIndex() 343 | 344 | 345 | var decrementActiveIndex = function () { 346 | if ( activeIndex > 0 ) { 347 | return --activeIndex; 348 | } 349 | else { 350 | activeIndex = _tabs.length - 1; 351 | return activeIndex; 352 | } 353 | }; // decrementActiveIndex() 354 | 355 | 356 | var focusActiveTab = function () { 357 | _tabs[activeIndex].tab.focus(); 358 | }; // focusActiveTab() 359 | 360 | 361 | var onClick = function ( index ) { 362 | activeIndex = index; 363 | activateTab(); 364 | }; // onClick() 365 | 366 | 367 | var moveBack = function ( e ) { 368 | e.preventDefault(); 369 | decrementActiveIndex(); 370 | focusActiveTab(); 371 | 372 | if ( !_options.manual ) { 373 | activateTab(); 374 | updateUrlHash(); 375 | } 376 | }; // moveBack() 377 | 378 | 379 | var moveNext = function ( e ) { 380 | e.preventDefault(); 381 | incrementActiveIndex(); 382 | focusActiveTab(); 383 | 384 | if ( !_options.manual ) { 385 | activateTab(); 386 | updateUrlHash(); 387 | } 388 | }; // moveNext() 389 | 390 | 391 | /** 392 | * A tabpanel is focusable upon hitting the TAB key 393 | * from a tab within a tablist. When navigating away 394 | * from the tabpanel, with the TAB key, remove the 395 | * tabindex from the tabpanel. 396 | */ 397 | var panelElementPress = function ( e ) { 398 | var keyCode = e.keyCode || e.which; 399 | 400 | switch ( keyCode ) { 401 | case util.keyCodes.TAB: 402 | removePanelTabindex(); 403 | break; 404 | 405 | default: 406 | break; 407 | } 408 | }; // panelElementPress() 409 | 410 | 411 | var removePanelTabindex = function () { 412 | _tabs[activeIndex].panel.removeAttribute('tabindex'); 413 | }; // removePanelTabindex() 414 | 415 | 416 | var tabElementPress = function ( e ) { 417 | var keyCode = e.keyCode || e.which; 418 | 419 | switch ( keyCode ) { 420 | case util.keyCodes.TAB: 421 | _tabs[selectedTab].panel.tabIndex = 0; 422 | activeIndex = selectedTab; 423 | break; 424 | 425 | case util.keyCodes.ENTER: 426 | case util.keyCodes.SPACE: 427 | e.preventDefault(); 428 | activateTab(); 429 | updateUrlHash(); 430 | break; 431 | 432 | case util.keyCodes.LEFT: 433 | case util.keyCodes.UP: 434 | moveBack( e ); 435 | break; 436 | 437 | case util.keyCodes.RIGHT: 438 | case util.keyCodes.DOWN: 439 | moveNext( e ); 440 | break; 441 | 442 | case util.keyCodes.END: 443 | e.preventDefault(); 444 | activeIndex = _tabs.length - 1; 445 | focusActiveTab(); 446 | if ( !_options.manual ) { 447 | activateTab(); 448 | updateUrlHash(); 449 | } 450 | break; 451 | 452 | case util.keyCodes.HOME: 453 | e.preventDefault(); 454 | activeIndex = 0; 455 | focusActiveTab(); 456 | if ( !_options.manual ) { 457 | activateTab(); 458 | updateUrlHash(); 459 | } 460 | break; 461 | 462 | case util.keyCodes.DELETE: 463 | if ( _tabs.length > 1 && canRemove ) { 464 | e.preventDefault(); 465 | removeTabAndPanel(activeIndex); 466 | focusActiveTab(); 467 | } 468 | break; 469 | 470 | default: 471 | break; 472 | } 473 | }; // tabElementPress() 474 | 475 | 476 | /** 477 | * This function shouldn't exist. BUT for... 478 | * https://github.com/nvaccess/nvda/issues/8906 479 | * https://github.com/FreedomScientific/VFO-standards-support/issues/132 480 | * 481 | * Note this doesn't completely fix JAWS announcements. 482 | * With this function, focus will be placed on the correct Tab, 483 | * but JAWS will make no announcement until the user begins 484 | * re-navigating with arrow keys. 485 | * 486 | * The alternative is not using this, having JAWS announce the 487 | * inactive tag (which will receive focus), JAWS will announce 488 | * to use the Space key to activate, but nothing will happen. 489 | */ 490 | // sept19-2021 - commenting this out as it causes focus issues with 491 | // iOS + VoiceOver. 492 | // var checkYoSelf = function ( index ) { 493 | // if ( index !== activeIndex ) { 494 | // focusActiveTab(); 495 | // } 496 | // }; // checkYoSelf() 497 | 498 | 499 | var deactivateTabs = function () { 500 | for ( var i = 0; i < _tabs.length; i++ ) { 501 | deactivateTab(i); 502 | } 503 | }; // deactivateTabs() 504 | 505 | 506 | var deactivateTab = function ( idx ) { 507 | _tabs[idx].panel.hidden = true; 508 | _tabs[idx].tab.tabIndex = -1; 509 | _tabs[idx].tab.setAttribute('aria-selected', false); 510 | _tabs[idx].tab.removeAttribute('aria-controls'); 511 | // remove the aria-controls from inactive tabs since 512 | // a user can *not* move to their associated element 513 | // if that element is not displayed. 514 | }; // deactivateTab() 515 | 516 | 517 | /** 518 | * Update the active Tab and make it focusable. 519 | * Deactivate any previously active Tab. 520 | * Reveal active Panel. 521 | */ 522 | var activateTab = function () { 523 | var active = _tabs[activeIndex] || _tabs[0]; 524 | deactivateTabs(); 525 | active.tab.setAttribute('aria-controls', active.tab.getAttribute('data-controls')); 526 | active.tab.setAttribute('aria-selected', true); 527 | active.tab.tabIndex = 0; 528 | if ( !active.panel.hasAttribute(_options.disabledAttribute) ) { 529 | active.panel.hidden = false; 530 | } 531 | selectedTab = activeIndex; 532 | return selectedTab; 533 | }; // activateTab() 534 | 535 | 536 | /** 537 | * Update URL Hash so direct link to the currently open Tab is exposed for copy & paste. 538 | */ 539 | var updateUrlHash = function () { 540 | var active = _tabs[activeIndex]; 541 | util.setUrlHash(active.tab.getAttribute('data-controls')); 542 | }; // updateUrlHash() 543 | 544 | 545 | init.call( this ); 546 | return this; 547 | }; // ARIAtabs() 548 | 549 | 550 | w.ARIAtabs = ARIAtabs; 551 | })( window, document ); 552 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Accessible ARIA Tab Widgets", 3 | "name": "a11y_tab_widgets", 4 | "description": "ES5 script to create accessible tab widgets", 5 | "version": "2.1.4", 6 | "main": "index.js", 7 | "homepage": "https://github.com/scottaohara/a11y_tab_widget/blob/master/README.md", 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://scottaohara@github.com/scottaohara/a11y_tab_widget.git" 11 | }, 12 | "bugs": { 13 | "url": "https://github.com/scottaohara/a11y_tab_widget/issues" 14 | }, 15 | "keywords": [ 16 | "a11y", 17 | "aria", 18 | "accessibility", 19 | "tabs", 20 | "tabpanels", 21 | "tablists", 22 | "tabbedUI", 23 | "javascript", 24 | "es5" 25 | ], 26 | "license": "MIT", 27 | "author": [ 28 | { 29 | "name": "Scott O'Hara", 30 | "email": "sao.npm@gmail.com", 31 | "web": "https://www.scottohara.me" 32 | } 33 | ], 34 | "scripts": { 35 | "test": "echo \"Error: no test specified\" && exit 1" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/disabled-tab.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Disabled Tab test | ARIA Tabs 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 |
16 |

17 | Tab widget with one disabled Tab 18 |

19 |

20 | The disabled Tab is not focusable via sequential keyboard navigation with the TAB key, arrow keys 21 | or pointer device (mouse, etc.) clicks. Consequently, the disabled Tab cannot activate a panel. 22 |

23 | 27 |
28 | 29 |
30 |
31 |
32 |

33 | Apples 34 |

35 |

36 | No explicit default was set, so per standard functionality 37 | this first tabpanel will be shown by default. 38 |

39 |

40 | Here's a link just to test keyboard tabbing. 41 | (it's odd talking about tabs, and tabs.) 42 |

43 |
44 |
45 |

46 | Bananas 47 |

48 |

49 | This is an example of a heading being used to populate the 50 | tab's label, while also not removing heading 51 | from the tabpanel. This is done by adding 52 | the value "keep" to data-atabs-heading. 53 |

54 |
55 |
56 |

57 | There are two headings here 58 |

59 |

60 | The tab is getting its label from the 61 | data-atabs-tab-label. The heading is 62 | kept in the tabpanel because it does 63 | not have the data-atabs-heading 64 | attribute. 65 |

66 |

It wouldn't make much sense

67 |

68 | If the contents of a tabpanel contain 69 | additional sub headings, it wouldn't make sense to remove 70 | the initial heading from the tabpanel. 71 | Doing so would create a gap in the heading structure. 72 |

73 |

Special demo: enabled a disabled tab

74 |

There are some cases, e.g. in a multi-step process, where a user completes a task in an exposed 75 | tab panel or panels, and then by completion of that task a previously disabled tab becomes enabled. 76 |

77 |

The following button showcases how that could be implemented for the disabled "Mangos" tab.

78 | 79 |
80 |
81 |

82 | Mangos (out of stock) 83 |

84 |

This text will never be visible, unless the tab is enabled via JS. Please refer to the demo button 85 | in the "Oranges" tab for an example how this could be achieved.

86 |
87 |
88 | 89 | 90 | 93 |
94 | 95 |
96 | 97 | 98 | 129 | 130 | 131 | -------------------------------------------------------------------------------- /tests/focus-test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Tablist focus test 6 | 12 | 13 | 14 | 15 |
16 | 17 |

18 | A test for focusing into a tablist 19 |

20 | 21 |

22 | Expected behavior: with JAWS or NVDA running, navigate using virtual cursor or by heading elements. When encountering "The test, heading level 2" press the Tab key. 23 |

24 |

25 | Focus should move to the "Banana" tab, as it has a tabindex=0 while the "Apple" tab has a tabindex=-1. 26 |

27 |

28 | Actual behavior: keyboard focus is not set to "Banana" and is instead set to "Apple" even though it has a negative tabindex. 29 |

30 |

31 | this a focusable element for testing. 32 |

33 | 34 |

35 | The test 36 |

37 | 38 |
39 | 40 | 41 | 42 |
43 | 44 |

Just so you don't have to view source

45 |

46 | You can check out the tablist code... 47 |

48 |
49 | The code 50 |

51 | 		<div role="tablist">
52 | 		  <button tabindex="-1" role="tab">Apple</button>
53 | 		  <button tabindex="0" role="tab" aria-selected="true">Banana</button>
54 | 		  <button tabindex="-1" role="tab">Orange</button>
55 | 		</div>
56 | 				
57 |
58 |
59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /tests/has-toc.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | No-js Table of contents test | ARIA Tabs 6 | 7 | 8 | 9 | 10 | 15 | 16 | 17 | 18 |
19 | 20 |
21 |

22 | Content with Table of Contents w/out JavaScript 23 |

24 |

25 | Turn off JavaScript and a table of contents navigation with skip links to the different sections will be available. 26 |

27 | 31 |
32 | 33 |
34 | 51 | 52 |

Where you're going the TOC won't even be there...

53 | 54 |
55 |
56 |

57 | Apples 58 |

59 |

60 | No explicit default was set, so per standard functionality 61 | this first tabpanel will be shown by default. 62 |

63 |

64 | Here's a link just to test keyboard tabbing. 65 | (it's odd talking about tabs, and keyboard tabs.) 66 |

67 |
68 |
69 |

70 | Bananas 71 |

72 |

73 | This is an example of a heading being used to populate the 74 | tab's label, while also not removing heading 75 | from the tabpanel. This is done by adding 76 | the value "keep" to data-atabs-heading. 77 |

78 |
79 |
80 |

81 | I like oranges 82 |

83 |

84 | Orange juice is great. 85 |

86 |
87 |
88 | 89 | 90 | 93 |
94 | 95 |
96 | 97 | 98 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /tests/inject-tabs-into-widget.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Dynamically Injected ARIA Tabs 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |

Injected content

16 | 20 |
21 | 22 |
23 |

24 | This Tab Widget only has a single section of content within it by default. Additional 25 | tabs/panels are collected from other locations in the DOM, and re-located within the Tab Widget. 26 |

27 | 28 |
29 |
30 |

31 | Apples 32 |

33 |

34 | No explicit default was set, so this 35 | panel should be shown by default. 36 |

37 |

and a link for focus fun (i go nowhere).

38 |
39 |
40 | 41 | 45 |
46 |

Was External

47 |

48 | This section was outside of the main tab grouping. 49 |

50 |

51 | If JS is off / broken, "was" won't make much sense here... 52 |

53 |
54 | 55 |
56 |

Was External 2

57 |

58 | This next section was outside of the main tab grouping. 59 |

60 |

61 | If JS is off / broken, "was" won't make much sense here... 62 |

63 |
64 | 65 |
66 |

Was External 3

67 |

68 | This other next section, after the previous next section, was outside of the main tab grouping. 69 |

70 |

71 | If JS is off / broken, "was" won't make much sense here... 72 |

73 |
74 | 75 | 78 |
79 | 80 |
81 | 82 | 83 | 84 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /tests/manual.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Manually activated ARIA Tabs 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 |
16 |

17 | Manual Tab Panels 18 |

19 |

20 | A tabpanels will be opened after the currently focused tab element has been purposefully activated by press of Enter or Space keys.

21 | 25 |
26 | 27 |
28 |

29 | Manual activation example 30 |

31 | 32 |
33 |
34 |

35 | Apples 36 |

37 |

38 | No explicit default was set, so per standard functionality 39 | this first tabpanel will be shown by default. 40 |

41 |

42 | Here's a link just to test keyboard tabbing. 43 | (it's odd talking about tabs, and keyboard tabs.) 44 |

45 |
46 |
47 |

48 | Oranges 49 |

50 |

51 | 55 | This is an example of a heading being used to populate the 56 | tab's label, while also not removing heading 57 | from the tabpanel. This is done by adding 58 | the value "keep" to data-atabs-heading. 59 |

60 |
61 |
62 |

63 | These things are never about fruit... 64 |

65 |

66 | The tab is getting its label from the 67 | data-atabs-tab-label. The heading is 68 | kept in the tabpanel because it does 69 | not have the data-atabs-heading 70 | attribute. 71 |

72 | 73 |
74 |
75 | 76 | 77 | 80 |
81 | 82 |
83 | 84 | 85 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /tests/multiple-instances.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Multiple instances of ARIA Tabs 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 |
16 |

17 | Tab Widget tests 18 |

19 |

20 | Examples of different configurations to generate Tab Widgets. 21 |

22 | 26 |
27 | 28 |
29 | 32 |
33 |

Tab Widget Example 1

34 | 35 | 52 | 53 |
54 |
55 |

56 | Apples 57 |

58 |

59 | No explicit default was set, so per standard functionality 60 | this first tabpanel will be shown by default. 61 |

62 |

63 | Here's a link just to test keyboard tabbing. 64 | (it's odd talking about tabs, and tabs.) 65 |

66 |
67 |
68 |

69 | Bananas 70 |

71 |

72 | This is an example of a heading being used to populate the 73 | tab's label, while also not removing heading 74 | from the tabpanel. This is done by adding 75 | the value "keep" to data-atabs-heading. 76 |

77 |
78 |
79 |

80 | Oranges are delicious and super great 81 |

82 |

83 | The tab is getting its label from the 84 | data-atabs-tab-label. The heading is 85 | kept in the tabpanel because it does 86 | not have the data-atabs-heading 87 | attribute. 88 |

89 |

90 | This is also an example of why using data-atabs-tab-label to define a tab's label 91 | can be preferred to using a heading or Table of Contents link. Shorter tab labels work better 92 | for these types of UI, rather than long headings / link labels. 93 |

94 |
95 |
96 |
97 | 98 | 99 | 102 |
103 |

Tab Widget Example 2

104 | 105 |
106 | 113 |
114 |
115 |

My default heading

116 |

117 | The tab for this tabpanel is created from a 118 | data-atabs-tab-label attribute which is on the 119 | data-atabs-panel. 120 |

121 |

122 | The tab associated with this panel has it's own unique 123 | class, beyond the default one set by the script. This is done 124 | via the tabpanel having the data-atabs-tab-class with a single string value of the 125 | class to add. For example "test-class". 126 |

127 |
128 | 129 |
130 |

Carrots

131 |

132 | The data-atabs-panel has the value "default", which results in this tabpanel being opened by default. 133 |

134 | 135 |

136 | While not necessarily condoned, this tabpanel 137 | contains a separate Tab Widget instance. 138 |

139 | 140 | 143 |
144 | 145 |
146 |

Lettuce

147 |

Leafy and crunchy, unless it's old and mushy...

148 |
149 | 150 |
151 |

Ice cream, the default tabpanel

152 |

153 | Not only is this Tab Widget nested within another, it also 154 | has data-atabs-orientation="vertical" set to the 155 | widget wrapper. This attribute with the "vertical" value 156 | will convert a tablist to a stacked UI. aria-orientation="vertical" will be 157 | set to the tablist. Up, down, left and right arrow keys will all still be used to navigate the 158 | individual tabs. 159 |

160 |
161 | 162 |
163 |

Mushrooms

164 |

Either gross or food. I don't get it.

165 |
166 | 167 |
168 | 173 | This tablist will be repositioned to the top 174 | of the Tab Widget container. And this message will be 175 | deleted when the script runs. So if you're seeing this, hi! 176 |
177 |
178 |
179 | 180 |
181 |

Not default

182 |

This tabpanel also has data-atabs-panel="default", 183 | but as it is not the first tabpanel with "default" as its value, 184 | this one is ignored. 185 |

186 |
187 |
188 |
189 | 190 | 193 | 194 |
195 | 196 |
197 | 198 | 199 | 200 | 201 | 205 |
206 |

This was an external section

207 |

208 | This section existed outside of the primary grouping of tab panels. 209 |

210 |

211 | You might need something like this if you don't have full control 212 | over your markup but need to create a Tab Widget with what you 213 | have to work with. 214 |

215 |

216 | For full context, view source to see the script that 217 | sets up the Tab Widgets. The following code snippet are the lines 218 | specific to adding a tab and tabpanel to 219 | a specific Tab Widget. 220 |

221 |

222 | var injectContent = document.getElementById('inject-content');
223 | var cloneContent = injectContent.cloneNode(true);
224 | var allTabs = []; // collection of all Tab Widgets
225 | 
226 | // remove the original instance of the external content from the document.
227 | injectContent.parentNode.removeChild(injectContent);
228 | 
229 | // Inject the external content into a particular Tab Widget.
230 | allTabs[1].addTab(cloneContent, 'This tab label is long for testing purposes', 'custom-class');
231 |       
232 |
233 | 234 | 235 | 236 | 257 | 258 | 259 | -------------------------------------------------------------------------------- /tests/nested.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Nested ARIA Tabs 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 |
16 |

17 | Nested Tab Widgets 18 |

19 | 23 |
24 | 25 |
26 | 27 |
28 |

A tale of two Tab Widgets

29 | 30 |
31 |
32 |

My default heading

33 |

34 | The tab for this tabpanel is created from a 35 | data-atabs-tab-label attribute which is on the 36 | data-atabs-panel. 37 |

38 |

39 | The tab associated with this panel has it's own unique 40 | class, beyond the default one set by the script. This is done 41 | via the tabpanel having the data-atabs-tab-class with a single string value of the class to add. For example "test-class". 42 |

43 |
44 | 45 |
46 |

Default

47 |

48 | The data-atabs-panel has 49 | the value "default", which results in 50 | this tabpanel being opened 51 | by default. 52 |

53 | 54 |

55 | While not necessarily condoned, 56 | this tabpanel 57 | contains a separate Tab Widget instance. 58 |

59 | 60 | 63 |
64 | 65 |
66 |

Apples

67 |

Hey. Apples are great, aren't they?

68 |
69 | 70 |
71 |

The default nested tabpanel

72 |

73 | This Tab Widget has data-atabs-orientation="vertical" set to the 74 | widget wrapper. This attribute with the "vertical" value 75 | will convert a tablist to a stacked UI. aria-orientation="vertical" will be set to the tablist. 76 |

77 |

78 | aria-orientation is not universally supported yet, so this 79 | is more for introducing announcements for future behavior, rather 80 | than for any unique functionality at this time. 81 |

82 |
83 |
84 |
85 | 86 |
87 |

Oranges

88 |

This tabpanel also has data-atabs-panel="default", 89 | but as it is not the first tabpanel with "default" as its value, 90 | this one is ignored. 91 |

92 |

93 | Also, there is no information about 94 | oranges here. Apologies. 95 |

96 |
97 |
98 |
99 | 100 | 103 |
104 | 105 |
106 | 107 | 108 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /tests/tab-panel-wrapper.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ARIA Tabs with Tab Panel wrapping element 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 |
16 |

17 | Tab Widget with tabpanel wrapping div 18 |

19 | 23 |
24 | 25 |
26 |
27 |

Tab Widget Example

28 | 29 |
30 |
31 |
32 |

33 | Apples 34 |

35 |

36 | No explicit default was set, so per standard functionality 37 | this first tabpanel will be shown by default. 38 |

39 |

40 | Here's a link just to test keyboard tabbing. 41 | (it's odd talking about tabs, and tabs.) 42 |

43 |
44 |
45 |

46 | Bananas 47 |

48 |

49 | This is an example of a heading being used to populate the 50 | tab's label, while also not removing heading 51 | from the tabpanel. This is done by adding 52 | the value "keep" to data-atabs-heading. 53 |

54 |
55 |
56 |

57 | There are two headings here 58 |

59 |

60 | The tab is getting its label from the 61 | data-atabs-tab-label. The heading is 62 | kept in the tabpanel because it does 63 | not have the data-atabs-heading 64 | attribute. 65 |

66 |

It wouldn't make much sense

67 |

68 | If the contents of a tabpanel contain 69 | additional sub headings, it wouldn't make sense to remove 70 | the initial heading from the tabpanel. 71 | Doing so would create a gap in the heading structure. 72 |

73 |
74 |
75 |
76 |
77 | 78 | 79 |
80 |
81 | 82 | 83 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /tests/vertical-orientation.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Vertical ARIA Tabs 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 |
16 |

17 | Vertical ARIA Tab Widget 18 |

19 | 23 |
24 | 25 |
26 |

27 | Up and down 28 |

29 | 30 |
31 |
32 |

Apples

33 |

Nothing particularly new to report here...

34 |
35 | 36 |
37 |

Bananas

38 |

39 | This Tab Widget has data-atabs-orientation="vertical" set to the 40 | widget wrapper. This attribute with the "vertical" value 41 | will convert a tablist to a stacked UI. aria-orientation="vertical" will be set to the tablist. 42 |

43 |

44 | aria-orientation is not universally supported yet, so this 45 | is more for introducing announcements for future behavior, rather 46 | than for any unique functionality at this time. 47 |

48 |
49 | 50 |
51 | 56 | This tablist will be repositioned to the top 57 | of the Tab Widget container. And this message will be 58 | deleted when the script runs. So if you're seeing this, hi! 59 | How's the Internet treating you with JS turned off? 60 |
61 |
62 | 63 | 66 |
67 | 68 |
69 | 70 | 71 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /tests/windows-7-tabs.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Windows 7 Tabs | ARIA Tabs 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 |
16 |

17 | Tabpanels contain role=document elements 18 |

19 |

20 | When testing with Windows 7 + Firefox (though sometimes other browsers) that when using the Tab key to navigate to the active tabpanel, that JAWS will announce the accessible name of the tabpanel, but will not have left forms mode. Wrapping the contents of the tabpanel in a role=document has been reported to "fix" this issue... 21 |

22 | 26 |
27 | 28 |
29 |
30 |

Tab Widget Example with role=document

31 | 32 |
33 |
34 |
35 |

36 | Apples 37 |

38 |

39 | No explicit default was set, so per standard functionality 40 | this first tabpanel will be shown by default. 41 |

42 |

43 | Here's a link just to test keyboard tabbing. 44 | (it's odd talking about tabs, and tabs.) 45 |

46 |
47 |
48 |
49 |
50 |

51 | Bananas 52 |

53 |

54 | This is an example of a heading being used to populate the 55 | tab's label, while also not removing heading 56 | from the tabpanel. This is done by adding 57 | the value "keep" to data-atabs-heading. 58 |

59 |
60 |
61 |
62 |
63 |

64 | There are two headings here 65 |

66 |

67 | The tab is getting its label from the 68 | data-atabs-tab-label. The heading is 69 | kept in the tabpanel because it does 70 | not have the data-atabs-heading 71 | attribute. 72 |

73 |

It wouldn't make much sense

74 |

75 | If the contents of a tabpanel contain 76 | additional sub headings, it wouldn't make sense to remove 77 | the initial heading from the tabpanel. 78 | Doing so would create a gap in the heading structure. 79 |

80 |
81 |
82 |
83 |
84 | 85 |
86 |
87 | 88 | 89 | 101 | 102 | 103 | --------------------------------------------------------------------------------