Bug 1775245 - Use ARIA role of "tree" for the addressbook and account manager trees. r=aleca,darktrojan
Differential Revision:
https://phabricator.services.mozilla.com/D149874
--- a/calendar/base/content/calendar-tab-panels.inc.xhtml
+++ b/calendar/base/content/calendar-tab-panels.inc.xhtml
@@ -187,17 +187,19 @@
command="calendar_new_calendar_command"
tooltiptext="&calendar.context.newserver.label;"/>
</hbox>
<calendar-modevbox id="calendar-list-inner-pane"
flex="1"
mode="calendar,task"
refcontrol="calendar-list-header"
context="list-calendars-context-menu">
- <html:ol id="calendar-list" is="orderable-tree-listbox"></html:ol>
+ <html:ol id="calendar-list" is="orderable-tree-listbox"
+ role="listbox">
+ </html:ol>
<template id="calendar-list-item" xmlns="http://www.w3.org/1999/xhtml">
<li draggable="true" role="option">
<div class="calendar-color"></div>
<div class="calendar-name"></div>
<img class="calendar-readstatus" src="chrome://calendar/skin/shared/icons/locked.svg" />
<button class="calendar-enable-button"></button>
<input type="checkbox" class="calendar-displayed" />
</li>
--- a/calendar/base/content/calendar-today-pane.inc.xhtml
+++ b/calendar/base/content/calendar-today-pane.inc.xhtml
@@ -127,17 +127,17 @@
<toolbarbutton id="todaypane-new-event-button"
mode="mail"
iconsize="small"
orient="horizontal"
label="&calendar.newevent.button.label;"
tooltiptext="&calendar.newevent.button.tooltip;"
command="calendar_new_event_command"/>
</hbox>
- <html:ul is="agenda-list" id="agenda"></html:ul>
+ <html:ul is="agenda-list" id="agenda" role="listbox"></html:ul>
<template id="agenda-listitem" xmlns="http://www.w3.org/1999/xhtml">
<div class="agenda-date-header"></div>
<div class="agenda-listitem-details">
<div class="agenda-listitem-calendar"></div>
<div class="agenda-listitem-details-inner">
<time class="agenda-listitem-time"></time>
<span class="agenda-listitem-title"></span>
<span class="agenda-listitem-relative"></span>
--- a/mail/base/content/about3Pane.xhtml
+++ b/mail/base/content/about3Pane.xhtml
@@ -35,17 +35,17 @@
<script defer="defer" src="chrome://messenger/content/pane-splitter.js"></script>
<script defer="defer" src="chrome://messenger/content/tree-listbox.js"></script>
<script defer="defer" src="chrome://messenger/content/jsTreeView.js"></script>
<script defer="defer" src="chrome://messenger/content/mailContext.js"></script>
<script defer="defer" src="chrome://messenger/content/about3Pane.js"></script>
</head>
<body class="layout-classic">
<div id="folderTreePane" tabindex="-1">
- <ul id="folderTree" is="tree-listbox"></ul>
+ <ul id="folderTree" is="tree-listbox" role="tree"></ul>
<template id="folderTemplate">
<li>
<div class="container">
<div class="twisty">
<img class="twisty-icon" src="chrome://global/skin/icons/arrow-down-12.svg" alt="" />
</div>
<div class="icon"></div>
<span class="name" tabindex="-1"></span>
--- a/mail/base/content/widgets/tree-listbox.js
+++ b/mail/base/content/widgets/tree-listbox.js
@@ -43,20 +43,31 @@
connectedCallback() {
if (this.hasConnected) {
return;
}
this.hasConnected = true;
this.setAttribute("is", "tree-listbox");
- this.setAttribute("role", "listbox");
+ switch (this.getAttribute("role")) {
+ case "tree":
+ this.isTree = true;
+ break;
+ case "listbox":
+ this.isTree = false;
+ break;
+ default:
+ throw new RangeError(
+ `Unsupported role ${this.getAttribute("role")}`
+ );
+ }
this.tabIndex = 0;
- this._initRows(this);
+ this._initRows();
if (this.querySelector("li")) {
this.selectedIndex = 0;
}
this.addEventListener("click", this);
this.addEventListener("keydown", this);
this._mutationObserver.observe(this, {
@@ -87,16 +98,19 @@
}
if (
row.classList.contains("children") &&
event.target.closest(".twisty")
) {
let rowIndex = this.rows.indexOf(row);
let didCollapse = row.classList.toggle("collapsed");
+ if (this.isTree) {
+ row.setAttribute("aria-expanded", !didCollapse);
+ }
if (didCollapse && row.querySelector(":is(ol, ul) > li.selected")) {
// The selected row was hidden. Select the visible ancestor of it.
this.selectedIndex = rowIndex;
} else if (this.selectedIndex > rowIndex) {
// Rows above the selected row have appeared or disappeared.
// Update the index of the selected row, but don't fire a 'select'
// event.
this._selectedIndex = this.rows.indexOf(
@@ -212,29 +226,26 @@
default:
return;
}
event.preventDefault();
}
_mutationObserver = new MutationObserver(mutations => {
+ this._initRows();
for (let mutation of mutations) {
let ancestor = mutation.target.closest("li");
for (let node of mutation.addedNodes) {
if (node.nodeType != Node.ELEMENT_NODE || node.localName != "li") {
continue;
}
node.classList.remove("selected");
- this._initRows(node);
- if (ancestor) {
- ancestor.classList.add("children");
- }
if (this._selectedIndex == -1) {
// There were no rows before this one was added. Select it.
this.selectedIndex = 0;
} else if (this._selectedIndex >= this.rows.indexOf(node)) {
// The selected row is further down the list than the inserted
// row. Update the selected index.
this._selectedIndex += 1 + node.querySelectorAll("li").length;
@@ -284,66 +295,60 @@
// The selected row is further down the list than the removed
// row. Update the selected index.
if (node.localName == "li") {
this._selectedIndex--;
}
this._selectedIndex -= node.querySelectorAll("li").length;
}
}
-
- if (
- ancestor &&
- (node.localName == "ul" ||
- (node.localName == "li" &&
- !mutation.target.querySelector("li")))
- ) {
- // There's no rows left under `ancestor`.
- ancestor.classList.remove("children");
- ancestor.classList.remove("collapsed");
- }
}
}
});
/**
- * Adds the 'option' role and 'children' class to `ancestor` if
- * appropriate and any descendants that are list items.
+ * Set the role attribute and classes for all descendants of the widget.
*/
- _initRows(ancestor) {
- let descendants = ancestor.querySelectorAll("li");
+ _initRows() {
+ let descendantItems = this.querySelectorAll("li");
+ let descendantLists = this.querySelectorAll("ol, ul");
- if (ancestor.localName == "li") {
- ancestor.setAttribute("role", "option");
- if (descendants.length > 0) {
- ancestor.classList.add("children");
+ for (let i = 0; i < descendantItems.length; i++) {
+ let row = descendantItems[i];
+ row.setAttribute("role", this.isTree ? "treeitem" : "option");
+ if (
+ i + 1 < descendantItems.length &&
+ row.contains(descendantItems[i + 1])
+ ) {
+ row.classList.add("children");
+ if (this.isTree) {
+ row.setAttribute(
+ "aria-expanded",
+ !row.classList.contains("collapsed")
+ );
+ }
+ } else {
+ row.classList.remove("children");
+ row.classList.remove("collapsed");
+ row.removeAttribute("aria-expanded");
}
}
- for (let i = 0; i < descendants.length - 1; i++) {
- let row = descendants[i];
- row.setAttribute("role", "option");
- row.classList.remove("selected");
- if (i + 1 < descendants.length && row.contains(descendants[i + 1])) {
- row.classList.add("children");
+ if (this.isTree) {
+ for (let list of descendantLists) {
+ list.setAttribute("role", "group");
}
}
// Don't add any inline style if we don't need to animate.
if (reducedMotionMedia.matches) {
return;
}
- // Add the height attribute to the inline style of a child list in order
- // to override the CSS declaration and guarantee a smooth transition
- // unaffected by the addition or removal of the `.collapsed` class.
- if (ancestor.matches("li.collapsed > :is(ol, ul)")) {
- ancestor.style.height = "0";
- }
- for (let childList of ancestor.querySelectorAll(
+ for (let childList of this.querySelectorAll(
"li.collapsed > :is(ol, ul)"
)) {
childList.style.height = "0";
}
}
/**
* Every visible row. Rows with collapsed ancestors are not included.
@@ -456,16 +461,19 @@
this.selectedIndex = index;
}
if (
row.classList.contains("children") &&
!row.classList.contains("collapsed")
) {
row.classList.add("collapsed");
+ if (this.isTree) {
+ row.setAttribute("aria-expanded", "false");
+ }
row.dispatchEvent(new CustomEvent("collapsed", { bubbles: true }));
this._animateCollapseRow(row);
}
}
/**
* Expands the row at `index` if it can be expanded.
*
@@ -473,32 +481,35 @@
*/
expandRowAtIndex(index) {
let row = this.getRowAtIndex(index);
if (
row.classList.contains("children") &&
row.classList.contains("collapsed")
) {
row.classList.remove("collapsed");
+ if (this.isTree) {
+ row.setAttribute("aria-expanded", "true");
+ }
row.dispatchEvent(new CustomEvent("expanded", { bubbles: true }));
this._animateExpandRow(row);
}
}
/**
* Animate the collapsing of a row containing child items.
*
* @param {HTMLLIElement} row - The parent row element.
*/
_animateCollapseRow(row) {
if (reducedMotionMedia.matches) {
return;
}
- let childList = row.querySelector(":is(ol, ul)");
+ let childList = row.querySelector("ol, ul");
let childListHeight = childList.scrollHeight;
let animation = childList.animate(
[{ height: `${childListHeight}px` }, { height: "0" }],
{
duration: ANIMATION_DURATION_MS,
easing: ANIMATION_EASING,
fill: "both",
@@ -515,17 +526,17 @@
*
* @param {HTMLLIElement} row - The parent row element.
*/
_animateExpandRow(row) {
if (reducedMotionMedia.matches) {
return;
}
- let childList = row.querySelector(":is(ol, ul)");
+ let childList = row.querySelector("ol, ul");
let childListHeight = childList.scrollHeight;
let animation = childList.animate(
[{ height: "0" }, { height: `${childListHeight}px` }],
{
duration: ANIMATION_DURATION_MS,
easing: ANIMATION_EASING,
fill: "both",
--- a/mail/base/test/browser/files/orderableTreeListbox.xhtml
+++ b/mail/base/test/browser/files/orderableTreeListbox.xhtml
@@ -69,17 +69,17 @@
}
</style>
<!-- This script is used for the automated test. -->
<script defer="defer" src="chrome://messenger/content/tree-listbox.js"></script>
<!-- This script is used when this file is loaded in a browser. -->
<script defer="defer" src="../../../content/widgets/tree-listbox.js"></script>
</head>
<body>
- <ol id="list" is="orderable-tree-listbox">
+ <ol id="list" is="orderable-tree-listbox" role="tree">
<li id="row-1">
<div draggable="true">
<div class="twisty"></div>
Item 1
</div>
</li>
<li id="row-2">
<div draggable="true">
--- a/mail/base/test/browser/files/treeListbox.xhtml
+++ b/mail/base/test/browser/files/treeListbox.xhtml
@@ -47,17 +47,17 @@
}
li.children.collapsed > div > div.twisty {
background-color: red;
}
</style>
<script defer="defer" src="chrome://messenger/content/tree-listbox.js"></script>
</head>
<body>
- <ul is="tree-listbox">
+ <ul is="tree-listbox" role="tree">
<li id="row-1">
<div>
<div class="twisty"></div>
Item with no children
</div>
</li>
<li id="row-2">
<div>
--- a/mail/components/addrbook/content/aboutAddressBook.xhtml
+++ b/mail/components/addrbook/content/aboutAddressBook.xhtml
@@ -65,41 +65,41 @@
class="toolbarbutton-1"
data-l10n-id="about-addressbook-toolbar-new-list"/>
<xul:toolbarbutton id="toolbarImport"
class="toolbarbutton-1"
data-l10n-id="about-addressbook-toolbar-import"/>
</xul:toolbar>
</xul:toolbox>
<div id="booksPane">
- <ul is="ab-tree-listbox" id="books">
- <li role="option" id="allAddressBooks" class="bookRow noDelete readOnly">
+ <ul is="ab-tree-listbox" id="books" role="tree">
+ <li id="allAddressBooks" class="bookRow noDelete readOnly">
<div class="bookRow-container">
<div class="twisty"></div>
<div class="bookRow-icon"></div>
<span class="bookRow-name" tabindex="-1" data-l10n-id="all-address-books"></span>
<div class="bookRow-menu"></div>
</div>
</li>
</ul>
<template id="bookRow">
- <li role="option" class="bookRow">
+ <li class="bookRow">
<div class="bookRow-container">
<div class="twisty">
<img class="twisty-icon" src="chrome://messenger/skin/icons/new/nav-down-sm.svg" alt="" />
</div>
<div class="bookRow-icon"></div>
<span class="bookRow-name" tabindex="-1"></span>
<div class="bookRow-menu"></div>
</div>
<ul></ul>
</li>
</template>
<template id="listRow">
- <li class="listRow" role="option">
+ <li class="listRow">
<div class="listRow-container">
<div class="listRow-icon"></div>
<span class="listRow-name" tabindex="-1"></span>
<div class="listRow-menu"></div>
</div>
</li>
</template>
</div>
--- a/mailnews/base/prefs/content/AccountManager.xhtml
+++ b/mailnews/base/prefs/content/AccountManager.xhtml
@@ -46,21 +46,22 @@
window.addEventListener("unload", event => { onUnload(); });
</script>
</head>
<html:body xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<stringbundle id="bundle_brand" src="chrome://branding/locale/brand.properties"/>
<stringbundle id="bundle_prefs" src="chrome://messenger/locale/prefs.properties"/>
<html:aside id="accountTreeBox">
<html:ol is="orderable-tree-listbox" id="accounttree"
+ role="tree"
flex="1"
onselect="onAccountTreeSelect(null, null);">
</html:ol>
<template id="accountTreeItem" xmlns="http://www.w3.org/1999/xhtml">
- <li role="option">
+ <li>
<div draggable="true">
<div class="twisty">
<img class="twisty-icon" alt=""
src="chrome://global/skin/icons/arrow-down-12.svg" />
</div>
<div class="icon"></div>
<span class="name" tabindex="-1"></span>
</div>