diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..942aa34 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,24 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +## [1.1.0] - 2020-01-11 + +### Added + +- Option to have all `tab` elements in the normal tabbing order, instead of just one. +- Option to ensure `tabpanel` elements are in the normal tabbing order. +- Option to automatically activate/deactivate tabs when moving focus between them using the arrow keys. +- Option to control the `tabindex` value that's added to `tab` and `tabpanel` elements, to account for custom tabbing orders on a page. +- Prevent `tab` activation or de-activation if it has `aria-disabled="true"` set (the API's `open` and `close` methods deliberately ignore this). +- Setting `aria-hidden="true"` as well as `hidden="hidden"` when hiding a `tabpanel`. + +### Changed + +- Set `aria-expanded="true"` on the active `tab` and `tabpanel` elements, instead of just the `tab`. This was originally being set on just the `tab` because NVDA wasn't announcing it on the `tabpanel`, however the WCAG spec does specifically indicate setting it on the `tabpanel`. This change should be a good compromise to be announced by NVDA, whilst also being compliant with the WCAG spec. +- When initialising, and an associated panel is not found for a `tab`, the `role` attribute will be removed if the element has `role="tab"` set. Previously the `role`, `tabindex`, `aria-controls`, `aria-selected`, and `aria-expanded` attributes were all being removed from it regardless, but that risked breaking other functionality on the page unrelated to the tablist. This change should be sufficient to prevent screen reader confusion related to the tablist, without affecting any other page functionality reliant on the processed element(s). + +### Fixed + +- Issue where no tabs were focusable after using the API to programmatically close them all. +- Issue where, when deleting the first `tab`, page focus was moved to the last `tab`. diff --git a/README.md b/README.md index d0a59eb..93db7c9 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![npm version](https://img.shields.io/npm/v/aria-tablist.svg)](http://npm.im/aria-tablist) [![gzip size](http://img.badgesize.io/https://unpkg.com/aria-tablist/dist/aria-tablist.min.js?compression=gzip)](https://unpkg.com/aria-tablist/dist/aria-tablist.min.js) -Dependency-free plain JavaScript module for WCAG compliant tablists +Dependency-free plain JavaScript module for WCAG compliant tablists. Also great for accordions. [Try out the examples](https://mynamesleon.github.io/aria-tablist/examples/) @@ -36,17 +36,19 @@ Or grab the minified JavaScript from unpkg: ``` +The module relies entirely on standard attributes: it sets the `role` on elements if it needs to, `aria-` attributes for indicating behaviour to screen readers, and relies on setting and removing `hidden="hidden"` to toggle element visibility. This means that you can use all of your own class names and styling, and the module won't override them. + ## HTML Requirements / Progressive Enhancement When the module is called on an element, the following steps are taken: -1. The module will look for elements with `role="tab"` set -2. If none are found, all direct children will be processed -3. For each tab (assumed or otherwise) the module will check for a matching panel by: - 1. Checking for an `aria-controls` attribute on the tab, and searching for an element with a matching ID - 2. If the tab has an `id`, searching for an element with an `aria-labelledby` attribute that matches that `id` -4. For any tabs without a matching panel, any applicable ARIA attributes will be removed from it -5. For the tabs that have associated panels, all relevant ARIA attributes will be set automatically +1. The module will look for elements with `role="tab"` set. +2. If none are found, all direct children will be processed. +3. For each assumed `tab`, the module will check for a matching `tabpanel` by: + 1. Checking for an `aria-controls` attribute on the `tab`, and searching for an element with a matching `id`. + 2. If the `tab` has an `id`, searching for an element with an `aria-labelledby` attribute that matches that `id`. +4. For any tabs that were processed where a matching panel was **not** found, if they had `role="tab"` set, the `role` attribute will be removed to prevent confusion to screen reader users. +5. The found tabs and associated panels will then have the relevant `role` and `aria-` attributes set automatically. This means your HTML only needs to indicate the relationship between the tabs and panels, and the module will handle the rest: @@ -60,6 +62,10 @@ This means your HTML only needs to indicate the relationship between the tabs an
...
...
...
+ + ``` So if you need to cater for users without JavaScript, or if the JavaScript fails to load for whatever reason, there won't be any applicable roles set that would confuse a screen reader user. @@ -86,7 +92,7 @@ You can of course include all of the optimal ARIA attributes straight away if yo ## Options -Most of the functionality is assumed from the included ARIA attributes in your HTML (see the [examples](https://mynamesleon.github.io/aria-tablist/examples/)), but some basic options are also available: +Most of the functionality is assumed from the included ARIA attributes in your HTML (see the [examples](https://mynamesleon.github.io/aria-tablist/examples/)). The remaining available options and their defaults are: ```javascript { @@ -100,15 +106,35 @@ Most of the functionality is assumed from the included ARIA attributes in your H */ deletable: false, + /** + * @description make all tabs focusable in the page's tabbing order (by setting a `tabindex` on them), instead of just 1 + */ + focusableTabs: false, + + /** + * @description make all tab panels focusable in the page's tabbing order (by setting a `tabindex` on them) + */ + focusablePanels: true, + + /** + * @description activate a tab when it receives focus from using the arrow keys + */ + arrowActivation: false, + + /** + * @description value to use when setting tabs or panels to be part of the page's tabbing order + */ + tabindex: 0, + /** * @description callback each time a tab opens */ - onOpen: (panel) => {}, + onOpen: (panel, tab) => {}, /** * @description callback each time a tab closes */ - onClose: (panel) => {}, + onClose: (panel, tab) => {}, /** * @description callback when a tab is deleted @@ -122,7 +148,7 @@ Most of the functionality is assumed from the included ARIA attributes in your H } ``` -All component options that accept a Function will have their context (`this`) set to include the full autocomplete API (assuming you use a normal `function: () {}` declaration for the callbacks instead of arrow functions). +All component options that accept a Function will have their context (`this`) set to include the full autocomplete API (assuming you use a normal `function: () {}` declaration for the callbacks instead of arrow functions). ## API @@ -146,21 +172,22 @@ The returned `AriaTablist` class instance exposes the following API (which is al options: Object; /** - * @description trigger a particular tab to open - * @param {Number|Element} index: tab index, or tab element - * @param {Boolean=} focusTab: whether to move focus to the tab + * @description trigger a particular tab to open (even if disabled) + * @param {Number|Element} index - tab index, or tab element + * @param {Boolean} [focusTab=true] - move focus to the tab before opening */ open(index: Number|Element, focusTab: Boolean = true): void; /** - * @description trigger a particular tab to close - * @param {Number|Element} index: tab index, or tab element + * @description trigger a particular tab to close (even if disabled) + * @param {Number|Element} index - tab index, or tab element + * @param {Boolean} [focusTab=false] - move focus to the tab before closing */ - close(index: Number|Element): void; + close(index: Number|Element, focusTab: Boolean = false): void; /** * @description delete a particular tab and its corresponding panel (if deletable) - * @param {Number|Element} index: tab index, or tab element + * @param {Number|Element} index - tab index, or tab element */ delete(index: Number|Element): void; diff --git a/examples/index.html b/examples/index.html index 101ed9a..fe1b623 100644 --- a/examples/index.html +++ b/examples/index.html @@ -7,7 +7,7 @@ Aria Tablist examples - +
@@ -159,7 +173,8 @@

Aria Tablist examples
- Dependency-free plain JavaScript module for WCAG - compliant tablists- Also great for + accordions.

@@ -218,10 +233,15 @@

Horizontal tabs

<div id="apple">...</div> <div id="orange">...</div> <div id="pear">...</div> - + +<script> + new AriaTablist(document.getElementById('fruits')); +</script>
-

Vertical tabs

+

Vertical tabs, all focusable

+

This example makes for a good accordion...

Apple
@@ -261,7 +281,10 @@

Vertical tabs

@@ -275,20 +298,96 @@ 

Vertical tabs

<div id="pear">Pear</div> <div aria-labelledby="pear">...</div> <div> -
+ +<script> + new AriaTablist(document.getElementById('fruits'), { + focusableTabs: true + }); +</script>

- Deletable, multi-selectable tabs, with second and - third tab active by default + Horizontal tabs that activate as you navigate + through them with arrow keypresses +

+
+
Apple
+
+ Orange +
+
+ Pear +
+
+
+ An apple is a sweet, edible fruit produced by an + apple tree (Malus domestica). Apple trees are + cultivated worldwide and are the most widely grown + species in the genus Malus. The tree originated in + Central Asia, where its wild ancestor, Malus + sieversii, is still found today. Apples have been + grown for thousands of years in Asia and Europe and + were brought to North America by European colonists. + Apples have religious and mythological significance + in many cultures, including Norse, Greek and + European Christian tradition. +
+
+ The orange is the fruit of the citrus species Citrus + × sinensis in the family Rutaceae, native to China. + It is also called sweet orange, to distinguish it + from the related Citrus × aurantium, referred to as + bitter orange. The sweet orange reproduces asexually + (apomixis through nucellar embryony); varieties of + sweet orange arise through mutations. +
+
+ The pear (/ˈpɛər/) tree and shrub are a species of + genus Pyrus /ˈpaɪrəs/, in the family Rosaceae, + bearing the pomaceous fruit of the same name. + Several species of pear are valued for their edible + fruit and juices while others are cultivated as + trees. +
+ +
+<div id="fruits">
+    <div aria-controls="apple">Apple</div>
+    <div aria-controls="orange">Orange</div>
+    <div aria-controls="pear">Pear</div>
+<div>
+
+<div id="apple">...</div>
+<div id="orange">...</div>
+<div id="pear">...</div>
+
+<script>
+    new AriaTablist(document.getElementById('fruits'), {
+        arrowActivation: true
+    });
+</script>
+
+
+

+ Vertical, all focusable, deletable, multi-selectable + tabs, with second and third tab active by default

-
Apple
-
+
Apple
+
An apple is a sweet, edible fruit produced by an apple tree (Malus domestica). Apple trees are cultivated worldwide and are the most widely @@ -303,12 +402,12 @@

Christian tradition.

Orange
-
+
The orange is the fruit of the citrus species Citrus × sinensis in the family Rutaceae, native to China. It is also called sweet orange, to @@ -319,12 +418,12 @@

orange arise through mutations.

Pear
-
+
The pear (/ˈpɛər/) tree and shrub are a species of genus Pyrus /ˈpaɪrəs/, in the family Rosaceae, bearing the pomaceous fruit of the @@ -335,9 +434,10 @@

@@ -355,10 +455,84 @@

<script> new AriaTablist(document.getElementById('fruits'), { - deletable: true + deletable: true, + focusableTabs: true }); -</script> - +</script> +

+
+

+ Horizontal tabs, with second tab open by default but + disabled +

+
+
Apple
+
+ Orange +
+
+ Pear +
+
+ An apple is a sweet, edible fruit produced by an + apple tree (Malus domestica). Apple trees are + cultivated worldwide and are the most widely + grown species in the genus Malus. The tree + originated in Central Asia, where its wild + ancestor, Malus sieversii, is still found today. + Apples have been grown for thousands of years in + Asia and Europe and were brought to North + America by European colonists. Apples have + religious and mythological significance in many + cultures, including Norse, Greek and European + Christian tradition. +
+
+ The orange is the fruit of the citrus species + Citrus × sinensis in the family Rutaceae, native + to China. It is also called sweet orange, to + distinguish it from the related Citrus × + aurantium, referred to as bitter orange. The + sweet orange reproduces asexually (apomixis + through nucellar embryony); varieties of sweet + orange arise through mutations. +
+
+ The pear (/ˈpɛər/) tree and shrub are a species + of genus Pyrus /ˈpaɪrəs/, in the family + Rosaceae, bearing the pomaceous fruit of the + same name. Several species of pear are valued + for their edible fruit and juices while others + are cultivated as trees. +
+
+ +
+<div id="fruits">
+    <div aria-controls="apple">Apple</div>
+    <div aria-controls="orange" aria-disabled="true" aria-selected="true">
+        Orange
+    </div>
+    <div aria-controls="pear">Pear</div>
+    
+    <div id="apple">...</div>
+    <div id="orange">...</div>
+    <div id="pear">...</div>
+<div>
+
+<script>
+    new AriaTablist(document.getElementById('fruits'));
+</script>
@@ -371,39 +545,43 @@

HTML requirements / Progressive enhancement

  1. The module will look for elements with - role="tab" set + role="tab" set.
  2. If none are found, all direct children will be - processed + processed.
  3. - For each tab, assumed or otherwise, the module - will check for a matching panel by: + For each assumed tab, the module + will check for a matching + tabpanel by:
    1. Checking for an aria-controls attribute on - the tab, and searching for an element - with a matching ID + the tab, and searching for + an element with a matching + id.
    2. If the tab has an id, searching for an element with an aria-labelledby attribute - that matches that id + that matches that id.
  4. - For any tabs without a matching panel, any - applicable ARIA attributes will be removed from - it + For any tabs that were processed where a + matching panel was not found, if they + have role="tab" set, the + role attribute will be removed to + prevent confusion to screen reader users.
  5. - For the tabs that have associated panels, all - relevant ARIA attributes will be set - automatically + The found tabs and associated panels will then + have the relevant role and + aria- attributes set automatically.

@@ -420,8 +598,8 @@

HTML requirements / Progressive enhancement

<div aria-labelledby="tab-1">...</div> <div aria-labelledby="tab-2">...</div> -<div aria-labelledby="tab-3">...</div> - +<div aria-labelledby="tab-3">...</div>

So if you need to cater for users without JavaScript, or if the JavaScript fails to load for @@ -434,16 +612,22 @@

HTML requirements / Progressive enhancement

indicating which tab should be active by default:

-<div id="tabs" role="tablist" aria-label="Tabs">
-    <div role="tab" tabindex="-1" aria-controls="panel-1" id="tab-1">Panel 1</div>
-    <div role="tab" tabindex="0" aria-selected="true" aria-controls="panel-2" id="tab-2">Panel 2</div>
-    <div role="tab" tabindex="-1" aria-controls="panel-3" id="tab-3">Panel 3</div>
+<div role="tablist" aria-label="Tabs" aria-orientation="horizontal">
+    <div role="tab" tabindex="-1" aria-controls="panel-1" id="tab-1">
+        Panel 1
+    </div>
+    <div role="tab" tabindex="0" aria-selected="true" aria-controls="panel-2" id="tab-2">
+        Panel 2
+    </div>
+    <div role="tab" tabindex="-1" aria-controls="panel-3" id="tab-3">
+        Panel 3
+    </div>
 <div>
 
 <div role="tabpanel" aria-labelledby="tab-1" hidden="hidden" id="panel-1">...</div>
 <div role="tabpanel" aria-labelledby="tab-2" id="panel-2">...</div>
-<div role="tabpanel" aria-labelledby="tab-3" hidden="hidden" id="panel-3">...</div>
-                        
+<div role="tabpanel" aria-labelledby="tab-3" hidden="hidden" id="panel-3">...</div>
@@ -455,7 +639,7 @@

Options

basic options are also available:

-
delay: Numer
+
delay: Number
Delay in milliseconds before showing tab(s) from user interaction
Default: 0 @@ -463,18 +647,45 @@

Options

deletable: Boolean
Allow tab deletion - can be overridden per tab - by setting `data-deletable="false"`
Default: + by setting data-deletable="false"
Default: + false +
+
focusableTabs: Boolean
+
+ Make all tabs focusable in the normal tabbing + order (by setting tabindex="0" on + them), instead of just 1
Default: + false +
+
focusablePanels: Boolean
+
+ Make all tab panels focusable in the normal + tabbing order (by setting + tabindex="0" on them)
Default: + true +
+
arrowActivation: Boolean
+
+ activate a tab when it receives focus from using + the arrow keys
Default: false
+
tabindex: Number
+
+ Value to use when setting tabs or panels to be + part of the page's tabbing order
Default: + 0 +
onOpen: Function
Callback each time a tab opens
Default: - (panel) => {} + (panel, tab) => {}
onClose: Function
Callback each time a tab closes
Default: - (panel) => {} + (panel, tab) => {}
onDelete: Function
@@ -525,15 +736,18 @@

API

Trigger a particular tab to open (by index, or - element) + element), even if disabled
close: - (index: Number|Element): void + (index: Number|Element, focusTab: Boolean = + false): void
Trigger a particular tab to close (by index, or - element) + element), even if disabled
delete: