├── .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 |
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 |
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.
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |