diff --git a/mail/base/content/about3Pane.js b/mail/base/content/about3Pane.js index 8c93c1a293..d1d63e78b9 100644 --- a/mail/base/content/about3Pane.js +++ b/mail/base/content/about3Pane.js @@ -2129,6 +2129,9 @@ var folderPane = { // At this point `dbViewWrapperListener.onCreatedView` gets called, // setting up gDBView and scrolling threadTree to the right end. + threadPane.updateListRole( + !gViewWrapper?.showThreaded && !gViewWrapper?.showGroupedBySort + ); threadPane.restoreSortIndicator(); threadPane.restoreSelection(); threadPaneHeader.onFolderSelected(); @@ -4914,6 +4917,16 @@ var threadPane = { break; } }, + + /** + * Update the ARIA Role of the tree view table body to properly communicate + * to assistive techonology the type of list we're rendering. + * + * @param {boolean} isListbox - If the list should have a listbox role. + */ + updateListRole(isListbox) { + threadTree.table.body.setAttribute("role", isListbox ? "listbox" : "tree"); + }, }; var messagePane = { @@ -5316,6 +5329,18 @@ customElements.whenDefined("tree-view-table-row").then(() => { let ariaLabelPromises = []; const propertiesSet = new Set(properties.value.split(" ")); + + if (propertiesSet.has("dummy")) { + const cell = this.querySelector(".subjectcol-column"); + const textIndex = textColumns.indexOf("subjectCol"); + const label = cellTexts[textIndex]; + const span = cell.querySelector(".subject-line span"); + cell.title = span.textContent = label; + this.setAttribute("aria-label", label); + this.dataset.properties = "dummy"; + return; + } + this.dataset.properties = properties.value.trim(); for (let column of threadPane.columns) { @@ -5775,6 +5800,7 @@ var sortController = { } }, sortByThread() { + threadPane.updateListRole(false); gViewWrapper.showThreaded = true; this.sortThreadPane("byDate"); }, @@ -5856,18 +5882,23 @@ var sortController = { }, toggleThreaded() { if (gViewWrapper.showThreaded) { + threadPane.updateListRole(true); gViewWrapper.showUnthreaded = true; } else { + threadPane.updateListRole(false); gViewWrapper.showThreaded = true; } }, sortThreaded() { + threadPane.updateListRole(false); gViewWrapper.showThreaded = true; }, groupBySort() { + threadPane.updateListRole(false); gViewWrapper.showGroupedBySort = true; }, sortUnthreaded() { + threadPane.updateListRole(true); gViewWrapper.showUnthreaded = true; }, sortAscending() { diff --git a/mail/base/content/widgets/tree-view.mjs b/mail/base/content/widgets/tree-view.mjs index 1d08245115..f7748e8619 100644 --- a/mail/base/content/widgets/tree-view.mjs +++ b/mail/base/content/widgets/tree-view.mjs @@ -2354,7 +2354,7 @@ class TreeViewTableBody extends HTMLTableSectionElement { this.tabIndex = 0; this.setAttribute("is", "tree-view-table-body"); - this.setAttribute("role", "treeview"); + this.setAttribute("role", "tree"); this.setAttribute("aria-multiselectable", "true"); let treeView = this.closest("tree-view"); @@ -2412,11 +2412,16 @@ class TreeViewTableRow extends HTMLTableRowElement { } set index(index) { + this.setAttribute( + "role", + this.list.table.body.getAttribute("role") === "tree" + ? "treeitem" + : "option" + ); this.setAttribute("aria-posinset", index + 1); this.id = `${this.list.id}-row${index}`; const isGroup = this.view.isContainer(index); - this.setAttribute("role", isGroup ? "group" : "treeitem"); this.classList.toggle("children", isGroup); const isGroupOpen = this.view.isContainerOpen(index); diff --git a/mail/base/test/browser/browser_cardsView.js b/mail/base/test/browser/browser_cardsView.js index 6cb8cf70c2..b078c19c2b 100644 --- a/mail/base/test/browser/browser_cardsView.js +++ b/mail/base/test/browser/browser_cardsView.js @@ -53,6 +53,17 @@ add_task(async function testSwitchToCardsView() { "The tree view should not switch to a card layout" ); + Assert.equal( + threadTree.table.body.getAttribute("role"), + "tree", + "The message list table should be presented as Tree View" + ); + Assert.equal( + threadTree.getRowAtIndex(0).getAttribute("role"), + "treeitem", + "The message row should be presented as Tree Item" + ); + displayContext = about3Pane.document.getElementById( "threadPaneDisplayContext" ); @@ -90,6 +101,16 @@ add_task(async function testSwitchToCardsView() { "thread-card", "tree view in cards layout" ); + Assert.equal( + threadTree.table.body.getAttribute("role"), + "tree", + "The message list table should remain as Tree View" + ); + Assert.equal( + threadTree.getRowAtIndex(0).getAttribute("role"), + "treeitem", + "The message row should remain as Tree Item" + ); let row = threadTree.getRowAtIndex(0); let star = row.querySelector(".button-star"); diff --git a/mail/base/test/browser/browser_threadTreeQuirks.js b/mail/base/test/browser/browser_threadTreeQuirks.js index 883cd326be..43559908a1 100644 --- a/mail/base/test/browser/browser_threadTreeQuirks.js +++ b/mail/base/test/browser/browser_threadTreeQuirks.js @@ -559,3 +559,42 @@ async function restoreMessages() { sourceMessageIDs.indexOf(b.messageId) ); } + +add_task(async function testThreadTreeA11yRoles() { + Assert.equal( + threadTree.table.body.getAttribute("role"), + "listbox", + "The tree view should be presented as ListBox" + ); + Assert.equal( + threadTree.getRowAtIndex(0).getAttribute("role"), + "option", + "The message row should be presented as Option" + ); + + about3Pane.sortController.sortThreaded(); + + await BrowserTestUtils.waitForCondition( + () => threadTree.table.body.getAttribute("role") == "tree", + "The tree view should switch to a Tree View role" + ); + Assert.equal( + threadTree.getRowAtIndex(0).getAttribute("role"), + "treeitem", + "The message row should be presented as Tree Item" + ); + + about3Pane.sortController.groupBySort(); + + await BrowserTestUtils.waitForCondition( + () => threadTree.table.body.getAttribute("role") == "tree", + "The message list table should remain presented as Tree View" + ); + Assert.equal( + threadTree.getRowAtIndex(0).getAttribute("role"), + "treeitem", + "The first dummy message row should be presented as Tree Item" + ); + + about3Pane.sortController.sortUnthreaded(); +});