diff --git a/aria-practices.html b/aria-practices.html index 7867293cf7..486d525334 100644 --- a/aria-practices.html +++ b/aria-practices.html @@ -2888,13 +2888,9 @@

Examples

A file selector tree that demonstrates how to explicitly define values for aria-level, aria-posinset and aria-setsize.
  • - Navigation Treeview Example Using Computed Properties: + Navigation Treeview Example: A tree that provides navigation to a set of web pages and demonstrates browser support for automatically computing aria-level, aria-posinset and aria-setsize based on DOM structure.
  • -
  • - Navigation Treeview Example Using Declared Properties: - A tree that provides navigation to a set of web pages and demonstrates how to explicitly define values for aria-level, aria-posinset and aria-setsize. -
  • diff --git a/cspell.json b/cspell.json index 7f63f4f0e0..a23c55293f 100644 --- a/cspell.json +++ b/cspell.json @@ -39,6 +39,7 @@ "comboboxes", "commenters", "contenteditable", + "contentinfos", "Cook'n", "Copernicium", "Coughlin", @@ -107,6 +108,7 @@ "Lewandowski", "listbox's", "Listboxes", + "listitems", "Livermorium", "Malo", "Manish", @@ -125,7 +127,7 @@ "Moscovium", "MSAA", "multithumb", - "Navs", + "navs", "Nemeth", "nightmode", "Nihonium", diff --git a/examples/index.html b/examples/index.html index 719a72a5e6..2874c16147 100644 --- a/examples/index.html +++ b/examples/index.html @@ -49,7 +49,12 @@

    Examples by Role

    banner - Banner Landmark + + + button @@ -91,7 +96,12 @@

    Examples by Role

    contentinfo - Contentinfo Landmark + + + dialog @@ -135,8 +145,7 @@

    Examples by Role

  • Checkbox (Two State)
  • Editor Menubar
  • Date Picker Spin Button
  • -
  • Navigation Treeview Using Computed Properties
  • -
  • Navigation Treeview Using Declared Properties
  • +
  • Navigation Treeview
  • @@ -211,7 +220,12 @@

    Examples by Role

    navigation - Navigation Landmark + + + none @@ -343,8 +357,7 @@

    Examples by Role

    @@ -358,8 +371,7 @@

    Examples by Role

    @@ -453,7 +465,12 @@

    Examples By Properties and States

    aria-current - Disclosure for Navigation Menus + + + aria-describedby @@ -502,8 +519,7 @@

    Examples By Properties and States

  • Treegrid Email Inbox
  • File Directory Treeview Using Computed Properties
  • File Directory Treeview Using Declared Properties
  • -
  • Navigation Treeview Using Computed Properties
  • -
  • Navigation Treeview Using Declared Properties
  • +
  • Navigation Treeview
  • @@ -571,8 +587,7 @@

    Examples By Properties and States

  • Treegrid Email Inbox
  • File Directory Treeview Using Computed Properties
  • File Directory Treeview Using Declared Properties
  • -
  • Navigation Treeview Using Computed Properties
  • -
  • Navigation Treeview Using Declared Properties
  • +
  • Navigation Treeview
  • @@ -604,8 +619,7 @@

    Examples By Properties and States

  • Tabs with Manual Activation
  • File Directory Treeview Using Computed Properties
  • File Directory Treeview Using Declared Properties
  • -
  • Navigation Treeview Using Computed Properties
  • -
  • Navigation Treeview Using Declared Properties
  • +
  • Navigation Treeview
  • Complementary Landmark
  • Form Landmark
  • Main Landmark
  • @@ -622,7 +636,6 @@

    Examples By Properties and States

  • Treegrid Email Inbox
  • File Directory Treeview Using Computed Properties
  • File Directory Treeview Using Declared Properties
  • -
  • Navigation Treeview Using Declared Properties
  • @@ -656,6 +669,10 @@

    Examples By Properties and States

    aria-orientation Slider with aria-orientation and aria-valuetext + + aria-owns + Navigation Treeview + aria-posinset @@ -664,7 +681,6 @@

    Examples By Properties and States

  • Treegrid Email Inbox
  • File Directory Treeview Using Computed Properties
  • File Directory Treeview Using Declared Properties
  • -
  • Navigation Treeview Using Declared Properties
  • @@ -727,7 +743,6 @@

    Examples By Properties and States

  • Treegrid Email Inbox
  • File Directory Treeview Using Computed Properties
  • File Directory Treeview Using Declared Properties
  • -
  • Navigation Treeview Using Declared Properties
  • diff --git a/examples/treeview/css/treeview-navigation.css b/examples/treeview/css/treeview-navigation.css new file mode 100644 index 0000000000..3114e26bd6 --- /dev/null +++ b/examples/treeview/css/treeview-navigation.css @@ -0,0 +1,160 @@ +.page header { + border: #005a9c solid 2px; + background: #005a9c; + color: white; + text-align: center; +} + +.page header .title { + font-size: 2.5em; + font-weight: bold; + font-family: serif; +} + +.page header .tagline { + font-style: italic; +} + +.page footer { + border: #005a9c solid 2px; + background: #005a9c; + font-family: serif; + color: white; + font-style: italic; + padding-left: 1em; +} + +.page .body { + display: grid; + grid-template-columns: auto auto; + border: #eee solid 2px; +} + +.page .body nav { + margin: 0; + padding: 6px; + width: 17em; + height: 60em; + background: #eee; +} + +.page .body nav.focus { + padding: 4px; + border: 2px solid #005a9c; +} + +.page .body .page { + margin: 0.25em; + padding: 0.25em; + height: 30em; +} + +.page .body .page h1 { + margin: 0; + padding: 0; +} + +.page .main { + padding: 1em; +} + +.treeview-navigation ul, +.treeview-navigation li { + margin: 0; + padding: 0; +} + +.treeview-navigation li li span.label { + padding-left: 1em; +} + +.treeview-navigation li li li span.label { + padding-left: 2em; +} + +.treeview-navigation[role="tree"] { + margin: 0; + padding: 0; + list-style: none; +} + +.treeview-navigation[role="tree"] li { + margin: 0; + padding: 0; + list-style: none; +} + +.treeview-navigation a[role="treeitem"] ul { + margin: 0; + padding: 0; +} + +.treeview-navigation + a[role="treeitem"][aria-expanded="false"] + + [role="group"] { + display: none; +} + +.treeview-navigation a[role="treeitem"][aria-expanded="true"] + [role="group"] { + display: block; +} + +.treeview-navigation a[role="treeitem"] > span svg { + transform: translate(0, 0); +} + +.treeview-navigation a[role="treeitem"][aria-expanded="false"] > span svg { + transform: rotate(270deg) translate(2px, 2px); +} + +.treeview-navigation a[role="treeitem"] { + margin: 0; + padding: 4px; + padding-left: 9px; + text-decoration: none; + color: #005a9c; + border: none; + display: block; +} + +.treeview-navigation a[role="treeitem"][aria-current] { + border-left: 5px solid #005a9c; + padding-left: 4px; + background-color: #ddd; +} + +.treeview-navigation a[role="treeitem"] span.icon svg polygon { + stroke-width: 2px; + fill: currentColor; + stroke: transparent; +} + +/* disable default keyboard focus styling for treeitems + Keyboard focus is styled with the following CSS */ + +.treeview-navigation a[role="treeitem"]:focus { + outline: 0; + padding: 2px; + padding-left: 7px; + border: 2px #005a9c solid; +} + +.treeview-navigation a[role="treeitem"][aria-current]:focus { + padding-left: 4px; + border-left-width: 5px; +} + +.treeview-navigation a[role="treeitem"]:hover { + background-color: #adddff; + text-decoration: underline; + padding-left: 4px; + border-left: 5px solid #333; +} + +.treeview-navigation a[role="treeitem"] span.icon:hover { + color: #333; +} + +.treeview-navigation a[role="treeitem"] span.icon svg polygon:hover { + stroke: currentColor; +} diff --git a/examples/treeview/images/aria-current.svg b/examples/treeview/images/aria-current.svg new file mode 100644 index 0000000000..c4f65c82dd --- /dev/null +++ b/examples/treeview/images/aria-current.svg @@ -0,0 +1,4 @@ + + + + diff --git a/examples/treeview/images/down-arrow-brown.png b/examples/treeview/images/down-arrow-brown.png new file mode 100644 index 0000000000..fe3e38aa31 Binary files /dev/null and b/examples/treeview/images/down-arrow-brown.png differ diff --git a/examples/treeview/images/right-arrow-brown.png b/examples/treeview/images/right-arrow-brown.png new file mode 100644 index 0000000000..9808455aea Binary files /dev/null and b/examples/treeview/images/right-arrow-brown.png differ diff --git a/examples/treeview/js/treeview-navigation.js b/examples/treeview/js/treeview-navigation.js new file mode 100644 index 0000000000..c46ee69f00 --- /dev/null +++ b/examples/treeview/js/treeview-navigation.js @@ -0,0 +1,505 @@ +/* + * This content is licensed according to the W3C Software License at + * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document + * + * File: treeview-navigation.js + * Desc: Tree item object for representing the state and user interactions for a + * tree widget for navigational links + */ + +'use strict'; + +class NavigationContentGenerator { + constructor(siteURL, siteName) { + this.siteName = siteName; + this.siteURL = siteURL; + this.fillerTextSentences = []; + + this.fillerTextSentences.push( + 'The content on this page is associated with the $linkName link for $siteName.' + ); + // this.fillerTextSentences.push('The text content in this paragraph is filler text providing a detectable change of content when the $linkName link is selected from the menu. '); + // this.fillerTextSentences.push('$siteName doesn\'t really exist, but the use of an organizational name is useful to provide context for the $linkName link. '); + // this.fillerTextSentences.push('Since $siteName doesn\'t exist there really is no real content associated with the $linkName link.'); + } + + renderParagraph(linkURL, linkName) { + var content = ''; + this.fillerTextSentences.forEach( + (s) => + (content += s + .replace('$siteName', this.siteName) + .replace('$siteURL', this.siteURL) + .replace('$linkName', linkName) + .replace('$linkURL', linkURL)) + ); + return content; + } +} + +class TreeViewNavigation { + constructor(node) { + var linkURL, linkTitle; + + // Check whether node is a DOM element + if (typeof node !== 'object') { + return; + } + + document.body.addEventListener('focusin', this.onBodyFocusin.bind(this)); + document.body.addEventListener('mousedown', this.onBodyFocusin.bind(this)); + + this.treeNode = node; + this.navNode = node.parentElement; + + this.treeitems = this.treeNode.querySelectorAll('[role="treeitem"]'); + for (let i = 0; i < this.treeitems.length; i++) { + let ti = this.treeitems[i]; + ti.addEventListener('keydown', this.onKeydown.bind(this)); + ti.addEventListener('click', this.onLinkClick.bind(this)); + // first tree item is in tab sequence of page + if (i == 0) { + ti.tabIndex = 0; + } else { + ti.tabIndex = -1; + } + var groupNode = this.getGroupNode(ti); + if (groupNode) { + var span = ti.querySelector('span.icon'); + span.addEventListener('click', this.onIconClick.bind(this)); + } + } + + // Initial content for page + if (location.href.split('#').length > 1) { + linkURL = location.href; + linkTitle = getLinkNameFromURL(location.href); + } else { + linkURL = location.href + '#home'; + linkTitle = 'Home'; + } + + this.contentGenerator = new NavigationContentGenerator( + '#home', + 'Mythical University' + ); + this.updateContent(linkURL, linkTitle, false); + + function getLinkNameFromURL(url) { + function capitalize(str) { + return str.charAt(0).toUpperCase() + str.slice(1); + } + + var name = url.split('#')[1]; + if (typeof name === 'string') { + name = name.split('-').map(capitalize).join(' '); + } else { + name = 'Home'; + } + return name; + } + } + + updateContent(linkURL, linkName, moveFocus) { + var h1Node, paraNodes; + + if (typeof moveFocus !== 'boolean') { + moveFocus = true; + } + + // Update content area + h1Node = document.querySelector('.page .main h1'); + if (h1Node) { + h1Node.textContent = linkName; + } + paraNodes = document.querySelectorAll('.page .main p'); + paraNodes.forEach( + (p) => + (p.innerHTML = this.contentGenerator.renderParagraph(linkURL, linkName)) + ); + + // move focus to the content region + if (moveFocus && h1Node) { + h1Node.tabIndex = -1; + h1Node.focus(); + } + + // Update aria-current + this.updateAriaCurrent(linkURL); + } + + getAriaCurrentURL() { + let url = false; + let node = this.treeNode.querySelector('[aria-current]'); + if (node) { + url = node.href; + } + return url; + } + + updateAriaCurrent(url) { + if (typeof url !== 'string') { + url = this.getAriaCurrentURL(); + } + + this.treeitems.forEach((item) => { + if (item.href === url) { + item.setAttribute('aria-current', 'page'); + // Make sure link is visible + this.showTreeitem(item); + this.setTabIndex(item); + } else { + item.removeAttribute('aria-current'); + } + }); + } + + showTreeitem(treeitem) { + var parentNode = this.getParentTreeitem(treeitem); + + while (parentNode) { + parentNode.setAttribute('aria-expanded', 'true'); + parentNode = this.getParentTreeitem(parentNode); + } + } + + setTabIndex(treeitem) { + this.treeitems.forEach((item) => (item.tabIndex = -1)); + treeitem.tabIndex = 0; + } + + getParentTreeitem(treeitem) { + var node = treeitem.parentNode; + + if (node) { + node = node.parentNode; + if (node) { + node = node.previousElementSibling; + if (node && node.getAttribute('role') === 'treeitem') { + return node; + } + } + } + return false; + } + + isVisible(treeitem) { + var flag = true; + if (this.isInSubtree(treeitem)) { + treeitem = this.getParentTreeitem(treeitem); + if (!treeitem || treeitem.getAttribute('aria-expanded') === 'false') { + return false; + } + } + return flag; + } + + isInSubtree(treeitem) { + if (treeitem.parentNode && treeitem.parentNode.parentNode) { + return treeitem.parentNode.parentNode.getAttribute('role') === 'group'; + } + return false; + } + + isExpandable(treeitem) { + return treeitem.hasAttribute('aria-expanded'); + } + + isExpanded(treeitem) { + return treeitem.getAttribute('aria-expanded') === 'true'; + } + + getGroupNode(treeitem) { + var groupNode = false; + var id = treeitem.getAttribute('aria-owns'); + if (id) { + groupNode = document.getElementById(id); + } + return groupNode; + } + + getVisibleTreeitems() { + var items = []; + for (var i = 0; i < this.treeitems.length; i++) { + var ti = this.treeitems[i]; + if (this.isVisible(ti)) { + items.push(ti); + } + } + return items; + } + + collapseTreeitem(treeitem) { + if (treeitem.getAttribute('aria-owns')) { + var groupNode = document.getElementById( + treeitem.getAttribute('aria-owns') + ); + if (groupNode) { + treeitem.setAttribute('aria-expanded', 'false'); + } + } + } + + expandTreeitem(treeitem) { + if (treeitem.getAttribute('aria-owns')) { + var groupNode = document.getElementById( + treeitem.getAttribute('aria-owns') + ); + if (groupNode) { + treeitem.setAttribute('aria-expanded', 'true'); + } + } + } + + expandAllSiblingTreeitems(treeitem) { + var parentNode = treeitem.parentNode.parentNode; + + if (parentNode) { + var siblingTreeitemNodes = parentNode.querySelectorAll( + ':scope > li > a[aria-expanded]' + ); + + for (var i = 0; i < siblingTreeitemNodes.length; i++) { + siblingTreeitemNodes[i].setAttribute('aria-expanded', 'true'); + } + } + } + + setFocusToTreeitem(treeitem) { + treeitem.focus(); + } + + setFocusToNextTreeitem(treeitem) { + var visibleTreeitems = this.getVisibleTreeitems(); + var nextItem = false; + + for (var i = visibleTreeitems.length - 1; i >= 0; i--) { + var ti = visibleTreeitems[i]; + if (ti === treeitem) { + break; + } + nextItem = ti; + } + if (nextItem) { + this.setFocusToTreeitem(nextItem); + } + } + + setFocusToPreviousTreeitem(treeitem) { + var visibleTreeitems = this.getVisibleTreeitems(); + var prevItem = false; + + for (var i = 0; i < visibleTreeitems.length; i++) { + var ti = visibleTreeitems[i]; + if (ti === treeitem) { + break; + } + prevItem = ti; + } + + if (prevItem) { + this.setFocusToTreeitem(prevItem); + } + } + + setFocusToParentTreeitem(treeitem) { + if (this.isInSubtree(treeitem)) { + var ti = treeitem.parentNode.parentNode.previousElementSibling; + this.setFocusToTreeitem(ti); + } + } + + setFocusByFirstCharacter(treeitem, char) { + var start, + i, + ti, + index = -1; + var visibleTreeitems = this.getVisibleTreeitems(); + char = char.toLowerCase(); + + // Get start index for search based on position of treeitem + start = visibleTreeitems.indexOf(treeitem) + 1; + if (start >= visibleTreeitems.length) { + start = 0; + } + + // Check remaining items in the tree + for (i = start; i < visibleTreeitems.length; i++) { + ti = visibleTreeitems[i]; + if (char === ti.textContent.trim()[0].toLowerCase()) { + index = i; + break; + } + } + + // If not found in remaining slots, check from beginning + if (index === -1) { + for (i = 0; i < start; i++) { + ti = visibleTreeitems[i]; + if (char === ti.textContent.trim()[0].toLowerCase()) { + index = i; + break; + } + } + } + + // If match was found... + if (index > -1) { + this.setFocusToTreeitem(visibleTreeitems[index]); + } + } + + // Event handlers + + onBodyFocusin(event) { + var tgt = event.target; + + if (this.treeNode.contains(tgt)) { + this.navNode.classList.add('focus'); + } else { + this.navNode.classList.remove('focus'); + this.updateAriaCurrent(); + } + } + + onIconClick(event) { + var tgt = event.currentTarget; + + if (this.isExpanded(tgt.parentNode.parentNode)) { + this.collapseTreeitem(tgt.parentNode.parentNode); + } else { + this.expandTreeitem(tgt.parentNode.parentNode); + } + + event.preventDefault(); + event.stopPropagation(); + } + + onLinkClick(event) { + var tgt = event.currentTarget; + this.updateContent(tgt.href, tgt.textContent.trim()); + + event.preventDefault(); + event.stopPropagation(); + } + + onKeydown(event) { + var tgt = event.currentTarget, + flag = false, + key = event.key; + + function isPrintableCharacter(str) { + return str.length === 1 && str.match(/\S/); + } + + if (event.altKey || event.ctrlKey || event.metaKey) { + return; + } + + if (event.shift) { + if ( + event.keyCode == this.keyCode.SPACE || + event.keyCode == this.keyCode.RETURN + ) { + event.stopPropagation(); + } else { + if (isPrintableCharacter(key)) { + if (key == '*') { + this.expandAllSiblingTreeitems(tgt); + flag = true; + } else { + this.setFocusByFirstCharacter(tgt, key); + } + } + } + } else { + switch (key) { + // NOTE: Return key is supported through the click event + case ' ': + this.updateContent(tgt.href, tgt.textContent.trim()); + flag = true; + break; + + case 'Up': + case 'ArrowUp': + this.setFocusToPreviousTreeitem(tgt); + flag = true; + break; + + case 'Down': + case 'ArrowDown': + this.setFocusToNextTreeitem(tgt); + flag = true; + break; + + case 'Right': + case 'ArrowRight': + if (this.isExpandable(tgt)) { + if (this.isExpanded(tgt)) { + this.setFocusToNextTreeitem(tgt); + } else { + this.expandTreeitem(tgt); + } + } + flag = true; + break; + + case 'Left': + case 'ArrowLeft': + if (this.isExpandable(tgt) && this.isExpanded(tgt)) { + this.collapseTreeitem(tgt); + flag = true; + } else { + if (this.isInSubtree(tgt)) { + this.setFocusToParentTreeitem(tgt); + flag = true; + } + } + break; + + case 'Home': + this.setFocusToTreeitem(this.treeitems[0]); + flag = true; + break; + + case 'End': + var visibleTreeitems = this.getVisibleTreeitems(); + this.setFocusToTreeitem( + visibleTreeitems[visibleTreeitems.length - 1] + ); + flag = true; + break; + + default: + if (isPrintableCharacter(key)) { + if (key == '*') { + this.expandAllSiblingTreeitems(tgt); + flag = true; + } else { + this.setFocusByFirstCharacter(tgt, key); + } + } + break; + } + } + + if (flag) { + event.stopPropagation(); + event.preventDefault(); + } + } +} + +/** + * ARIA Treeview example + * @function onload + * @desc after page has loaded initialize all treeitems based on the role=treeitem + */ + +window.addEventListener('load', function () { + var trees = document.querySelectorAll('nav [role="tree"]'); + + for (let i = 0; i < trees.length; i++) { + new TreeViewNavigation(trees[i]); + } +}); diff --git a/examples/treeview/treeview-1/treeview-1a.html b/examples/treeview/treeview-1/treeview-1a.html index e1f98dc6a0..ebdf02c22b 100644 --- a/examples/treeview/treeview-1/treeview-1a.html +++ b/examples/treeview/treeview-1/treeview-1a.html @@ -46,8 +46,7 @@

    File Directory Treeview Example Using Computed Properties

    Similar examples include:

    Example

    diff --git a/examples/treeview/treeview-1/treeview-1b.html b/examples/treeview/treeview-1/treeview-1b.html index e835593ed3..97c4cb7b38 100644 --- a/examples/treeview/treeview-1/treeview-1b.html +++ b/examples/treeview/treeview-1/treeview-1b.html @@ -40,8 +40,7 @@

    File Directory Treeview Example Using Declared Properties

    Similar examples include:

    Example

    diff --git a/examples/treeview/treeview-2/css/treeLinks.css b/examples/treeview/treeview-2/css/treeLinks.css deleted file mode 100644 index dc27306fa7..0000000000 --- a/examples/treeview/treeview-2/css/treeLinks.css +++ /dev/null @@ -1,68 +0,0 @@ -ul[role="tree"] { - margin: 0; - padding: 0; - list-style: none; -} - -ul[role="tree"] li { - margin: 0; - padding: 0; - list-style: none; -} - -ul[role="tree"] a { - text-decoration: underline; - border-color: transparent; -} - -[role="treeitem"] ul { - margin: 0; - padding: 0; - margin-left: 0.9em; -} - -[role="treeitem"][aria-expanded="false"] > ul { - display: none; -} - -[role="treeitem"][aria-expanded="true"] > ul { - display: block; -} - -[role="treeitem"][aria-expanded="false"] > span::before { - content: url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='utf-8'%3F%3E%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpolygon points='1 1, 1 11, 8 6' fill=' %23034575' stroke= '%23034575' /%3E%3C/svg%3E "); - position: relative; - left: -0.25em; -} - -[role="treeitem"][aria-expanded="true"] > span::before { - content: url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='utf-8'%3F%3E%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpolygon points='1 1, 11 1, 6 8' fill='%23034575' stroke= '%23034575' /%3E%3C/svg%3E "); - position: relative; - left: -0.25em; -} - -[role="treeitem"], -[role="treeitem"] span { - width: 16em; - margin: 0; - padding: 0.125em; - border: 2px transparent solid; - display: block; -} - -/* disable default keyboard focus styling for treeitems - Keyboard focus is styled with the following CSS */ -[role="treeitem"]:focus { - outline: 0; -} - -[role="treeitem"].focus, -[role="treeitem"] span.focus { - border-color: black; - background-color: #eee; -} - -[role="treeitem"].hover, -[role="treeitem"] span.hover { - background-color: #ddd; -} diff --git a/examples/treeview/treeview-2/js/treeLinks.js b/examples/treeview/treeview-2/js/treeLinks.js deleted file mode 100644 index 011d68ce21..0000000000 --- a/examples/treeview/treeview-2/js/treeLinks.js +++ /dev/null @@ -1,247 +0,0 @@ -/* - * This content is licensed according to the W3C Software License at - * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document - * - * File: TreeLinks.js - * - * Desc: Tree widget that implements ARIA Authoring Practices - * for a tree being used as a file viewer - */ - -/* global TreeitemLink */ - -'use strict'; - -/** - * ARIA Treeview example - * - * @function onload - * @description after page has loaded initialize all treeitems based on the role=treeitem - */ - -window.addEventListener('load', function () { - var trees = document.querySelectorAll('[role="tree"]'); - - for (var i = 0; i < trees.length; i++) { - var t = new TreeLinks(trees[i]); - t.init(); - } -}); - -/* - * @constructor - * - * @desc - * Tree item object for representing the state and user interactions for a - * tree widget - * - * @param node - * An element with the role=tree attribute - */ - -var TreeLinks = function (node) { - // Check whether node is a DOM element - if (typeof node !== 'object') { - return; - } - - this.domNode = node; - - this.treeitems = []; - this.firstChars = []; - - this.firstTreeitem = null; - this.lastTreeitem = null; -}; - -TreeLinks.prototype.init = function () { - function findTreeitems(node, tree, group) { - var elem = node.firstElementChild; - var ti = group; - - while (elem) { - if ( - (elem.tagName.toLowerCase() === 'li' && - elem.firstElementChild.tagName.toLowerCase() === 'span') || - elem.tagName.toLowerCase() === 'a' - ) { - ti = new TreeitemLink(elem, tree, group); - ti.init(); - tree.treeitems.push(ti); - tree.firstChars.push(ti.label.substring(0, 1).toLowerCase()); - } - - if (elem.firstElementChild) { - findTreeitems(elem, tree, ti); - } - - elem = elem.nextElementSibling; - } - } - - // initialize pop up menus - if (!this.domNode.getAttribute('role')) { - this.domNode.setAttribute('role', 'tree'); - } - - findTreeitems(this.domNode, this, false); - - this.updateVisibleTreeitems(); - - this.firstTreeitem.domNode.tabIndex = 0; -}; - -TreeLinks.prototype.setFocusToItem = function (treeitem) { - for (var i = 0; i < this.treeitems.length; i++) { - var ti = this.treeitems[i]; - - if (ti === treeitem) { - ti.domNode.tabIndex = 0; - ti.domNode.focus(); - } else { - ti.domNode.tabIndex = -1; - } - } -}; - -TreeLinks.prototype.setFocusToNextItem = function (currentItem) { - var nextItem = false; - - for (var i = this.treeitems.length - 1; i >= 0; i--) { - var ti = this.treeitems[i]; - if (ti === currentItem) { - break; - } - if (ti.isVisible) { - nextItem = ti; - } - } - - if (nextItem) { - this.setFocusToItem(nextItem); - } -}; - -TreeLinks.prototype.setFocusToPreviousItem = function (currentItem) { - var prevItem = false; - - for (var i = 0; i < this.treeitems.length; i++) { - var ti = this.treeitems[i]; - if (ti === currentItem) { - break; - } - if (ti.isVisible) { - prevItem = ti; - } - } - - if (prevItem) { - this.setFocusToItem(prevItem); - } -}; - -TreeLinks.prototype.setFocusToParentItem = function (currentItem) { - if (currentItem.groupTreeitem) { - this.setFocusToItem(currentItem.groupTreeitem); - } -}; - -TreeLinks.prototype.setFocusToFirstItem = function () { - this.setFocusToItem(this.firstTreeitem); -}; - -TreeLinks.prototype.setFocusToLastItem = function () { - this.setFocusToItem(this.lastTreeitem); -}; - -TreeLinks.prototype.expandTreeitem = function (currentItem) { - if (currentItem.isExpandable) { - currentItem.domNode.setAttribute('aria-expanded', true); - this.updateVisibleTreeitems(); - } -}; - -TreeLinks.prototype.expandAllSiblingItems = function (currentItem) { - for (var i = 0; i < this.treeitems.length; i++) { - var ti = this.treeitems[i]; - - if (ti.groupTreeitem === currentItem.groupTreeitem && ti.isExpandable) { - this.expandTreeitem(ti); - } - } -}; - -TreeLinks.prototype.collapseTreeitem = function (currentItem) { - var groupTreeitem = false; - - if (currentItem.isExpanded()) { - groupTreeitem = currentItem; - } else { - groupTreeitem = currentItem.groupTreeitem; - } - - if (groupTreeitem) { - groupTreeitem.domNode.setAttribute('aria-expanded', false); - this.updateVisibleTreeitems(); - this.setFocusToItem(groupTreeitem); - } -}; - -TreeLinks.prototype.updateVisibleTreeitems = function () { - this.firstTreeitem = this.treeitems[0]; - - for (var i = 0; i < this.treeitems.length; i++) { - var ti = this.treeitems[i]; - - var parent = ti.domNode.parentNode; - - ti.isVisible = true; - - while (parent && parent !== this.domNode) { - if (parent.getAttribute('aria-expanded') == 'false') { - ti.isVisible = false; - } - parent = parent.parentNode; - } - - if (ti.isVisible) { - this.lastTreeitem = ti; - } - } -}; - -TreeLinks.prototype.setFocusByFirstCharacter = function (currentItem, char) { - var start, index; - - char = char.toLowerCase(); - - // Get start index for search based on position of currentItem - start = this.treeitems.indexOf(currentItem) + 1; - if (start === this.treeitems.length) { - start = 0; - } - - // Check remaining slots in the menu - index = this.getIndexFirstChars(start, char); - - // If not found in remaining slots, check from beginning - if (index === -1) { - index = this.getIndexFirstChars(0, char); - } - - // If match was found... - if (index > -1) { - this.setFocusToItem(this.treeitems[index]); - } -}; - -TreeLinks.prototype.getIndexFirstChars = function (startIndex, char) { - for (var i = startIndex; i < this.firstChars.length; i++) { - if (this.treeitems[i].isVisible) { - if (char === this.firstChars[i]) { - return i; - } - } - } - return -1; -}; diff --git a/examples/treeview/treeview-2/js/treeitemLinks.js b/examples/treeview/treeview-2/js/treeitemLinks.js deleted file mode 100644 index 17471a7fa9..0000000000 --- a/examples/treeview/treeview-2/js/treeitemLinks.js +++ /dev/null @@ -1,265 +0,0 @@ -/* - * This content is licensed according to the W3C Software License at - * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document - * - * File: TreeitemLink.js - * - * Desc: Treeitem widget that implements ARIA Authoring Practices - * for a tree being used as a file viewer - */ - -'use strict'; - -/* - * @constructor - * - * @desc - * Treeitem object for representing the state and user interactions for a - * treeItem widget - * - * @param node - * An element with the role=tree attribute - */ - -var TreeitemLink = function (node, treeObj, group) { - // Check whether node is a DOM element - if (typeof node !== 'object') { - return; - } - - node.tabIndex = -1; - this.tree = treeObj; - this.groupTreeitem = group; - this.domNode = node; - this.label = node.textContent.trim(); - this.stopDefaultClick = false; - - if (node.getAttribute('aria-label')) { - this.label = node.getAttribute('aria-label').trim(); - } - - this.isExpandable = false; - this.isVisible = false; - this.inGroup = false; - - if (group) { - this.inGroup = true; - } - - var elem = node.firstElementChild; - - while (elem) { - if (elem.tagName.toLowerCase() == 'ul') { - elem.setAttribute('role', 'group'); - this.isExpandable = true; - break; - } - - elem = elem.nextElementSibling; - } - - this.keyCode = Object.freeze({ - RETURN: 13, - SPACE: 32, - PAGEUP: 33, - PAGEDOWN: 34, - END: 35, - HOME: 36, - LEFT: 37, - UP: 38, - RIGHT: 39, - DOWN: 40, - }); -}; - -TreeitemLink.prototype.init = function () { - this.domNode.tabIndex = -1; - - if (!this.domNode.getAttribute('role')) { - this.domNode.setAttribute('role', 'treeitem'); - } - - this.domNode.addEventListener('keydown', this.handleKeydown.bind(this)); - this.domNode.addEventListener('click', this.handleClick.bind(this)); - this.domNode.addEventListener('focus', this.handleFocus.bind(this)); - this.domNode.addEventListener('blur', this.handleBlur.bind(this)); - - if (this.isExpandable) { - this.domNode.firstElementChild.addEventListener( - 'mouseover', - this.handleMouseOver.bind(this) - ); - this.domNode.firstElementChild.addEventListener( - 'mouseout', - this.handleMouseOut.bind(this) - ); - } else { - this.domNode.addEventListener('mouseover', this.handleMouseOver.bind(this)); - this.domNode.addEventListener('mouseout', this.handleMouseOut.bind(this)); - } -}; - -TreeitemLink.prototype.isExpanded = function () { - if (this.isExpandable) { - return this.domNode.getAttribute('aria-expanded') === 'true'; - } - - return false; -}; - -/* EVENT HANDLERS */ - -TreeitemLink.prototype.handleKeydown = function (event) { - var flag = false, - char = event.key; - - function isPrintableCharacter(str) { - return str.length === 1 && str.match(/\S/); - } - - function printableCharacter(item) { - if (char == '*') { - item.tree.expandAllSiblingItems(item); - flag = true; - } else { - if (isPrintableCharacter(char)) { - item.tree.setFocusByFirstCharacter(item, char); - flag = true; - } - } - } - - this.stopDefaultClick = false; - - if (event.altKey || event.ctrlKey || event.metaKey) { - return; - } - - if (event.shift) { - if ( - event.keyCode == this.keyCode.SPACE || - event.keyCode == this.keyCode.RETURN - ) { - event.stopPropagation(); - this.stopDefaultClick = true; - } else { - if (isPrintableCharacter(char)) { - printableCharacter(this); - } - } - } else { - switch (event.keyCode) { - case this.keyCode.SPACE: - case this.keyCode.RETURN: - if (this.isExpandable) { - if (this.isExpanded()) { - this.tree.collapseTreeitem(this); - } else { - this.tree.expandTreeitem(this); - } - flag = true; - } else { - event.stopPropagation(); - this.stopDefaultClick = true; - } - break; - - case this.keyCode.UP: - this.tree.setFocusToPreviousItem(this); - flag = true; - break; - - case this.keyCode.DOWN: - this.tree.setFocusToNextItem(this); - flag = true; - break; - - case this.keyCode.RIGHT: - if (this.isExpandable) { - if (this.isExpanded()) { - this.tree.setFocusToNextItem(this); - } else { - this.tree.expandTreeitem(this); - } - } - flag = true; - break; - - case this.keyCode.LEFT: - if (this.isExpandable && this.isExpanded()) { - this.tree.collapseTreeitem(this); - flag = true; - } else { - if (this.inGroup) { - this.tree.setFocusToParentItem(this); - flag = true; - } - } - break; - - case this.keyCode.HOME: - this.tree.setFocusToFirstItem(); - flag = true; - break; - - case this.keyCode.END: - this.tree.setFocusToLastItem(); - flag = true; - break; - - default: - if (isPrintableCharacter(char)) { - printableCharacter(this); - } - break; - } - } - - if (flag) { - event.stopPropagation(); - event.preventDefault(); - } -}; - -TreeitemLink.prototype.handleClick = function (event) { - // only process click events that directly happened on this treeitem - if ( - event.target !== this.domNode && - event.target !== this.domNode.firstElementChild - ) { - return; - } - - if (this.isExpandable) { - if (this.isExpanded()) { - this.tree.collapseTreeitem(this); - } else { - this.tree.expandTreeitem(this); - } - event.stopPropagation(); - } -}; - -TreeitemLink.prototype.handleFocus = function () { - var node = this.domNode; - if (this.isExpandable) { - node = node.firstElementChild; - } - node.classList.add('focus'); -}; - -TreeitemLink.prototype.handleBlur = function () { - var node = this.domNode; - if (this.isExpandable) { - node = node.firstElementChild; - } - node.classList.remove('focus'); -}; - -TreeitemLink.prototype.handleMouseOver = function (event) { - event.currentTarget.classList.add('hover'); -}; - -TreeitemLink.prototype.handleMouseOut = function (event) { - event.currentTarget.classList.remove('hover'); -}; diff --git a/examples/treeview/treeview-2/treeview-2a.html b/examples/treeview/treeview-2/treeview-2a.html deleted file mode 100644 index 32ca3d980b..0000000000 --- a/examples/treeview/treeview-2/treeview-2a.html +++ /dev/null @@ -1,611 +0,0 @@ - - - - - Navigation Treeview Example Using Computed Properties | WAI-ARIA Authoring Practices 1.2 - - - - - - - - - - - - - - -
    -

    Navigation Treeview Example Using Computed Properties

    -

    - The below example demonstrates how the - Treeview Design Pattern - can be used to build a navigation tree for a set of hierarchically organized web pages. - In this example, the user can browse a set of pages about foods that is organized into categories. - Activating an item in the tree will open a page about the chosen food. -

    -

    - Since a tree item is the only kind of interactive element that can be contained in a tree, links to web pages in a navigation tree have the treeitem role. -

    -

    - This example relies on the browser to compute values for aria-setsize, aria-posinset, and aria-level. - The ARIA 1.0 specification for these properties states that browsers can, but are not required to, compute their values. - So, some browser and assistive technology combinations may not compute or report correct position and level information if it is not explicitly declared. - If testing reveals gaps in support for these properties, override automatic computation by explicitly declaring their values as demonstrated in the example of a - Navigation Treeview using declared properties. -

    -

    Similar examples include:

    - -
    -
    -

    Example

    -
    - - -
    -

    Foods

    - -
    - -
    - -
    -

    Accessibility Features

    -

    - To make the focus indicator easier to see, nodes in the tree have a custom focus and hover styling created using CSS focus and hover pseudo-classes. -

    -
    - -
    -

    Terms Used to Describe Trees

    -

    - A tree item that can be expanded to reveal child items is called a parent node. - It is a closed node when the children are hidden and an open node when it is expanded. - An end node does not have any children. - For a complete list of terms and definitions, see the - Treeview Design Pattern. -

    -
    - -
    -

    Keyboard Support

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    KeyFunction
    Enter
    or Space
    -
      -
    • Performs the default action (e.g. onclick event) for the focused node.
    • -
    • In this example, the default action is to activate the link, opening its target page.
    • -
    -
    Down arrow -
      -
    • Moves focus to the next node that is focusable without opening or closing a node.
    • -
    • If focus is on the last node, does nothing.
    • -
    -
    Up arrow -
      -
    • Moves focus to the previous node that is focusable without opening or closing a node.
    • -
    • If focus is on the first node, does nothing.
    • -
    -
    Right Arrow -
      -
    • When focus is on a closed node, opens the node; focus does not move.
    • -
    • When focus is on a open node, moves focus to the first child node.
    • -
    • When focus is on an end node, does nothing.
    • -
    -
    Left Arrow -
      -
    • When focus is on an open node, closes the node.
    • -
    • When focus is on a child node that is also either an end node or a closed node, moves focus to its parent node.
    • -
    • When focus is on a root node that is also either an end node or a closed node, does nothing.
    • -
    -
    HomeMoves focus to first node without opening or closing a node.
    EndMoves focus to the last node that can be focused without expanding any nodes that are closed.
    a-z, A-Z -
      -
    • Focus moves to the next node with a name that starts with the typed character.
    • -
    • Search wraps to first node if a matching name is not found among the nodes that follow the focused node.
    • -
    • Search ignores nodes that are descendants of closed nodes.
    • -
    -
    - * (asterisk) - -
      -
    • Expands all closed sibling nodes that are at the same level as the focused node.
    • -
    • Focus does not move.
    • -
    -
    -
    - -
    -

    Role, Property, State, and Tabindex Attributes

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    RoleAttributeElementUsage
    treeul -
      -
    • Identifies the ul element as a tree widget.
    • -
    • - Because focus movement in the tree is managed with a - roving tabindex, - the tree container does not need a tabindex attribute. -
    • -
    -
    aria-labelledby="IDREF"ulRefers to the heading element that contains the label that identifies the purpose of the tree.
    treeitemli
    or a
    -
      -
    • Identifies the element as a treeitem.
    • -
    • The role is set on either the li or on an a element contained in the li.
    • -
    • The role is set on the element that is interactive and focusable: -
        -
      • If the node is a parent node that does not contain a link, the li element has the treeitem role.
      • -
      • If the node is an end node that contains a link, the a element has the treeitem role.
      • -
      -
    • -
    -
    tabindex="-1"li
    or a
    -
      -
    • Makes the element with the treeitem role focusable without including it in the tab sequence of the page.
    • -
    • All treeitem elements are focusable, but only one is included in the tab sequence.
    • -
    -
    tabindex="0"li
    or a
    -
      -
    • Includes the element with the treeitem role in the tab sequence.
    • -
    • Only one treeitem in the tree has tabindex="0".
    • -
    • In this implementation, the first treeitem in the tree is included in the tab sequence when the page loads.
    • -
    • - When the user moves focus in the tree, the element included in the tab sequence changes to the element with focus as described in the section on - roving tabindex. -
    • -
    -
    aria-expanded="false"li -
      -
    • Applied only to treeitem elements that are parent nodes, i.e., they contain a ul with the group role.
    • -
    • Indicates the parent node is closed, i.e., the descendant elements are not visible.
    • -
    • The visual indication of the collapsed state is synchronized by a CSS attribute selector.
    • -
    -
    aria-expanded="true"li -
      -
    • Applied only to treeitem elements that are parent nodes, i.e., they contain a ul with the group role.
    • -
    • Indicates the parent node is open, i.e., the descendant elements are visible.
    • -
    • The visual indication of the expanded state is synchronized by a CSS attribute selector.
    • -
    -
    groupul -
      -
    • Identifies the ul element as a container of treeitem elements that form a branch of the tree.
    • -
    • The group is contained in the element that serves as the parent treeitem.
    • -
    • Browsers use the grouping to compute aria-level, aria-setsize and aria-posinset values for the nodes contained in the branch.
    • -
    -
    - none - - li - -
      -
    • Hides the implicit listitem role of the li element from assistive technologies.
    • -
    • A listitem is required to be contained by a list, but the containing element is no longer a list; it is a tree or a group.
    • -
    • Removing the listitem semantic from the browser's accessibility tree eliminates the potential for confusing rendering by assistive technologies.
    • -
    -
    -
    - -
    -

    Javascript and CSS Source Code

    - -
    - -
    -

    HTML Source Code

    - -
    - - - -
    -
    - - - diff --git a/examples/treeview/treeview-2/treeview-2b.html b/examples/treeview/treeview-2/treeview-2b.html deleted file mode 100644 index 5999adbd65..0000000000 --- a/examples/treeview/treeview-2/treeview-2b.html +++ /dev/null @@ -1,772 +0,0 @@ - - - - - Navigation Treeview Example Using Declared Properties | WAI-ARIA Authoring Practices 1.2 - - - - - - - - - - - - - - - -
    -

    Navigation Treeview Example Using Declared Properties

    -

    - The below example demonstrates how the - Treeview Design Pattern - can be used to build a navigation tree for a set of hierarchically organized web pages. - In this example, the user can browse a set of pages about foods that is organized into categories. - Activating an item in the tree will open a page about the chosen food. -

    -

    - Since a tree item is the only kind of interactive element that can be contained in a tree, links to web pages in a navigation tree have the treeitem role. -

    -

    - The code in this example explicitly declares values for aria-setsize, aria-posinset and aria-level, which overrides browser computation of values for these properties. - The ARIA 1.0 specification for these properties states that browsers can, but are not required to, compute these values. -

    -

    Similar examples include:

    - -
    -
    -

    Example

    -
    - -
    -

    Foods

    - -
    - -
    - -
    -

    Accessibility Features

    -

    - To make the focus indicator easier to see, nodes in the tree have a custom focus and hover styling created using CSS focus and hover pseudo-classes. -

    -
    - -
    -

    Terms Used to Describe Trees

    -

    - A tree item that can be expanded to reveal child items is called a parent node. - It is a closed node when the children are hidden and an open node when it is expanded. - An end node does not have any children. - For a complete list of terms and definitions, see the - Treeview Design Pattern. -

    -
    - -
    -

    Keyboard Support

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    KeyFunction
    Enter
    or Space
    -
      -
    • Performs the default action (e.g. onclick event) for the focused node.
    • -
    • In this example, the default action is to activate the link, opening its target page.
    • -
    -
    Down arrow -
      -
    • Moves focus to the next node that is focusable without opening or closing a node.
    • -
    • If focus is on the last node, does nothing.
    • -
    -
    Up arrow -
      -
    • Moves focus to the previous node that is focusable without opening or closing a node.
    • -
    • If focus is on the first node, does nothing.
    • -
    -
    Right Arrow -
      -
    • When focus is on a closed node, opens the node; focus does not move.
    • -
    • When focus is on a open node, moves focus to the first child node.
    • -
    • When focus is on an end node, does nothing.
    • -
    -
    Left Arrow -
      -
    • When focus is on an open node, closes the node.
    • -
    • When focus is on a child node that is also either an end node or a closed node, moves focus to its parent node.
    • -
    • When focus is on a root node that is also either an end node or a closed node, does nothing.
    • -
    -
    HomeMoves focus to first node without opening or closing a node.
    EndMoves focus to the last node that can be focused without expanding any nodes that are closed.
    a-z, A-Z -
      -
    • Focus moves to the next node with a name that starts with the typed character.
    • -
    • Search wraps to first node if a matching name is not found among the nodes that follow the focused node.
    • -
    • Search ignores nodes that are descendants of closed nodes.
    • -
    -
    - * (asterisk) - -
      -
    • Expands all closed sibling nodes that are at the same level as the focused node.
    • -
    • Focus does not move.
    • -
    -
    -
    - -
    -

    Role, Property, State, and Tabindex Attributes

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    RoleAttributeElementUsage
    treeul -
      -
    • Identifies the ul element as a tree widget.
    • -
    • - Because focus movement in the tree is managed with a - roving tabindex, - the tree container does not need a tabindex attribute. -
    • -
    -
    aria-labelledby="IDREF"ulRefers to the heading element that contains the label that identifies the purpose of the tree.
    treeitemli
    or a
    -
      -
    • Identifies the element as a treeitem.
    • -
    • The role is set on either the li or on an a element contained in the li.
    • -
    • The role is set on the element that is interactive and focusable: -
        -
      • If the node is a parent node that does not contain a link, the li element has the treeitem role.
      • -
      • If the node is an end node that contains a link, the a element has the treeitem role.
      • -
      -
    • -
    -
    tabindex="-1"li
    or a
    -
      -
    • Makes the element with the treeitem role focusable without including it in the tab sequence of the page.
    • -
    • All treeitem elements are focusable, but only one is included in the tab sequence.
    • -
    -
    tabindex="0"li
    or a
    -
      -
    • Includes the element with the treeitem role in the tab sequence.
    • -
    • Only one treeitem in the tree has tabindex="0".
    • -
    • In this implementation, the first treeitem in the tree is included in the tab sequence when the page loads.
    • -
    • - When the user moves focus in the tree, the element included in the tab sequence changes to the element with focus as described in the section on - roving tabindex. -
    • -
    -
    aria-expanded="false"li -
      -
    • Applied only to treeitem elements that are parent nodes, i.e., they contain a ul with the group role.
    • -
    • Indicates the parent node is closed, i.e., the descendant elements are not visible.
    • -
    • The visual indication of the collapsed state is synchronized by a CSS attribute selector.
    • -
    -
    aria-expanded="true"li -
      -
    • Applied only to treeitem elements that are parent nodes, i.e., they contain a ul with the group role.
    • -
    • Indicates the parent node is open, i.e., the descendant elements are visible.
    • -
    • The visual indication of the expanded state is synchronized by a CSS attribute selector.
    • -
    -
    aria-setsize="number"liDefines the number of treeitem elements in the set of treeitem elements that are in the same branch and at the same level within the hierarchy.
    aria-posinset="number"li -
      -
    • Defines the position of the element within the set of other treeitem elements that are in the same branch and at the same level within the hierarchy.
    • -
    • Counting is one-based, not zero-based.
    • -
    -
    aria-level="number"li -
      -
    • Defines the level of the treeitem in the hierarchical tree structure.
    • -
    • Counting is one-based.
    • -
    • Root treeitem elements have aria-level=1.
    • -
    -
    groupul -
      -
    • Identifies the ul element as a container of treeitem elements that form a branch of the tree.
    • -
    • The group is contained in the element that serves as the parent treeitem.
    • -
    • Browsers use the grouping to compute aria-level, aria-setsize and aria-posinset values for the nodes contained in the branch.
    • -
    -
    - none - - li - -
      -
    • Hides the implicit listitem role of the li element from assistive technologies.
    • -
    • A listitem is required to be contained by a list, but the containing element is no longer a list; it is a tree or a group.
    • -
    • Removing the listitem semantic from the browser's accessibility tree eliminates the potential for confusing rendering by assistive technologies.
    • -
    -
    -
    - -
    -

    Javascript and CSS Source Code

    - -
    - -
    -

    HTML Source Code

    - -
    - - - -
    -
    - - - diff --git a/examples/treeview/treeview-navigation.html b/examples/treeview/treeview-navigation.html new file mode 100644 index 0000000000..a50bc80e23 --- /dev/null +++ b/examples/treeview/treeview-navigation.html @@ -0,0 +1,742 @@ + + + + + Navigation Treeview Example | WAI-ARIA Authoring Practices 1.2 + + + + + + + + + + + + + +
    +

    Navigation Treeview Example

    +
    +

    CAUTION! Before considering use of the ARIA tree pattern for site navigation, it is important to understand:

    +
      +
    • Correct implementation of the tree role requires implementation of complex functionality that is not needed for typical site navigation that is styled to look like a tree with expandable sections.
    • +
    • A pattern more suited for typical site navigation with expandable groups of links is the disclosure pattern.
    • +
    +
    +

    + The below example demonstrates how the + Treeview Design Pattern + can be used to build a navigation tree for a set of hierarchically organized web pages. + It illustrates navigation of a mythical university web site that is comparable to the navigation illustrated in the Example of Disclosure for Navigation Menus. + As noted above, the disclosure pattern is better suited for most web sites because few sites need the additional keyboard functionality required to support the ARIA tree role. +

    +

    + This example relies on the browser to compute values for aria-setsize, aria-posinset, and aria-level. + The ARIA specification for these properties states that browsers can, but are not required to, compute their values. + So, some browser and assistive technology combinations may not compute or report correct position and level information if it is not explicitly declared. + If testing reveals gaps in support for these properties, override automatic computation by explicitly declaring their values as demonstrated in the example of a + File Directory Treeview Example Using Declared Properties. +

    +

    Similar examples include:

    + + +
    +
    +

    Example

    +
    + + + + +
    + +
    +

    Accessibility Features

    +

    Focus Movement After Content Load

    +

    + An important aspect of designing a navigation tree experience is the behavior of keyboard focus when an item in the tree is activated. + If activating a tree item changes content on the page without triggering a browser page load, i.e., works like typical single-page apps, the focus position after the content load significantly affects efficiency for keyboard and assistive technology users. + Accessible navigation trees typically implement one of the following two behaviors: +

    +
      +
    1. + Activating a tree item moves focus to the beginning of the new content, ideally a level one heading with content that matches the name of the tree item that was activated. + Focusing on the heading informs screen reader users that navigation is complete and confirms the destination. + This behavior is appropriate when common or important use case scenarios assume users want to start interacting with the loaded content after activating the tree item. + Note: Keyboard users will need to navigate back to the navigation tree to view other pages. + To optimize keyboard efficiency, design a layout that logically locates the tree immediately before the content display area in the Tab sequence. +
    2. +
    3. + Activating an item in the tree keeps focus on the activated item in the tree. + In this case, the movement of aria-current to the currently focused tree item informs screen reader users that navigation is complete and confirms the destination. + This behavior is appropriate when common or important use case scenarios assume users are likely to need to peruse content from multiple nodes in the tree before deciding to interact with the loaded content. + Note: screen reader users will need to navigate to the content to read it. + In some cases, it might be possible to help screen reader users more quickly perceive the nature of the loaded content without navigating to it by referencing a portion of the content with an aria-describedby attribute on the tree item. +
    4. +
    +

    The example on this page illustrates the first technique of focusing the level one heading in the newly loaded content.

    +

    Assistive Technology Support Features

    +
      +
    • Since the tree presents a site navigation system, it is wrapped in a navigation region implemented with a nav element that has an aria-label that matches the label on the tree.
    • +
    • To ensure assistive technology users can identify and easily locate the tree item associated with the currently displayed page: +
        +
      • The aria-current="page" attribute is applied to the item in the tree associated with the currently displayed page.
      • +
      • + The tree item with aria-current is also the only item with tabindex="0". + That is, when tabbing into the tree, focus always lands on the item representing the current page. +
      • +
      • When focus leaves the tree, if the item representing the current page is in a branch that has been collapsed, that branch is expanded to ensure the current page item is both visible and exposed as active to assistive technologies.
      • +
      +
    • +
    +

    Visual design and high contrast features

    +
      +
    • To help communicate that the arrow keys are available for navigation within the tree, a border is added to the tree container when focus is within the tree.
    • +
    • To support operating system high contrast settings: +
        +
      • + Because transparent borders are visible on some systems with operating system high contrast settings enabled, transparency cannot be used to create a visual difference between the element that is focused an other elements. + Instead of using transparency, the focused element has a thicker border and less padding. + When an element receives focus, its border changes from 1 to 3 pixels and padding is reduced by 2 pixels. + When an element loses focus, its border changes from 3 pixels to 1 and padding is increased by 2 pixels. +
      • +
      • + To ensure the arrow icons used to indicate the expanded or collapsed state have sufficient contrast with the background when high contrast settings invert colors, the CSS currentColor value for the fill and stroke properties of the SVG polygon element is used to synchronize the color with text content. + If specific colors are used to specify the fill and stroke properties, these colors will remain the same in high contrast mode, which could lead to insufficient contrast between the icon and the background or even make the icon invisible if its color matches the high contrast mode background. +
      • +
      +
    • +
    +
    + +
    +

    Terms Used to Describe Trees

    +

    + A tree item that can be expanded to reveal child items is called a parent node. + It is a closed node when the children are hidden and an open node when it is expanded. + An end node does not have any children. + For a complete list of terms and definitions, see the + Treeview Design Pattern. +

    +
    + +
    +

    Keyboard Support

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    KeyFunction
    Enter
    or Space
    +
      +
    • Performs the default action (e.g. onclick event) for the focused node which is to activate the link, opening its target page.
    • +
    • + Moves focus to the h1 element in the newly loaded content. + Note: Moving focus is optional behavior. Please read the above accessibility feature sections for details. +
    • +
    +
    Down arrow +
      +
    • Moves focus to the next node that is focusable without opening or closing a node.
    • +
    • If focus is on the last node, does nothing.
    • +
    +
    Up arrow +
      +
    • Moves focus to the previous node that is focusable without opening or closing a node.
    • +
    • If focus is on the first node, does nothing.
    • +
    +
    Right Arrow +
      +
    • When focus is on a closed node, opens the node; focus does not move.
    • +
    • When focus is on a open node, moves focus to the first child node.
    • +
    • When focus is on an end node, does nothing.
    • +
    +
    Left Arrow +
      +
    • When focus is on an open node, closes the node.
    • +
    • When focus is on a child node that is also either an end node or a closed node, moves focus to its parent node.
    • +
    • When focus is on a root node that is also either an end node or a closed node, does nothing.
    • +
    +
    HomeMoves focus to first node without opening or closing a node.
    EndMoves focus to the last node that can be focused without expanding any nodes that are closed.
    a-z, A-Z +
      +
    • Focus moves to the next node with a name that starts with the typed character.
    • +
    • Search wraps to first node if a matching name is not found among the nodes that follow the focused node.
    • +
    • Search ignores nodes that are descendants of closed nodes.
    • +
    +
    + * (asterisk) + +
      +
    • Expands all closed sibling nodes that are at the same level as the focused node.
    • +
    • Focus does not move.
    • +
    +
    +
    + +
    +

    Role, Property, State, and Tabindex Attributes

    + +

    Landmarks

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    RoleAttributeElementUsage
    bannerrole="banner"header +
      +
    • Identifies the common content found at the top of most pages in a website.
    • +
    • In a real web page the header element would typically not need role="banner" attribute to define the landmark, but in this example the header element is not in the proper scope of the body element to identify is as a banner landmark, so it must be explicitly identified using the role attribute.
    • +
    +
    navigationnav + Identifies the region containing the navigation tree. +
    aria-label="navigation label"nav + The aria-label attribute provides an accessible name for the navigation landmark. +
    regionsection +
      +
    • Identifies the main content region of the page in this example.
    • +
    • In a real web page, this region would typically be identified with main landmark, but since the page containing this example already has a main landmark, this section is identified using the region landmark.
    • +
    +
    aria-labelledby="IDREFs"section + The aria-labelledby attribute provides an accessible name for the region landmark by concatenating the website and page titles. +
    contentinforole="contentinfo"footer +
      +
    • Identifies the common content found at the bottom of most pages in a website.
    • +
    • In a real web page the footer element would typically not need role="contentinfo" attribute to define the landmark, but in this example the footer element is not in the proper scope of the body element to identify is as a contentinfo landmark, so it must be explicitly identified using the role attribute.
    • +
    +
    + +

    Tree

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    RoleAttributeElementUsage
    treeul +
      +
    • Identifies the ul element as a tree widget.
    • +
    • + Because focus movement in the tree is managed with a + roving tabindex, + the tree container does not need a tabindex attribute. +
    • +
    +
    aria-label="Mythical University"ulProvides an accessible name for the tree.
    treeitemaIdentifies the element as a treeitem.
    tabindex="-1"a +
      +
    • Makes the element with the treeitem role focusable without including it in the tab sequence of the page.
    • +
    • All treeitem elements are focusable, but only one is included in the tab sequence.
    • +
    +
    tabindex="0"a +
      +
    • Includes the element with the treeitem role in the tab sequence.
    • +
    • Only one treeitem in the tree has tabindex="0".
    • +
    • In this implementation tabindex="0" is always on the treeitem with aria-current="page". +
    • +
    +
    aria-current="page"a +
      +
    • Applied to the treeitem that is the link to the currently displayed page.
    • +
    • The visual indication of the treeitem with aria-current is a vertical bar to the left of the treeitem label.
    • +
    +
    aria-expanded="false"a +
      +
    • Applied to treeitem elements that are parent nodes, i.e., they have aria-owns referencing a ul with role group.
    • +
    • Indicates the parent node is closed, i.e., the descendant elements are not visible.
    • +
    • The visual indication of the collapsed state is synchronized by a CSS attribute selector.
    • +
    +
    aria-expanded="true"a +
      +
    • Applied to treeitem elements that are parent nodes, i.e., they have aria-owns referencing a ul with role group.
    • +
    • Indicates the parent node is open, i.e., the descendant elements are visible.
    • +
    • The visual indication of the expanded state is synchronized by a CSS attribute selector.
    • +
    +
    aria-owns="IDREF"aRefers to the element with role group that contains the set of child treeitem elements that belong to this parent treeitem.
    groupul +
      +
    • Identifies the ul element as a container of treeitem elements that form a branch of the tree.
    • +
    • The group is owned by the element that serves as the parent treeitem.
    • +
    • Browsers use the grouping to compute aria-level, aria-setsize and aria-posinset values for the nodes contained in the branch.
    • +
    +
    + none + + li + +
      +
    • Hides the implicit listitem role of the li element from assistive technologies.
    • +
    • A listitem is required to be contained by a list, but the containing element is no longer a list; it is a tree or a group.
    • +
    • Removing the listitem semantic from the browser's accessibility tree eliminates the potential for confusing rendering by assistive technologies.
    • +
    +
    +
    + +
    +

    Javascript and CSS Source Code

    + +
    + +
    +

    HTML Source Code

    + +
    + + + +
    +
    + + + diff --git a/test/tests/treeview_treeview-2a.js b/test/tests/treeview_treeview-2a.js deleted file mode 100644 index d42e1206f2..0000000000 --- a/test/tests/treeview_treeview-2a.js +++ /dev/null @@ -1,814 +0,0 @@ -const { ariaTest } = require('..'); -const { Key } = require('selenium-webdriver'); -const assertAttributeValues = require('../util/assertAttributeValues'); -const assertAriaLabelledby = require('../util/assertAriaLabelledby'); -const assertRovingTabindex = require('../util/assertRovingTabindex'); -const replaceExternalLink = require('../util/replaceExternalLink'); - -const exampleFile = 'treeview/treeview-2/treeview-2a.html'; - -const ex = { - treeSelector: '#ex1 [role="tree"]', - treeitemSelector: '#ex1 [role="treeitem"]', - groupSelector: '#ex1 [role="group"]', - folderSelector: '#ex1 [role="treeitem"][aria-expanded]', - topLevelFolderSelector: '#ex1 [role="tree"] > [role="treeitem"]', - nextLevelFolderSelector: '[role="group"] > [role="treeitem"][aria-expanded]', - linkSelector: '#ex1 a[role="treeitem"]', -}; - -const openAllFolders = async function (t) { - const closedFoldersSelector = ex.treeitemSelector + '[aria-expanded="false"]'; - let closedFolders = await t.context.queryElements(t, closedFoldersSelector); - - // Going through all closed folder elements in dom order will open parent - // folders first, therefore all child folders will be visible before clicked - for (let folder of closedFolders) { - await folder.click(); - } -}; - -const checkFocus = async function (t, selector, index) { - return t.context.session.executeScript( - function (/* selector, index*/) { - const [selector, index] = arguments; - const items = document.querySelectorAll(selector); - return items[index] === document.activeElement; - }, - selector, - index - ); -}; - -const checkFocusOnParentFolder = async function (t, el) { - return t.context.session.executeScript(function () { - const el = arguments[0]; - - // the element is a folder - if (el.hasAttribute('aria-expanded')) { - return ( - document.activeElement === - el.parentElement.closest('[role="treeitem"][aria-expanded]') - ); - } - // the element is a folder - else { - return ( - document.activeElement === - el.closest('[role="treeitem"][aria-expanded]') - ); - } - }, el); -}; - -const isTopLevelFolder = async function (t, el) { - return t.context.session.executeScript(function () { - const el = arguments[0]; - return el.parentElement.getAttribute('role') === 'tree'; - }, el); -}; - -const isFolderTreeitem = async function (el) { - return (await el.getTagName()) === 'li'; -}; - -const isOpenedFolderTreeitem = async function (el) { - return (await el.getAttribute('aria-expanded')) === 'true'; -}; - -const isClosedFolderTreeitem = async function (el) { - return (await el.getAttribute('aria-expanded')) === 'false'; -}; - -const hasAriaExpandedAttribute = async function (t, el) { - return t.context.session.executeScript(async function () { - const el = arguments[0]; - return el.hasAttribute('aria-expanded'); - }, el); -}; - -ariaTest('role="tree" on ul element', exampleFile, 'tree-role', async (t) => { - const trees = await t.context.queryElements(t, ex.treeSelector); - - t.is( - trees.length, - 1, - 'One "role=tree" element should be found by selector: ' + ex.treeSelector - ); - - t.is( - await trees[0].getTagName(), - 'ul', - 'role="tree" should be found on a "ul"' - ); -}); - -ariaTest( - 'aria-labelledby on role="tree" element', - exampleFile, - 'tree-aria-labelledby', - async (t) => { - await assertAriaLabelledby(t, ex.treeSelector); - } -); - -ariaTest( - 'role="treeitem" on "li" or "a" element', - exampleFile, - 'treeitem-role', - async (t) => { - // Get all the list items in the tree structure - const listItems = await t.context.queryElements(t, '#ex1 [role="tree"] li'); - - // Check the role "treeitem" is on the list item (in the case of a directory) or contained link - for (let item of listItems) { - const hasAriaExpanded = await hasAriaExpandedAttribute(t, item); - - // if "aria-expanded" is contained on the list item, it is a directory - if (hasAriaExpanded) { - t.is( - await item.getAttribute('role'), - 'treeitem', - 'role="treeitem" should be found on a "li" items that have attribute "aria-expanded"' - ); - } else { - const links = await t.context.queryElements(t, 'a', item); - t.is( - await links[0].getAttribute('role'), - 'treeitem', - 'role="treeitem" should be found on focusable "a" elements within tree structure' - ); - } - } - } -); - -ariaTest('role="none" on "li" element', exampleFile, 'none-role', async (t) => { - // Get all the list items in the tree structure - const listItems = await t.context.queryElements(t, '#ex1 [role="tree"] li'); - - for (let item of listItems) { - const hasAriaExpanded = await hasAriaExpandedAttribute(t, item); - - // if "aria-expanded" is not on the list item, it is a leaf node - if (!hasAriaExpanded) { - t.is( - await item.getAttribute('role'), - 'none', - 'role="none" should be found on a "li" items that do not have attribute "aria-expanded", or, are leaf nodes' - ); - } - } -}); - -ariaTest( - 'treeitem tabindex set by roving tabindex', - exampleFile, - 'treeitem-tabindex', - async (t) => { - await openAllFolders(t); - - await assertRovingTabindex(t, ex.treeitemSelector, Key.ARROW_DOWN); - } -); - -ariaTest( - 'aria-expanded attribute on treeitem matches dom', - exampleFile, - 'treeitem-aria-expanded', - async (t) => { - const folders = await t.context.queryElements(t, ex.folderSelector); - - for (let folder of folders) { - // If the folder is displayed - if (await folder.isDisplayed()) { - // By default, all folders will be closed - t.is(await folder.getAttribute('aria-expanded'), 'false'); - t.is( - await ( - await t.context.queryElement(t, '[role="treeitem"]', folder) - ).isDisplayed(), - false - ); - - // Send enter to the folder - await folder.sendKeys(Key.ENTER); - - // After click, it should be open - t.is(await folder.getAttribute('aria-expanded'), 'true'); - t.is( - await ( - await t.context.queryElement(t, '[role="treeitem"]', folder) - ).isDisplayed(), - true - ); - } - } - - for (let i = folders.length - 1; i >= 0; i--) { - // If the folder is displayed - if (await folders[i].isDisplayed()) { - const folderText = await folders[i].getText(); - - // Send enter to the folder - await folders[i].sendKeys(Key.ENTER); - - // After sending enter, it should be closed - t.is( - await folders[i].getAttribute('aria-expanded'), - 'false', - folderText - ); - t.is( - await ( - await t.context.queryElement(t, '[role="treeitem"]', folders[i]) - ).isDisplayed(), - false, - folderText - ); - } - } - } -); - -ariaTest( - 'role="group" on "ul" elements', - exampleFile, - 'group-role', - async (t) => { - const groups = await t.context.queryElements(t, ex.groupSelector); - - t.truthy( - groups.length, - 'role="group" elements should be found by selector: ' + ex.groupSelector - ); - - for (let group of groups) { - t.is( - await group.getTagName(), - 'ul', - 'role="group" should be found on a "ul"' - ); - } - } -); - -// Keys - -ariaTest( - 'Key enter opens folder and activates link', - exampleFile, - 'key-enter-or-space', - async (t) => { - let folders = await t.context.queryElements(t, ex.folderSelector); - - // Going through all closed folder elements in dom order will open parent - // folders first, therefore all child folders will be visible before sending "enter" - for (let folder of folders) { - await folder.sendKeys(Key.ENTER); - } - - // Assert that the attribute value "aria-expanded" on all folders is "true" - await assertAttributeValues(t, ex.folderSelector, 'aria-expanded', 'true'); - - // Update url to remove external reference for dependable testing - const newUrl = t.context.url + '#test-url-change'; - await replaceExternalLink(t, newUrl, ex.linkSelector, 0); - - // Test a leaf node - let leafNodes = await t.context.queryElements(t, ex.linkSelector); - await leafNodes[0].sendKeys(Key.ENTER); - - t.is( - await t.context.session.getCurrentUrl(), - newUrl, - 'ENTER key on first element found by selector "' + - ex.linkSelector + - '" should activate link.' - ); - } -); - -// This test fails due to bug #869. -ariaTest.failing( - 'Key space opens folder and activates link', - exampleFile, - 'key-enter-or-space', - async (t) => { - let folders = await t.context.queryElements(t, ex.folderSelector); - - // Going through all closed folder elements in dom order will open parent - // folders first, therefore all child folders will be visible before sending "enter" - for (let folder of folders) { - await folder.sendKeys(Key.SPACE); - } - - // Assert that the attribute value "aria-expanded" on all folders is "true" - await assertAttributeValues(t, ex.folderSelector, 'aria-expanded', 'true'); - - // Update url to remove external reference for dependable testing - const newUrl = t.context.url + '#test-url-change'; - await replaceExternalLink(t, newUrl, ex.linkSelector, 0); - - // Test a leaf node - let leafNodes = await t.context.queryElements(t, ex.linkSelector); - await leafNodes[0].sendKeys(Key.SPACE); - - t.is( - await t.context.session.getCurrentUrl(), - newUrl, - 'SPACE key on first element found by selector "' + - ex.linkSelector + - '" should activate link.' - ); - } -); - -ariaTest( - 'key down arrow moves focus', - exampleFile, - 'key-down-arrow', - async (t) => { - // Check that the down arrow does not open folders - const topLevelFolders = await t.context.queryElements( - t, - ex.topLevelFolderSelector - ); - - for (let i = 0; i < topLevelFolders.length; i++) { - await topLevelFolders[i].sendKeys(Key.ARROW_DOWN); - - // If we are on the last top level folder, the focus will not move - const nextIndex = i === topLevelFolders.length - 1 ? i : i + 1; - - t.true( - await checkFocus(t, ex.topLevelFolderSelector, nextIndex), - 'Sending key ARROW_DOWN to top level folder at index ' + - i + - ' will move focus to ' + - nextIndex - ); - - t.is( - await topLevelFolders[i].getAttribute('aria-expanded'), - 'false', - 'Sending key ARROW_DOWN to top level folder at index ' + - i + - ' should not expand the folder' - ); - } - - // Reload page - await t.context.session.get(await t.context.session.getCurrentUrl()); - - // Open all folders - await openAllFolders(t); - - const items = await t.context.queryElements(t, ex.treeitemSelector); - - for (let i = 0; i < items.length; i++) { - await items[i].sendKeys(Key.ARROW_DOWN); - - // If we are on the last item, the focus will not move - const nextIndex = i === items.length - 1 ? i : i + 1; - - t.true( - await checkFocus(t, ex.treeitemSelector, nextIndex), - 'Sending key ARROW_DOWN to folder/item at index ' + - i + - ' will move focus to ' + - nextIndex - ); - } - } -); - -ariaTest('key up arrow moves focus', exampleFile, 'key-up-arrow', async (t) => { - // Check that the down arrow does not open folders - const topLevelFolders = await t.context.queryElements( - t, - ex.topLevelFolderSelector - ); - - for (let i = topLevelFolders.length - 1; i >= 0; i--) { - await topLevelFolders[i].sendKeys(Key.ARROW_UP); - - // If we are on the last top level folder, the focus will not move - const nextIndex = i === 0 ? i : i - 1; - - t.true( - await checkFocus(t, ex.topLevelFolderSelector, nextIndex), - 'Sending key ARROW_UP to top level folder at index ' + - i + - ' will move focus to ' + - nextIndex - ); - - t.is( - await topLevelFolders[i].getAttribute('aria-expanded'), - 'false', - 'Sending key ARROW_UP to top level folder at index ' + - i + - ' should not expand the folder' - ); - } - - // Reload page - await t.context.session.get(await t.context.session.getCurrentUrl()); - - // Open all folders - await openAllFolders(t); - - const items = await t.context.queryElements(t, ex.treeitemSelector); - - for (let i = items.length - 1; i >= 0; i--) { - await items[i].sendKeys(Key.ARROW_UP); - - // If we are on the last item, the focus will not move - const nextIndex = i === 0 ? i : i - 1; - - t.true( - await checkFocus(t, ex.treeitemSelector, nextIndex), - 'Sending key ARROW_UP to folder/item at index ' + - i + - ' will move focus to ' + - nextIndex - ); - } -}); - -ariaTest( - 'key right arrow opens folders and moves focus', - exampleFile, - 'key-right-arrow', - async (t) => { - const items = await t.context.queryElements(t, ex.treeitemSelector); - - let i = 0; - while (i < items.length) { - const isFolder = await isFolderTreeitem(items[i]); - const isClosed = await isClosedFolderTreeitem(items[i]); - - await items[i].sendKeys(Key.ARROW_RIGHT); - - // If the item is a folder and it was originally closed - if (isFolder && isClosed) { - t.is( - await items[i].getAttribute('aria-expanded'), - 'true', - 'Sending key ARROW_RIGHT to folder at treeitem index ' + - i + - ' when the folder is closed should open the folder' - ); - - t.true( - await checkFocus(t, ex.treeitemSelector, i), - 'Sending key ARROW_RIGHT to folder at treeitem index ' + - i + - ' when the folder was closed should not move the focus' - ); - continue; - } - - // If the folder is an open folder, the focus will move - else if (isFolder) { - t.true( - await checkFocus(t, ex.treeitemSelector, i + 1), - 'Sending key ARROW_RIGHT to folder at treeitem index ' + - i + - ' should move focus to item ' + - (i + 1) - ); - } - - // If we are a link, the focus will not move - else { - t.true( - await checkFocus(t, ex.treeitemSelector, i), - 'Sending key ARROW_RIGHT to link item at treeitem index ' + - i + - ' should not move focus' - ); - } - i++; - } - } -); - -// This test fails due to bug #866. -ariaTest.failing( - 'key left arrow closes folders and moves focus', - exampleFile, - 'key-left-arrow', - async (t) => { - // Open all folders - await openAllFolders(t); - - const items = await t.context.queryElements(t, ex.treeitemSelector); - - let i = items.length - 1; - while (i > 0) { - const isFolder = await isFolderTreeitem(items[i]); - const isOpened = await isOpenedFolderTreeitem(items[i]); - const isTopLevel = isFolder ? await isTopLevelFolder(t, items[i]) : false; - - await items[i].sendKeys(Key.ARROW_LEFT); - - // If the item is a folder and the folder was opened, arrow will close folder - if (isFolder && isOpened) { - t.is( - await items[i].getAttribute('aria-expanded'), - 'false', - 'Sending key ARROW_LEFT to folder at treeitem index ' + - i + - ' when the folder is opened should close the folder' - ); - - t.true( - await checkFocus(t, ex.treeitemSelector, i), - 'Sending key ARROW_LEFT to folder at treeitem index ' + - i + - ' when the folder is opened should not move the focus' - ); - // Send one more arrow key to the folder that is now closed - continue; - } - - // If the item is a top level folder and closed, arrow will do nothing - else if (isTopLevel) { - t.true( - await checkFocus(t, ex.treeitemSelector, i), - 'Sending key ARROW_LEFT to link in top level folder at treeitem index ' + - i + - ' should not move focus' - ); - } - - // If the item is a link in folder, or a closed folder, arrow will move up a folder - else { - t.true( - await checkFocusOnParentFolder(t, items[i]), - 'Sending key ARROW_LEFT to link in folder at treeitem index ' + - i + - ' should move focus to parent folder' - ); - - t.is( - await items[i].isDisplayed(), - true, - 'Sending key ARROW_LEFT to link in folder at treeitem index ' + - i + - ' should not close the folder it is in' - ); - } - - i--; - } - } -); - -ariaTest('key home moves focus', exampleFile, 'key-home', async (t) => { - // Test that key "home" works when no folder is open - const topLevelFolders = await t.context.queryElements( - t, - ex.topLevelFolderSelector - ); - - for (let i = topLevelFolders.length - 1; i >= 0; i--) { - await topLevelFolders[i].sendKeys(Key.HOME); - - t.true( - await checkFocus(t, ex.topLevelFolderSelector, 0), - 'Sending key HOME to top level folder at index ' + - i + - ' should move focus to first top level folder' - ); - - t.is( - await topLevelFolders[i].getAttribute('aria-expanded'), - 'false', - 'Sending key HOME to top level folder at index ' + - i + - ' should not expand the folder' - ); - } - - // Reload page - await t.context.session.get(await t.context.session.getCurrentUrl()); - - // Open all folders - await openAllFolders(t); - - const items = await t.context.queryElements(t, ex.treeitemSelector); - - for (let i = items.length - 1; i >= 0; i--) { - await items[i].sendKeys(Key.HOME); - - t.true( - await checkFocus(t, ex.treeitemSelector, 0), - 'Sending key HOME to top level folder/item at index ' + - i + - ' will move focus to the first item' - ); - } -}); - -ariaTest('key end moves focus', exampleFile, 'key-end', async (t) => { - // Test that key "end" works when no folder is open - const topLevelFolders = await t.context.queryElements( - t, - ex.topLevelFolderSelector - ); - - for (let i = topLevelFolders.length - 1; i >= 0; i--) { - await topLevelFolders[i].sendKeys(Key.END); - - t.true( - await checkFocus( - t, - ex.topLevelFolderSelector, - topLevelFolders.length - 1 - ), - 'Sending key END to top level folder at index ' + - i + - ' should move focus to last top level folder' - ); - - t.is( - await topLevelFolders[i].getAttribute('aria-expanded'), - 'false', - 'Sending key END to top level folder at index ' + - i + - ' should not expand the folder' - ); - } - - // Reload page - await t.context.session.get(await t.context.session.getCurrentUrl()); - - // Open all folders - await openAllFolders(t); - - const items = await t.context.queryElements(t, ex.treeitemSelector); - - for (let i = items.length - 1; i >= 0; i--) { - await items[i].sendKeys(Key.END); - - t.true( - await checkFocus(t, ex.treeitemSelector, items.length - 1), - 'Sending key END to top level folder/item at index ' + - i + - ' will move focus to the last item in the last opened folder' - ); - } -}); - -ariaTest('characters move focus', exampleFile, 'key-character', async (t) => { - const charIndexTestClosed = [ - { sendChar: 'g', sendIndex: 0, endIndex: 2 }, - { sendChar: 'f', sendIndex: 2, endIndex: 0 }, - { sendChar: 'v', sendIndex: 0, endIndex: 1 }, - ]; - - const charIndexTestOpened = [ - { sendChar: 'a', sendIndex: 0, endIndex: 3 }, - { sendChar: 'a', sendIndex: 3, endIndex: 9 }, - { sendChar: 'v', sendIndex: 9, endIndex: 15 }, - { sendChar: 'v', sendIndex: 15, endIndex: 15 }, - { sendChar: 'i', sendIndex: 15, endIndex: 41 }, - { sendChar: 'o', sendIndex: 41, endIndex: 1 }, - ]; - - const topLevelFolders = await t.context.queryElements( - t, - ex.topLevelFolderSelector - ); - - for (let test of charIndexTestClosed) { - // Send character to treeitem - await topLevelFolders[test.sendIndex].sendKeys(test.sendChar); - - // Test that the focus switches to the appropriate item - t.true( - await checkFocus(t, ex.topLevelFolderSelector, test.endIndex), - 'Sending character ' + - test.sendChar + - ' to treeitem ' + - test.sendIndex + - ' should move the focus to treeitem ' + - test.endIndex - ); - - await assertAttributeValues( - t, - ex.topLevelFolderSelector, - 'aria-expanded', - 'false' - ); - } - - // Reload page - await t.context.session.get(await t.context.session.getCurrentUrl()); - - // Open all folders - await openAllFolders(t); - - const items = await t.context.queryElements(t, ex.treeitemSelector); - - for (let test of charIndexTestOpened) { - // Send character to treeitem - await items[test.sendIndex].sendKeys(test.sendChar); - - // Test that the focus switches to the appropriate treeitem - t.true( - await checkFocus(t, ex.treeitemSelector, test.endIndex), - 'Sending character ' + - test.sendChar + - ' to treeitem ' + - test.sendIndex + - ' should move the focus to treeitem ' + - test.endIndex - ); - } -}); - -ariaTest( - 'asterisk key opens folders', - exampleFile, - 'key-asterisk', - async (t) => { - /* Test that "*" ONLY opens all top level nodes and no other folders */ - - const topLevelFolders = await t.context.queryElements( - t, - ex.topLevelFolderSelector - ); - const nextLevelFolders = await t.context.queryElements( - t, - ex.nextLevelFolderSelector - ); - - // Send Key - await topLevelFolders[0].sendKeys('*'); - - await assertAttributeValues( - t, - ex.topLevelFolderSelector, - 'aria-expanded', - 'true' - ); - await assertAttributeValues( - t, - ex.nextLevelFolderSelector, - 'aria-expanded', - 'false' - ); - - /* Test that "*" ONLY opens sibling folders at that level */ - - // Send key - await nextLevelFolders[0].sendKeys('*'); - - // The subfolders of first top level folder should all be open - - const subFoldersOfFirstFolder = await t.context.queryElements( - t, - ex.nextLevelFolderSelector, - topLevelFolders[0] - ); - for (let el of subFoldersOfFirstFolder) { - t.true( - (await el.getAttribute('aria-expanded')) === 'true', - 'Subfolders under the first top level folder should all be opened after sending one "*" to subfolder under first top level folder' - ); - } - - // The subfolders of second top level folder should all be closed - - const subFoldersOfSecondFolder = await t.context.queryElements( - t, - ex.nextLevelFolderSelector, - topLevelFolders[1] - ); - for (let el of subFoldersOfSecondFolder) { - t.true( - (await el.getAttribute('aria-expanded')) === 'false', - 'Subfolders under the second top level folder should all be closed after sending one "*" to subfolder under first top level folder' - ); - } - - // The subfolders of third top level folder should all be closed - - const subFoldersOfThirdFolder = await t.context.queryElements( - t, - ex.nextLevelFolderSelector, - topLevelFolders[2] - ); - for (let el of subFoldersOfThirdFolder) { - t.true( - (await el.getAttribute('aria-expanded')) === 'false', - 'Subfolders under the third top level folder should all be closed after sending one "*" to subfolder under first top level folder' - ); - } - } -); diff --git a/test/tests/treeview_treeview-2b.js b/test/tests/treeview_treeview-2b.js deleted file mode 100644 index eeaa499509..0000000000 --- a/test/tests/treeview_treeview-2b.js +++ /dev/null @@ -1,977 +0,0 @@ -const { ariaTest } = require('..'); -const { By, Key } = require('selenium-webdriver'); -const assertAttributeValues = require('../util/assertAttributeValues'); -const assertAriaLabelledby = require('../util/assertAriaLabelledby'); -const assertRovingTabindex = require('../util/assertRovingTabindex'); -const replaceExternalLink = require('../util/replaceExternalLink'); - -const exampleFile = 'treeview/treeview-2/treeview-2b.html'; - -const ex = { - treeSelector: '#ex1 [role="tree"]', - treeitemSelector: '#ex1 [role="treeitem"]', - groupSelector: '#ex1 [role="group"]', - folderSelector: '#ex1 [role="treeitem"][aria-expanded]', - topLevelFolderSelector: '#ex1 [role="tree"] > [role="treeitem"]', - nextLevelFolderSelector: '[role="group"] > [role="treeitem"][aria-expanded]', - linkSelector: '#ex1 a[role="treeitem"]', - groupItemSelectors: { - 1: [ - // Top level folders - '[role="tree"]>[role="treeitem"]', - ], - 2: [ - // Content of first top level folder - '[role="tree"]>[role="treeitem"]:nth-of-type(1)>[role="group"]>li', - - // Content of second top level folder - '[role="tree"]>[role="treeitem"]:nth-of-type(2)>[role="group"]>li', - - // Content of third top level folder - '[role="tree"]>[role="treeitem"]:nth-of-type(3)>[role="group"]>li', - ], - - 3: [ - // Content of subfolders of first top level folder - '[role="tree"]>[role="treeitem"]:nth-of-type(1)>[role="group"]>[role="treeitem"]:nth-of-type(3) [role="treeitem"]', - '[role="tree"]>[role="treeitem"]:nth-of-type(1)>[role="group"]>[role="treeitem"]:nth-of-type(5) [role="treeitem"]', - - // Content of subfolders of second top level folder - '[role="tree"]>[role="treeitem"]:nth-of-type(2)>[role="group"]>[role="treeitem"]:nth-of-type(1) [role="treeitem"]', - '[role="tree"]>[role="treeitem"]:nth-of-type(2)>[role="group"]>[role="treeitem"]:nth-of-type(2) [role="treeitem"]', - '[role="tree"]>[role="treeitem"]:nth-of-type(2)>[role="group"]>[role="treeitem"]:nth-of-type(3) [role="treeitem"]', - - // Content of subfolders of third top level folder - '[role="tree"]>[role="treeitem"]:nth-of-type(3)>[role="group"]>[role="treeitem"]:nth-of-type(1) [role="treeitem"]', - '[role="tree"]>[role="treeitem"]:nth-of-type(3)>[role="group"]>[role="treeitem"]:nth-of-type(2) [role="treeitem"]', - '[role="tree"]>[role="treeitem"]:nth-of-type(3)>[role="group"]>[role="treeitem"]:nth-of-type(3) [role="treeitem"]', - ], - }, -}; - -const openAllFolders = async function (t) { - const closedFoldersSelector = ex.treeitemSelector + '[aria-expanded="false"]'; - let closedFolders = await t.context.queryElements(t, closedFoldersSelector); - - // Going through all closed folder elements in dom order will open parent - // folders first, therefore all child folders will be visible before clicked - for (let folder of closedFolders) { - await folder.click(); - } -}; - -const checkFocus = async function (t, selector, index) { - return t.context.session.executeScript( - function (/* selector, index*/) { - const [selector, index] = arguments; - const items = document.querySelectorAll(selector); - return items[index] === document.activeElement; - }, - selector, - index - ); -}; - -const checkFocusOnParentFolder = async function (t, el) { - return t.context.session.executeScript(function () { - const el = arguments[0]; - - // the element is a folder - if (el.hasAttribute('aria-expanded')) { - return ( - document.activeElement === - el.parentElement.closest('[role="treeitem"][aria-expanded]') - ); - } - // the element is a folder - else { - return ( - document.activeElement === - el.closest('[role="treeitem"][aria-expanded]') - ); - } - }, el); -}; - -const isTopLevelFolder = async function (t, el) { - return t.context.session.executeScript(function () { - const el = arguments[0]; - return el.parentElement.getAttribute('role') === 'tree'; - }, el); -}; - -const isFolderTreeitem = async function (el) { - return (await el.getTagName()) === 'li'; -}; - -const isOpenedFolderTreeitem = async function (el) { - return (await el.getAttribute('aria-expanded')) === 'true'; -}; - -const isClosedFolderTreeitem = async function (el) { - return (await el.getAttribute('aria-expanded')) === 'false'; -}; - -const hasAriaExpandedAttribute = async function (t, el) { - return t.context.session.executeScript(async function () { - const el = arguments[0]; - return el.hasAttribute('aria-expanded'); - }, el); -}; - -ariaTest('role="tree" on ul element', exampleFile, 'tree-role', async (t) => { - const trees = await t.context.queryElements(t, ex.treeSelector); - - t.is( - trees.length, - 1, - 'One "role=tree" element should be found by selector: ' + ex.treeSelector - ); - - t.is( - await trees[0].getTagName(), - 'ul', - 'role="tree" should be found on a "ul"' - ); -}); - -ariaTest( - 'aria-labelledby on role="tree" element', - exampleFile, - 'tree-aria-labelledby', - async (t) => { - await assertAriaLabelledby(t, ex.treeSelector); - } -); - -ariaTest( - 'role="treeitem" on "li" or "a" element', - exampleFile, - 'treeitem-role', - async (t) => { - // Get all the list items in the tree structure - const listItems = await t.context.queryElements(t, '#ex1 [role="tree"] li'); - - // Check the role "treeitem" is on the list item (in the case of a directory) or contained link - for (let item of listItems) { - const hasAriaExpanded = await hasAriaExpandedAttribute(t, item); - - // if "aria-expanded" is contained on the list item, it is a directory - if (hasAriaExpanded) { - t.is( - await item.getAttribute('role'), - 'treeitem', - 'role="treeitem" should be found on a "li" items that have attribute "aria-expanded"' - ); - } else { - const links = await t.context.queryElements(t, 'a', item); - t.is( - await links[0].getAttribute('role'), - 'treeitem', - 'role="treeitem" should be found on focusable "a" elements within tree structure' - ); - } - } - } -); - -ariaTest('role="none" on "li" element', exampleFile, 'none-role', async (t) => { - // Get all the list items in the tree structure - const listItems = await t.context.queryElements(t, '#ex1 [role="tree"] li'); - - for (let item of listItems) { - const hasAriaExpanded = await hasAriaExpandedAttribute(t, item); - - // if "aria-expanded" is not on the list item, it is a leaf node - if (!hasAriaExpanded) { - t.is( - await item.getAttribute('role'), - 'none', - 'role="none" should be found on a "li" items that do not have attribute "aria-expanded", or, are leaf nodes' - ); - } - } -}); - -ariaTest( - 'treeitem tabindex set by roving tabindex', - exampleFile, - 'treeitem-tabindex', - async (t) => { - await openAllFolders(t); - - await assertRovingTabindex(t, ex.treeitemSelector, Key.ARROW_DOWN); - } -); - -ariaTest( - 'aria-expanded attribute on treeitem matches dom', - exampleFile, - 'treeitem-aria-expanded', - async (t) => { - const folders = await t.context.queryElements(t, ex.folderSelector); - - for (let folder of folders) { - // If the folder is displayed - if (await folder.isDisplayed()) { - // By default, all folders will be closed - t.is(await folder.getAttribute('aria-expanded'), 'false'); - t.is( - await ( - await t.context.queryElement(t, '[role="treeitem"]', folder) - ).isDisplayed(), - false - ); - - // Send enter to the folder - await folder.sendKeys(Key.ENTER); - - // After click, it should be open - t.is(await folder.getAttribute('aria-expanded'), 'true'); - t.is( - await ( - await t.context.queryElement(t, '[role="treeitem"]', folder) - ).isDisplayed(), - true - ); - } - } - - for (let i = folders.length - 1; i >= 0; i--) { - // If the folder is displayed - if (await folders[i].isDisplayed()) { - const folderText = await folders[i].getText(); - - // Send enter to the folder - await folders[i].sendKeys(Key.ENTER); - - // After sending enter, it should be closed - t.is( - await folders[i].getAttribute('aria-expanded'), - 'false', - folderText - ); - t.is( - await ( - await t.context.queryElement(t, '[role="treeitem"]', folders[i]) - ).isDisplayed(), - false, - folderText - ); - } - } - } -); - -ariaTest( - '"aria-setsize" attribute on treeitem', - exampleFile, - 'treeitem-aria-setsize', - async (t) => { - for (const [, levelSelectors] of Object.entries(ex.groupItemSelectors)) { - for (const selector of levelSelectors) { - const items = await t.context.queryElements(t, selector); - const setsize = items.length; - - for (const item of items) { - // The item is a folder with "treeitem" role and "aria-setsize" set - if ((await item.getAttribute('role')) === 'treeitem') { - t.is( - await item.getAttribute('aria-setsize'), - setsize.toString(), - '"aria-setsize" attribute should be set to group size (' + - setsize + - ') in group "' + - selector + - '"' - ); - } - - // The item is a li that contains a link the "treeitem" role and "aria-setsize" set - else { - let treeitem = item.findElement(By.css('[role="treeitem"]')); - t.is( - await treeitem.getAttribute('aria-setsize'), - setsize.toString(), - '"aria-setsize" attribute should be set to group size (' + - setsize + - ') in group "' + - selector + - '"' - ); - } - } - } - } - } -); - -ariaTest( - '"aria-posinset" attribute on treeitem', - exampleFile, - 'treeitem-aria-posinset', - async (t) => { - for (const [, levelSelectors] of Object.entries(ex.groupItemSelectors)) { - for (const selector of levelSelectors) { - const items = await t.context.queryElements(t, selector); - let pos = 0; - - for (const item of items) { - pos++; - - // The item is a folder with "treeitem" role and "aria-posinset" set - if ((await item.getAttribute('role')) === 'treeitem') { - t.is( - await item.getAttribute('aria-posinset'), - pos.toString(), - '"aria-posinset" attribute should be set to "' + - pos + - '" for treeitem in group "' + - selector + - '"' - ); - } - - // The item is a li that contains a link the "treeitem" role and "aria-posinset" set - else { - let treeitem = item.findElement(By.css('[role="treeitem"]')); - t.is( - await treeitem.getAttribute('aria-posinset'), - pos.toString(), - '"aria-posinset" attribute should be set to "' + - pos + - '" for treeitem in group "' + - selector + - '"' - ); - } - } - } - } - } -); - -ariaTest( - '"aria-level" attribute on treeitem', - exampleFile, - 'treeitem-aria-level', - async (t) => { - for (const [level, levelSelectors] of Object.entries( - ex.groupItemSelectors - )) { - for (const selector of levelSelectors) { - const items = await t.context.queryElements(t, selector); - for (const item of items) { - // The item is a folder with "treeitem" role and "aria-level" set - if ((await item.getAttribute('role')) === 'treeitem') { - t.is( - await item.getAttribute('aria-level'), - level.toString(), - '"aria-level" attribute should be set to level "' + - level + - '" in group "' + - selector + - '"' - ); - } - - // The item is a li that contains a link the "treeitem" role and "aria-level" set - else { - let treeitem = item.findElement(By.css('[role="treeitem"]')); - t.is( - await treeitem.getAttribute('aria-level'), - level.toString(), - '"aria-level" attribute should be set to level "' + - level + - '" in group "' + - selector + - '"' - ); - } - } - } - } - } -); - -ariaTest( - 'role="group" on "ul" elements', - exampleFile, - 'group-role', - async (t) => { - const groups = await t.context.queryElements(t, ex.groupSelector); - - t.truthy( - groups.length, - 'role="group" elements should be found by selector: ' + ex.groupSelector - ); - - for (let group of groups) { - t.is( - await group.getTagName(), - 'ul', - 'role="group" should be found on a "ul"' - ); - } - } -); - -// Keys - -ariaTest( - 'Key enter opens folder and activates link', - exampleFile, - 'key-enter-or-space', - async (t) => { - let folders = await t.context.queryElements(t, ex.folderSelector); - - // Going through all closed folder elements in dom order will open parent - // folders first, therefore all child folders will be visible before sending "enter" - for (let folder of folders) { - await folder.sendKeys(Key.ENTER); - } - - // Assert that the attribute value "aria-expanded" on all folders is "true" - await assertAttributeValues(t, ex.folderSelector, 'aria-expanded', 'true'); - - // Update url to remove external reference for dependable testing - const newUrl = t.context.url + '#test-url-change'; - await replaceExternalLink(t, newUrl, ex.linkSelector, 0); - - // Test a leaf node - let leafNodes = await t.context.queryElements(t, ex.linkSelector); - await leafNodes[0].sendKeys(Key.ENTER); - - t.is( - await t.context.session.getCurrentUrl(), - newUrl, - 'ENTER key on first element found by selector "' + - ex.linkSelector + - '" should activate link.' - ); - } -); - -// This test fails due to bug #869. -ariaTest.failing( - 'Key space opens folder and activates link', - exampleFile, - 'key-enter-or-space', - async (t) => { - let folders = await t.context.queryElements(t, ex.folderSelector); - - // Going through all closed folder elements in dom order will open parent - // folders first, therefore all child folders will be visible before sending "space" - for (let folder of folders) { - await folder.sendKeys(Key.SPACE); - } - - // Assert that the attribute value "aria-expanded" on all folders is "true" - await assertAttributeValues(t, ex.folderSelector, 'aria-expanded', 'true'); - - // Update url to remove external reference for dependable testing - const newUrl = t.context.url + '#test-url-change'; - await replaceExternalLink(t, newUrl, ex.linkSelector, 0); - - // Test a leaf node - let leafNodes = await t.context.queryElements(t, ex.linkSelector); - await leafNodes[0].sendKeys(Key.SPACE); - - t.is( - await t.context.session.getCurrentUrl(), - newUrl, - 'SPACE key on first element found by selector "' + - ex.linkSelector + - '" should activate link.' - ); - } -); - -ariaTest( - 'key down arrow moves focus', - exampleFile, - 'key-down-arrow', - async (t) => { - // Check that the down arrow does not open folders - const topLevelFolders = await t.context.queryElements( - t, - ex.topLevelFolderSelector - ); - - for (let i = 0; i < topLevelFolders.length; i++) { - await topLevelFolders[i].sendKeys(Key.ARROW_DOWN); - - // If we are on the last top level folder, the focus will not move - const nextIndex = i === topLevelFolders.length - 1 ? i : i + 1; - - t.true( - await checkFocus(t, ex.topLevelFolderSelector, nextIndex), - 'Sending key ARROW_DOWN to top level folder at index ' + - i + - ' will move focus to ' + - nextIndex - ); - - t.is( - await topLevelFolders[i].getAttribute('aria-expanded'), - 'false', - 'Sending key ARROW_DOWN to top level folder at index ' + - i + - ' should not expand the folder' - ); - } - - // Reload page - await t.context.session.get(await t.context.session.getCurrentUrl()); - - // Open all folders - await openAllFolders(t); - - const items = await t.context.queryElements(t, ex.treeitemSelector); - - for (let i = 0; i < items.length; i++) { - await items[i].sendKeys(Key.ARROW_DOWN); - - // If we are on the last item, the focus will not move - const nextIndex = i === items.length - 1 ? i : i + 1; - - t.true( - await checkFocus(t, ex.treeitemSelector, nextIndex), - 'Sending key ARROW_DOWN to folder/item at index ' + - i + - ' will move focus to ' + - nextIndex - ); - } - } -); - -ariaTest('key up arrow moves focus', exampleFile, 'key-up-arrow', async (t) => { - // Check that the down arrow does not open folders - const topLevelFolders = await t.context.queryElements( - t, - ex.topLevelFolderSelector - ); - - for (let i = topLevelFolders.length - 1; i >= 0; i--) { - await topLevelFolders[i].sendKeys(Key.ARROW_UP); - - // If we are on the last top level folder, the focus will not move - const nextIndex = i === 0 ? i : i - 1; - - t.true( - await checkFocus(t, ex.topLevelFolderSelector, nextIndex), - 'Sending key ARROW_UP to top level folder at index ' + - i + - ' will move focus to ' + - nextIndex - ); - - t.is( - await topLevelFolders[i].getAttribute('aria-expanded'), - 'false', - 'Sending key ARROW_UP to top level folder at index ' + - i + - ' should not expand the folder' - ); - } - - // Reload page - await t.context.session.get(await t.context.session.getCurrentUrl()); - - // Open all folders - await openAllFolders(t); - - const items = await t.context.queryElements(t, ex.treeitemSelector); - - for (let i = items.length - 1; i >= 0; i--) { - await items[i].sendKeys(Key.ARROW_UP); - - // If we are on the last item, the focus will not move - const nextIndex = i === 0 ? i : i - 1; - - t.true( - await checkFocus(t, ex.treeitemSelector, nextIndex), - 'Sending key ARROW_UP to folder/item at index ' + - i + - ' will move focus to ' + - nextIndex - ); - } -}); - -ariaTest( - 'key right arrow opens folders and moves focus', - exampleFile, - 'key-right-arrow', - async (t) => { - const items = await t.context.queryElements(t, ex.treeitemSelector); - - let i = 0; - while (i < items.length) { - const isFolder = await isFolderTreeitem(items[i]); - const isClosed = await isClosedFolderTreeitem(items[i]); - - await items[i].sendKeys(Key.ARROW_RIGHT); - - // If the item is a folder and it was originally closed - if (isFolder && isClosed) { - t.is( - await items[i].getAttribute('aria-expanded'), - 'true', - 'Sending key ARROW_RIGHT to folder at treeitem index ' + - i + - ' when the folder is closed should open the folder' - ); - - t.true( - await checkFocus(t, ex.treeitemSelector, i), - 'Sending key ARROW_RIGHT to folder at treeitem index ' + - i + - ' when the folder was closed should not move the focus' - ); - continue; - } - - // If the folder is an open folder, the focus will move - else if (isFolder) { - t.true( - await checkFocus(t, ex.treeitemSelector, i + 1), - 'Sending key ARROW_RIGHT to folder at treeitem index ' + - i + - ' should move focus to item ' + - (i + 1) - ); - } - - // If we are a link, the focus will not move - else { - t.true( - await checkFocus(t, ex.treeitemSelector, i), - 'Sending key ARROW_RIGHT to link item at treeitem index ' + - i + - ' should not move focus' - ); - } - i++; - } - } -); - -// This test fails due to bug #866. -ariaTest.failing( - 'key left arrow closes folders and moves focus', - exampleFile, - 'key-left-arrow', - async (t) => { - // Open all folders - await openAllFolders(t); - - const items = await t.context.queryElements(t, ex.treeitemSelector); - - let i = items.length - 1; - while (i > 0) { - const isFolder = await isFolderTreeitem(items[i]); - const isOpened = await isOpenedFolderTreeitem(items[i]); - const isTopLevel = isFolder ? await isTopLevelFolder(t, items[i]) : false; - - await items[i].sendKeys(Key.ARROW_LEFT); - - // If the item is a folder and the folder was opened, arrow will close folder - if (isFolder && isOpened) { - t.is( - await items[i].getAttribute('aria-expanded'), - 'false', - 'Sending key ARROW_LEFT to folder at treeitem index ' + - i + - ' when the folder is opened should close the folder' - ); - - t.true( - await checkFocus(t, ex.treeitemSelector, i), - 'Sending key ARROW_LEFT to folder at treeitem index ' + - i + - ' when the folder is opened should not move the focus' - ); - // Send one more arrow key to the folder that is now closed - continue; - } - - // If the item is a top level folder and closed, arrow will do nothing - else if (isTopLevel) { - t.true( - await checkFocus(t, ex.treeitemSelector, i), - 'Sending key ARROW_LEFT to link in top level folder at treeitem index ' + - i + - ' should not move focus' - ); - } - - // If the item is a link in folder, or a closed folder, arrow will move up a folder - else { - t.true( - await checkFocusOnParentFolder(t, items[i]), - 'Sending key ARROW_LEFT to link in folder at treeitem index ' + - i + - ' should move focus to parent folder' - ); - - t.is( - await items[i].isDisplayed(), - true, - 'Sending key ARROW_LEFT to link in folder at treeitem index ' + - i + - ' should not close the folder it is in' - ); - } - - i--; - } - } -); - -ariaTest('key home moves focus', exampleFile, 'key-home', async (t) => { - // Test that key "home" works when no folder is open - const topLevelFolders = await t.context.queryElements( - t, - ex.topLevelFolderSelector - ); - - for (let i = topLevelFolders.length - 1; i >= 0; i--) { - await topLevelFolders[i].sendKeys(Key.HOME); - - t.true( - await checkFocus(t, ex.topLevelFolderSelector, 0), - 'Sending key HOME to top level folder at index ' + - i + - ' should move focus to first top level folder' - ); - - t.is( - await topLevelFolders[i].getAttribute('aria-expanded'), - 'false', - 'Sending key HOME to top level folder at index ' + - i + - ' should not expand the folder' - ); - } - - // Reload page - await t.context.session.get(await t.context.session.getCurrentUrl()); - - // Open all folders - await openAllFolders(t); - - const items = await t.context.queryElements(t, ex.treeitemSelector); - - for (let i = items.length - 1; i >= 0; i--) { - await items[i].sendKeys(Key.HOME); - - t.true( - await checkFocus(t, ex.treeitemSelector, 0), - 'Sending key HOME to top level folder/item at index ' + - i + - ' will move focus to the first item' - ); - } -}); - -ariaTest('key end moves focus', exampleFile, 'key-end', async (t) => { - // Test that key "end" works when no folder is open - const topLevelFolders = await t.context.queryElements( - t, - ex.topLevelFolderSelector - ); - - for (let i = topLevelFolders.length - 1; i >= 0; i--) { - await topLevelFolders[i].sendKeys(Key.END); - - t.true( - await checkFocus( - t, - ex.topLevelFolderSelector, - topLevelFolders.length - 1 - ), - 'Sending key END to top level folder at index ' + - i + - ' should move focus to last top level folder' - ); - - t.is( - await topLevelFolders[i].getAttribute('aria-expanded'), - 'false', - 'Sending key END to top level folder at index ' + - i + - ' should not expand the folder' - ); - } - - // Reload page - await t.context.session.get(await t.context.session.getCurrentUrl()); - - // Open all folders - await openAllFolders(t); - - const items = await t.context.queryElements(t, ex.treeitemSelector); - - for (let i = items.length - 1; i >= 0; i--) { - await items[i].sendKeys(Key.END); - - t.true( - await checkFocus(t, ex.treeitemSelector, items.length - 1), - 'Sending key END to top level folder/item at index ' + - i + - ' will move focus to the last item in the last opened folder' - ); - } -}); - -ariaTest('characters move focus', exampleFile, 'key-character', async (t) => { - const charIndexTestClosed = [ - { sendChar: 'g', sendIndex: 0, endIndex: 2 }, - { sendChar: 'f', sendIndex: 2, endIndex: 0 }, - { sendChar: 'v', sendIndex: 0, endIndex: 1 }, - ]; - - const charIndexTestOpened = [ - { sendChar: 'a', sendIndex: 0, endIndex: 3 }, - { sendChar: 'a', sendIndex: 3, endIndex: 9 }, - { sendChar: 'v', sendIndex: 9, endIndex: 15 }, - { sendChar: 'v', sendIndex: 15, endIndex: 15 }, - { sendChar: 'i', sendIndex: 15, endIndex: 41 }, - { sendChar: 'o', sendIndex: 41, endIndex: 1 }, - ]; - - const topLevelFolders = await t.context.queryElements( - t, - ex.topLevelFolderSelector - ); - - for (let test of charIndexTestClosed) { - // Send character to treeitem - await topLevelFolders[test.sendIndex].sendKeys(test.sendChar); - - // Test that the focus switches to the appropriate item - t.true( - await checkFocus(t, ex.topLevelFolderSelector, test.endIndex), - 'Sending character ' + - test.sendChar + - ' to treeitem ' + - test.sendIndex + - ' should move the focus to treeitem ' + - test.endIndex - ); - - await assertAttributeValues( - t, - ex.topLevelFolderSelector, - 'aria-expanded', - 'false' - ); - } - - // Reload page - await t.context.session.get(await t.context.session.getCurrentUrl()); - - // Open all folders - await openAllFolders(t); - - const items = await t.context.queryElements(t, ex.treeitemSelector); - - for (let test of charIndexTestOpened) { - // Send character to treeitem - await items[test.sendIndex].sendKeys(test.sendChar); - - // Test that the focus switches to the appropriate treeitem - t.true( - await checkFocus(t, ex.treeitemSelector, test.endIndex), - 'Sending character ' + - test.sendChar + - ' to treeitem ' + - test.sendIndex + - ' should move the focus to treeitem ' + - test.endIndex - ); - } -}); - -ariaTest( - 'asterisk key opens folders', - exampleFile, - 'key-asterisk', - async (t) => { - /* Test that "*" ONLY opens all top level nodes and no other folders */ - - const topLevelFolders = await t.context.queryElements( - t, - ex.topLevelFolderSelector - ); - const nextLevelFolders = await t.context.queryElements( - t, - ex.nextLevelFolderSelector - ); - - // Send Key - await topLevelFolders[0].sendKeys('*'); - - await assertAttributeValues( - t, - ex.topLevelFolderSelector, - 'aria-expanded', - 'true' - ); - await assertAttributeValues( - t, - ex.nextLevelFolderSelector, - 'aria-expanded', - 'false' - ); - - /* Test that "*" ONLY opens sibling folders at that level */ - - // Send key - await nextLevelFolders[0].sendKeys('*'); - - // The subfolders of first top level folder should all be open - - const subFoldersOfFirstFolder = await t.context.queryElements( - t, - ex.nextLevelFolderSelector, - topLevelFolders[0] - ); - for (let el of subFoldersOfFirstFolder) { - t.true( - (await el.getAttribute('aria-expanded')) === 'true', - 'Subfolders under the first top level folder should all be opened after sending one "*" to subfolder under first top level folder' - ); - } - - // The subfolders of second top level folder should all be closed - - const subFoldersOfSecondFolder = await t.context.queryElements( - t, - ex.nextLevelFolderSelector, - topLevelFolders[1] - ); - for (let el of subFoldersOfSecondFolder) { - t.true( - (await el.getAttribute('aria-expanded')) === 'false', - 'Subfolders under the second top level folder should all be closed after sending one "*" to subfolder under first top level folder' - ); - } - - // The subfolders of third top level folder should all be closed - - const subFoldersOfThirdFolder = await t.context.queryElements( - t, - ex.nextLevelFolderSelector, - topLevelFolders[2] - ); - for (let el of subFoldersOfThirdFolder) { - t.true( - (await el.getAttribute('aria-expanded')) === 'false', - 'Subfolders under the third top level folder should all be closed after sending one "*" to subfolder under first top level folder' - ); - } - } -); diff --git a/test/tests/treeview_treeview-navigation.js b/test/tests/treeview_treeview-navigation.js new file mode 100644 index 0000000000..0cc8b5e325 --- /dev/null +++ b/test/tests/treeview_treeview-navigation.js @@ -0,0 +1,984 @@ +const { ariaTest } = require('..'); +const { Key } = require('selenium-webdriver'); +const assertAttributeValues = require('../util/assertAttributeValues'); +const assertAriaLabelExists = require('../util/assertAriaLabelExists'); +const assertAriaLabelledby = require('../util/assertAriaLabelledby'); +const assertRovingTabindex = require('../util/assertRovingTabindex'); +const assertAriaOwns = require('../util/assertAriaOwns'); + +const exampleFile = 'treeview/treeview-navigation.html'; + +const ex = { + bannerSelector: '#ex1 header', + navigationSelector: '#ex1 nav', + regionSelector: '#ex1 section', + contentinfoSelector: '#ex1 footer', + treeSelector: '#ex1 [role="tree"]', + treeitemSelector: '#ex1 [role="treeitem"]', + tabIndexZeroSelector: '#ex1 [tabindex="0"]', + groupSelector: '#ex1 [role="group"]', + expandableSelector: '#ex1 [role="treeitem"][aria-expanded]', + topLevelTreeitemsSelector: '#ex1 [role="tree"] > li > [role="treeitem"]', + nextLevelTreeitemsSelector: + '#ex1 [role="tree"] > li > [role="group"] > li > [role="treeitem"]', + topLevelExpandableTreeitemsSelector: + '#ex1 [role="tree"] > li > [role="treeitem"][aria-expanded]', + nextLevelExpandableTreeitemsSelector: + '#ex1 [role="tree"] > li > [role="group"] > li > [role="treeitem"][aria-expanded]', + aboutChildTreeitemsSelector: '#id-about-subtree > li > [role="treeitem"]', + aboutChildExpandableTreeitemsSelector: + '#id-about-subtree > li > [role="treeitem"][aria-expanded]', + admissionsChildTreeitemsSelector: + '#id-admissions-subtree > li > [role="treeitem"]', + admissionsChildExpandableTreeitemsSelector: + '#id-admissions-subtree > li > [role="treeitem"][aria-expanded]', + linkSelector: '#ex1 a[role="treeitem"]', + h1Selector: '#ex1 .page h1', +}; + +const openAllExpandableTreeitems = async function (t) { + const closedExpandableTreeitemsSelector = + ex.treeitemSelector + '[aria-expanded="false"] span.icon'; + let closedTreeitems = await t.context.queryElements( + t, + closedExpandableTreeitemsSelector + ); + + // Going through all closed expandable tree elements in dom order will open parent + // treeitems first, therefore all child treeitems will be visible before clicked + for (let treeitem of closedTreeitems) { + await treeitem.click(); + } +}; + +const closeAllTopLevelExpandableTreeitems = async function (t) { + const openExpandableTreeitemsSelector = + ex.topLevelExpandableTreeitemsSelector + '[aria-expanded="true"] span.icon'; + + let openTreeitems = await t.context.queryElements( + t, + openExpandableTreeitemsSelector + ); + + // Going through all open expandable tree elements in dom order will close parent + // treeitems first, therefore all child treeitems will be invisible + for (let treeitem of openTreeitems) { + await treeitem.click(); + } +}; + +const checkFocus = async function (t, selector, index) { + return t.context.session.executeScript( + function (/* selector, index*/) { + const [selector, index] = arguments; + const items = document.querySelectorAll(selector); + return items[index] === document.activeElement; + }, + selector, + index + ); +}; + +const checkFocusOnParentTreeitem = async function (t, el) { + return t.context.session.executeScript(function () { + const el = arguments[0]; + return ( + document.activeElement === + el.parentElement.parentElement.previousElementSibling + ); + }, el); +}; + +const isTopLevelTreeitem = async function (t, el) { + return t.context.session.executeScript(function () { + const el = arguments[0]; + return el.parentElement.parentElement.getAttribute('role') === 'tree'; + }, el); +}; + +const getOwnedElement = async function (t, el) { + return t.context.session.executeScript(function () { + const el = arguments[0]; + return document.getElementById(el.getAttribute('aria-owns')); + }, el); +}; + +const isExpandableTreeitem = async function (el) { + return typeof (await el.getAttribute('aria-owns')) === 'string'; +}; + +const isOpenedExpandableTreeitem = async function (el) { + return (await el.getAttribute('aria-expanded')) === 'true'; +}; + +const isClosedExpandableTreeitem = async function (el) { + return (await el.getAttribute('aria-expanded')) === 'false'; +}; + +// Tests for landmark roles in example + +ariaTest( + 'role="banner" on header element', + exampleFile, + 'banner-role', + async (t) => { + const banners = await t.context.queryElements(t, ex.bannerSelector); + + t.is( + banners.length, + 1, + 'One "role=banner" element should be found by selector: ' + + ex.bannerSelector + ); + + t.is( + await banners[0].getTagName(), + 'header', + 'role="banner" should be found on a "header"' + ); + } +); + +ariaTest( + 'nav element identifies navigation landmark', + exampleFile, + 'navigation-role', + async (t) => { + const navs = await t.context.queryElements(t, ex.navigationSelector); + + t.is( + navs.length, + 1, + 'One nav element should be found by selector: ' + ex.navigationSelector + ); + } +); + +ariaTest( + 'aria-label on nav element', + exampleFile, + 'navigation-aria-label', + async (t) => { + await assertAriaLabelExists(t, ex.navigationSelector); + } +); + +ariaTest( + 'section element identifies region landmark', + exampleFile, + 'region-role', + async (t) => { + const regions = await t.context.queryElements(t, ex.regionSelector); + + t.is( + regions.length, + 1, + 'One section element should be found by selector: ' + ex.regionSelector + ); + } +); + +ariaTest( + 'aria-labelledby on section element', + exampleFile, + 'region-aria-labelledby', + async (t) => { + await assertAriaLabelledby(t, ex.regionSelector); + } +); + +ariaTest( + 'role="contentinfo" on footer element', + exampleFile, + 'contentinfo-role', + async (t) => { + const contentinfos = await t.context.queryElements( + t, + ex.contentinfoSelector + ); + + t.is( + contentinfos.length, + 1, + 'One "role=contentinfo" element should be found by selector: ' + + ex.contentinfoSelector + ); + + t.is( + await contentinfos[0].getTagName(), + 'footer', + 'role="contentinfo" should be found on a "footer"' + ); + } +); + +// Tests for treeview widget roles, properties and states + +ariaTest('role="tree" on ul element', exampleFile, 'tree-role', async (t) => { + const trees = await t.context.queryElements(t, ex.treeSelector); + + t.is( + trees.length, + 1, + 'One "role=tree" element should be found by selector: ' + ex.treeSelector + ); + + t.is( + await trees[0].getTagName(), + 'ul', + 'role="tree" should be found on a "ul"' + ); +}); + +ariaTest( + 'aria-label on role="tree" element', + exampleFile, + 'tree-aria-label', + async (t) => { + await assertAriaLabelExists(t, ex.treeSelector); + } +); + +ariaTest( + 'role="treeitem" on "a" element', + exampleFile, + 'treeitem-role', + async (t) => { + // Get all the list items in the tree structure + const listitems = await t.context.queryElements(t, '#ex1 [role="tree"] a'); + + // Check the role "treeitem" is on each a element + for (let item of listitems) { + t.is( + await item.getAttribute('role'), + 'treeitem', + 'role="treeitem" should be found on a "a" items that have attribute "aria-expanded"' + ); + } + } +); + +// We do not want roving tabindex anymore +ariaTest.failing( + 'treeitem tabindex set by roving tabindex', + exampleFile, + 'treeitem-tabindex', + async (t) => { + await openAllExpandableTreeitems(t); + await assertRovingTabindex(t, ex.treeitemSelector, Key.ARROW_DOWN); + } +); + +ariaTest( + 'treeitem aria-current="page" on item with tabindex="0"', + exampleFile, + 'treeitem-aria-current', + async (t) => { + await assertAttributeValues( + t, + ex.tabIndexZeroSelector, + 'aria-current', + 'page' + ); + } +); + +ariaTest( + 'treeitem aria-current="page" is visible after focus leaves treeview widget', + exampleFile, + 'treeitem-aria-current', + async (t) => { + const nextLevelExpandableTreeitems = await t.context.queryElements( + t, + ex.nextLevelExpandableTreeitemsSelector + ); + + const h1Element = await t.context.queryElement(t, ex.h1Selector); + + for (let i = 0; i < nextLevelExpandableTreeitems.length; i++) { + // select activate link to move aria-current to a lower level page + await openAllExpandableTreeitems(t); + await nextLevelExpandableTreeitems[i].sendKeys(Key.ENTER); + + // Close parent treeview items so link with aria-current is not visible + await closeAllTopLevelExpandableTreeitems(t); + + // Move focus to the main content area + await h1Element.click(); + + // Check is menuitem with aria-current is visible + + t.true( + await nextLevelExpandableTreeitems[i].isDisplayed(), + 'After moving focus the link index ' + + i + + ' with aria-current should be visible.' + ); + } + } +); + +ariaTest( + 'aria-expanded attribute on treeitem matches dom', + exampleFile, + 'treeitem-aria-expanded', + async (t) => { + const expandableTreeitems = await t.context.queryElements( + t, + ex.expandableSelector + ); + + for (let treeitem of expandableTreeitems) { + // If the treeitem is displayed + if (await treeitem.isDisplayed()) { + // By default, all expandable treeitems will be closed + t.is(await treeitem.getAttribute('aria-expanded'), 'false'); + t.is(await (await getOwnedElement(t, treeitem)).isDisplayed(), false); + + // Send enter to the treeitem + await treeitem.sendKeys(Key.ARROW_RIGHT); + + // After keypress, it should be open + t.is(await treeitem.getAttribute('aria-expanded'), 'true'); + t.is(await (await getOwnedElement(t, treeitem)).isDisplayed(), true); + } + } + + for (let i = expandableTreeitems.length - 1; i >= 0; i--) { + // If the treeitem is displayed + if (await expandableTreeitems[i].isDisplayed()) { + const treeitemText = await expandableTreeitems[i].getText(); + + // Send enter to the expandable treeitem + await expandableTreeitems[i].sendKeys(Key.ARROW_LEFT); + + // After sending enter, it should be closed + t.is( + await expandableTreeitems[i].getAttribute('aria-expanded'), + 'false', + treeitemText + ); + t.is( + await ( + await getOwnedElement(t, expandableTreeitems[i]) + ).isDisplayed(), + false, + treeitemText + ); + } + } + } +); + +ariaTest( + 'aria-owns on expandable treeitems', + exampleFile, + 'treeitem-aria-owns', + async (t) => { + await assertAriaOwns(t, ex.expandableSelector); + } +); + +ariaTest( + 'role="group" on "ul" elements', + exampleFile, + 'group-role', + async (t) => { + const groups = await t.context.queryElements(t, ex.groupSelector); + + t.truthy( + groups.length, + 'role="group" elements should be found by selector: ' + ex.groupSelector + ); + + for (let group of groups) { + t.is( + await group.getTagName(), + 'ul', + 'role="group" should be found on a "ul"' + ); + } + } +); + +ariaTest('role="none" on "li" element', exampleFile, 'none-role', async (t) => { + // Get all the list items in the tree structure + const listitems = await t.context.queryElements(t, '#ex1 [role="tree"] li'); + + for (let item of listitems) { + t.is( + await item.getAttribute('role'), + 'none', + 'role="none" should be found on a "li" items' + ); + } +}); + +// Keys + +ariaTest( + 'Key enter changes title', + exampleFile, + 'key-enter-or-space', + async (t) => { + await openAllExpandableTreeitems(t); + + const items = await t.context.queryElements(t, ex.treeitemSelector); + + for (let item of items) { + const itemText = await item.getText(); + + // Send enter to the treeitem + await item.sendKeys(Key.ENTER); + + const h1Element = await t.context.queryElement(t, ex.h1Selector); + const h1Text = await h1Element.getText(); + + t.is( + h1Text, + itemText, + 'Sending ENTER key to link "' + + itemText + + '" should be the h1 element content' + ); + } + } +); + +ariaTest( + 'Key space opens treeitem', + exampleFile, + 'key-enter-or-space', + async (t) => { + await openAllExpandableTreeitems(t); + + const items = await t.context.queryElements(t, ex.treeitemSelector); + + for (let item of items) { + const itemText = await item.getText(); + + // Send enter to the treeitem + await item.sendKeys(Key.ENTER); + + const h1Element = await t.context.queryElement(t, ex.h1Selector); + const h1Text = await h1Element.getText(); + + t.is( + h1Text, + itemText, + 'Sending SPACE key to link "' + + itemText + + '" should be the h1 element content' + ); + } + } +); + +ariaTest( + 'key down arrow moves focus', + exampleFile, + 'key-down-arrow', + async (t) => { + // Check that the down arrow does not open treeitems + const topLevelTreeitems = await t.context.queryElements( + t, + ex.topLevelTreeitemsSelector + ); + + for (let i = 0; i < topLevelTreeitems.length; i++) { + await topLevelTreeitems[i].sendKeys(Key.ARROW_DOWN); + + // If we are on the last top level treeitem, the focus will not move + const nextIndex = i === topLevelTreeitems.length - 1 ? i : i + 1; + + t.true( + await checkFocus(t, ex.topLevelTreeitemsSelector, nextIndex), + 'Sending key ARROW_DOWN to top level treeitem at index ' + + i + + ' will move focus to ' + + nextIndex + ); + + const isExpandable = await topLevelTreeitems[i].getAttribute('aria-owns'); + + if (isExpandable) { + const expandedState = await topLevelTreeitems[i].getAttribute( + 'aria-expanded' + ); + + t.is( + expandedState, + 'false', + 'Sending key ARROW_DOWN to top level treeitem at index ' + + i + + ' should not expand the treeitem' + ); + } + } + + // Reload page + await t.context.session.get(t.context.url); + + // Open all treeitems + await openAllExpandableTreeitems(t); + + const items = await t.context.queryElements(t, ex.treeitemSelector); + + for (let i = 0; i < items.length; i++) { + await items[i].sendKeys(Key.ARROW_DOWN); + + // If we are on the last item, the focus will not move + const nextIndex = i === items.length - 1 ? i : i + 1; + + t.true( + await checkFocus(t, ex.treeitemSelector, nextIndex), + 'Sending key ARROW_DOWN to treeitem at index ' + + i + + ' will move focus to ' + + nextIndex + ); + } + } +); + +ariaTest('key up arrow moves focus', exampleFile, 'key-up-arrow', async (t) => { + // Check that the up arrow does not open treeitems + const topLevelTreeitems = await t.context.queryElements( + t, + ex.topLevelTreeitemsSelector + ); + + for (let i = topLevelTreeitems.length - 1; i >= 0; i--) { + await topLevelTreeitems[i].sendKeys(Key.ARROW_UP); + + // If we are on the last top level treeitem, the focus will not move + const nextIndex = i === 0 ? i : i - 1; + + t.true( + await checkFocus(t, ex.topLevelTreeitemsSelector, nextIndex), + 'Sending key ARROW_UP to top level treeitem at index ' + + i + + ' will move focus to ' + + nextIndex + ); + + const isExpandable = await topLevelTreeitems[i].getAttribute('aria-owns'); + + if (isExpandable) { + t.is( + await topLevelTreeitems[i].getAttribute('aria-expanded'), + 'false', + 'Sending key ARROW_UP to top level expandable treeitem at index ' + + i + + ' should not expand the treeitem' + ); + } + } + + // Reload page + await t.context.session.get(t.context.url); + + // Open all treeitems + await openAllExpandableTreeitems(t); + + const items = await t.context.queryElements(t, ex.treeitemSelector); + + for (let i = items.length - 1; i >= 0; i--) { + await items[i].sendKeys(Key.ARROW_UP); + + // If we are on the last item, the focus will not move + const nextIndex = i === 0 ? i : i - 1; + + t.true( + await checkFocus(t, ex.treeitemSelector, nextIndex), + 'Sending key ARROW_UP to item at index ' + + i + + ' will move focus to ' + + nextIndex + ); + } +}); + +ariaTest( + 'key right arrow opens treeitems and moves focus', + exampleFile, + 'key-right-arrow', + async (t) => { + const items = await t.context.queryElements(t, ex.treeitemSelector); + + let i = 0; + while (i < items.length) { + const isExpandable = await isExpandableTreeitem(items[i]); + const isClosed = await isClosedExpandableTreeitem(items[i]); + + await items[i].sendKeys(Key.ARROW_RIGHT); + + // If the item is a treeitem and it was originally closed + if (isExpandable && isClosed) { + t.is( + await items[i].getAttribute('aria-expanded'), + 'true', + 'Sending key ARROW_RIGHT to treeitem at treeitem index ' + + i + + ' when the treeitem is closed should open the treeitem' + ); + + t.true( + await checkFocus(t, ex.treeitemSelector, i), + 'Sending key ARROW_RIGHT to treeitem at treeitem index ' + + i + + ' when the treeitem was closed should not move the focus' + ); + continue; + } + + // If the treeitem is an open treeitem, the focus will move + else if (isExpandable) { + t.true( + await checkFocus(t, ex.treeitemSelector, i + 1), + 'Sending key ARROW_RIGHT to open treeitem at treeitem index ' + + i + + ' should move focus to item ' + + (i + 1) + ); + } + + // If we are a document, the focus will not move + else { + t.true( + await checkFocus(t, ex.treeitemSelector, i), + 'Sending key ARROW_RIGHT to document item at treeitem index ' + + i + + ' should not move focus' + ); + } + i++; + } + } +); + +ariaTest( + 'key left arrow closes treeitems and moves focus', + exampleFile, + 'key-left-arrow', + async (t) => { + // Open all treeitems + await openAllExpandableTreeitems(t); + + const items = await t.context.queryElements(t, ex.treeitemSelector); + + let i = items.length - 1; + while (i > 0) { + const isExpandable = await isExpandableTreeitem(items[i]); + const isOpened = await isOpenedExpandableTreeitem(items[i]); + const isTopLevel = isExpandableTreeitem + ? await isTopLevelTreeitem(t, items[i]) + : false; + + await items[i].sendKeys(Key.ARROW_LEFT); + + // If the item is a treeitem and the treeitem was opened, arrow will close treeitem + if (isExpandable && isOpened) { + t.is( + await items[i].getAttribute('aria-expanded'), + 'false', + 'Sending key ARROW_LEFT to expandable treeitem at index ' + + i + + ' when the expandable treeitem is opened should close the treeitem' + ); + + t.true( + await checkFocus(t, ex.treeitemSelector, i), + 'Sending key ARROW_LEFT to expandable treeitem index ' + + i + + ' when the expandable treeitem is opened should not move the focus' + ); + // Send one more arrow key to the treeitem that is now closed + continue; + } + + // If the item is a top level treeitem and closed, arrow will do nothing + else if (isTopLevel) { + t.true( + await checkFocus(t, ex.treeitemSelector, i), + 'Sending key ARROW_LEFT to document in top level treeitem at treeitem index ' + + i + + ' should not move focus' + ); + } + + // If the item is treeitem, or a closed treeitem, arrow will move up a treeitem + else { + t.true( + await checkFocusOnParentTreeitem(t, items[i]), + 'Sending key ARROW_LEFT to document in treeitem at treeitem index ' + + i + + ' should move focus to parent treeitem' + ); + + t.is( + await items[i].isDisplayed(), + true, + 'Sending key ARROW_LEFT to document in expandable treeitem at index ' + + i + + ' should not close the treeitem it is in' + ); + } + + i--; + } + } +); + +ariaTest('key home moves focus', exampleFile, 'key-home', async (t) => { + // Test that key "home" works when no treeitem is open + const topLevelTreeitems = await t.context.queryElements( + t, + ex.topLevelTreeitemsSelector + ); + + for (let i = topLevelTreeitems.length - 1; i >= 0; i--) { + await topLevelTreeitems[i].sendKeys(Key.HOME); + + t.true( + await checkFocus(t, ex.topLevelTreeitemsSelector, 0), + 'Sending key HOME to top level treeitem at index ' + + i + + ' should move focus to first top level treeitem' + ); + + if (await isExpandableTreeitem(topLevelTreeitems[i])) { + t.is( + await topLevelTreeitems[i].getAttribute('aria-expanded'), + 'false', + 'Sending key HOME to top level treeitem at index ' + + i + + ' should not expand the treeitem' + ); + } + } + + // Reload page + await t.context.session.get(t.context.url); + + // Open all treeitems + await openAllExpandableTreeitems(t); + + const items = await t.context.queryElements(t, ex.treeitemSelector); + + for (let i = items.length - 1; i >= 0; i--) { + await items[i].sendKeys(Key.HOME); + + t.true( + await checkFocus(t, ex.treeitemSelector, 0), + 'Sending key HOME to treeitem at index ' + + i + + ' will move focus to the first treeitem' + ); + } +}); + +ariaTest('key end moves focus', exampleFile, 'key-end', async (t) => { + // Test that key "end" works when no treeitem is open + const topLevelTreeitems = await t.context.queryElements( + t, + ex.topLevelTreeitemsSelector + ); + + for (let i = topLevelTreeitems.length - 1; i >= 0; i--) { + await topLevelTreeitems[i].sendKeys(Key.END); + + t.true( + await checkFocus( + t, + ex.topLevelTreeitemsSelector, + topLevelTreeitems.length - 1 + ), + 'Sending key END to top level treeitem at index ' + + i + + ' should move focus to last top level treeitem' + ); + + if (await isExpandableTreeitem(topLevelTreeitems[i])) { + t.is( + await topLevelTreeitems[i].getAttribute('aria-expanded'), + 'false', + 'Sending key END to top level treeitem at index ' + + i + + ' should not expand the treeitem' + ); + } + } + + // Reload page + await t.context.session.get(t.context.url); + + // Open all expandable treeitems + await openAllExpandableTreeitems(t); + + const items = await t.context.queryElements(t, ex.treeitemSelector); + + for (let i = items.length - 1; i >= 0; i--) { + await items[i].sendKeys(Key.END); + + t.true( + await checkFocus(t, ex.treeitemSelector, items.length - 1), + 'Sending key END to treeitem at index ' + + i + + ' will move focus to the last item in the last opened treeitem' + ); + } +}); + +ariaTest('characters move focus', exampleFile, 'key-character', async (t) => { + const charIndexTestClosed = [ + { sendChar: 'p', sendIndex: 0, endIndex: 0 }, + { sendChar: 'A', sendIndex: 0, endIndex: 1 }, + { sendChar: 'H', sendIndex: 1, endIndex: 0 }, + ]; + + const charIndexTestOpened = [ + { sendChar: 'f', sendIndex: 0, endIndex: 4 }, + { sendChar: 'h', sendIndex: 0, endIndex: 5 }, + { sendChar: 'u', sendIndex: 2, endIndex: 15 }, + { sendChar: 'u', sendIndex: 15, endIndex: 15 }, + { sendChar: 'c', sendIndex: 15, endIndex: 21 }, + { sendChar: 'r', sendIndex: 21, endIndex: 28 }, + { sendChar: 'f', sendIndex: 28, endIndex: 4 }, + { sendChar: 'g', sendIndex: 3, endIndex: 16 }, + { sendChar: 'f', sendIndex: 10, endIndex: 11 }, + ]; + + const topLevelTreeitems = await t.context.queryElements( + t, + ex.topLevelTreeitemsSelector + ); + + for (let test of charIndexTestClosed) { + // Send character to treeitem + await topLevelTreeitems[test.sendIndex].sendKeys(test.sendChar); + + // Test that the focus switches to the appropriate item + t.true( + await checkFocus(t, ex.topLevelTreeitemsSelector, test.endIndex), + 'Sending character ' + + test.sendChar + + ' to treeitem ' + + test.sendIndex + + ' should move the focus to treeitem ' + + test.endIndex + ); + } + + // Reload page + await t.context.session.get(t.context.url); + + // Open all treeitems + await openAllExpandableTreeitems(t); + + const items = await t.context.queryElements(t, ex.treeitemSelector); + + for (let test of charIndexTestOpened) { + // Send character to treeitem + await items[test.sendIndex].sendKeys(test.sendChar); + + // Test that the focus switches to the appropriate treeitem + t.true( + await checkFocus(t, ex.treeitemSelector, test.endIndex), + 'Sending character ' + + test.sendChar + + ' to treeitem ' + + test.sendIndex + + ' should move the focus to treeitem ' + + test.endIndex + ); + } +}); + +ariaTest( + 'asterisk key opens expandable treeitems', + exampleFile, + 'key-asterisk', + async (t) => { + // Test that "*" ONLY opens all top level nodes and no other treeitems + + const topLevelTreeitems = await t.context.queryElements( + t, + ex.topLevelExpandableTreeitemsSelector + ); + + // Send Key + await topLevelTreeitems[0].sendKeys('*'); + + await assertAttributeValues( + t, + ex.topLevelExpandableTreeitemsSelector, + 'aria-expanded', + 'true' + ); + await assertAttributeValues( + t, + ex.nextLevelExpandableTreeitemsSelector, + 'aria-expanded', + 'false' + ); + + // Test that "*" on "about" child treeitems on that level + + const aboutChildTreeitems = await t.context.queryElements( + t, + ex.aboutChildTreeitemsSelector + ); + + const aboutChildExpandableTreeitems = await t.context.queryElements( + t, + ex.aboutChildExpandableTreeitemsSelector + ); + + // Send key + + await aboutChildTreeitems[0].sendKeys('*'); + + // The child expandable treeitems of "About" top level treeitem should all be open + + for (let el of aboutChildExpandableTreeitems) { + t.true( + (await el.getAttribute('aria-expanded')) === 'true', + 'Sibling "' + + (await el.getText()) + + '" treeitem of "' + + (await aboutChildTreeitems[0].getText()) + + '" treeitem should all be opened after sending one "*" to the treeitem.' + ); + } + + // Test that "*" on "admissions" child treeitems on that level + + const admissionsChildTreeitems = await t.context.queryElements( + t, + ex.admissionsChildTreeitemsSelector + ); + + const admissionsChildExpandableTreeitems = await t.context.queryElements( + t, + ex.admissionsChildExpandableTreeitemsSelector + ); + + // Send key + + await admissionsChildTreeitems[0].sendKeys('*'); + + // The child expandable treeitems of "admissions" top level treeitem should all be open + + for (let el of admissionsChildExpandableTreeitems) { + t.true( + (await el.getAttribute('aria-expanded')) === 'true', + 'Sibling "' + + (await el.getText()) + + '" treeitem of "' + + (await admissionsChildTreeitems[0].getText()) + + '" treeitem should all be opened after sending one "*" to the treeitem.' + ); + } + } +); diff --git a/test/util/assertAriaOwns.js b/test/util/assertAriaOwns.js new file mode 100644 index 0000000000..37f3a97cd0 --- /dev/null +++ b/test/util/assertAriaOwns.js @@ -0,0 +1,48 @@ +const assert = require('assert'); + +/** + * Confirm the aria-owns element. + * + * @param {obj} t - ava execution object + * @param {String} elementSelector - the element with aria-owns set + */ + +module.exports = async function assertAriaOwns(t, elementSelector) { + const elements = await t.context.queryElements(t, elementSelector); + + for (let element of elements) { + const ariaOwnsExists = await t.context.session.executeScript( + async function () { + const selector = arguments[0]; + let el = document.querySelector(selector); + return el.hasAttribute('aria-owns'); + }, + elementSelector + ); + + assert.ok( + ariaOwnsExists, + '"aria-owns" attribute should exist on element(s): ' + elementSelector + ); + + const ownsId = await element.getAttribute('aria-owns'); + + assert.ok( + ownsId, + '"aria-owns" attribute should have a value on element(s): ' + + elementSelector + ); + + const ownedEl = await t.context.queryElements(t, `#${ownsId}`); + + assert.equal( + ownedEl.length, + 1, + 'Element with id "' + + ownsId + + '" should exist as reference by "aria-owns" on: ' + + elementSelector + ); + } + t.pass(); +};