Commit 47b7742b authored by Luciano Pacheco's avatar Luciano Pacheco Committed by Commit Bot

[Files app] Add role and aria-describedby for file list column headers

This allows screen reader users to know that the column header is
clickable and also what is the sorting order it will apply when
clicked.

Use aria-described to point to the text (with translation) that
describes the action performed when clicking in the column header.

Use aria role=button so screen reader recognizes column header as a
clickable element, Chromevox will say:
"Press Search plus Space to activate."

Add test that check for those 2 attributes in all column headers.

Remove 2 missing files from the test harness:
test_util_unittest.js and details_panel.js

Change cr.ui.table.TableColumn.headerRenderFunction to accept Element
in addition to just plain text, so we can customize further the column
header, in this case for add these ARIA attributes, but in the future
it needs a "ripple" effect when clicked too.

Test: browser_tests --gtest_filter="*fileListAriaAttributes*"
Bug: 888620
Change-Id: I6d56d94a2d59aad3b416133106f569cc495fb944
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1610669Reviewed-by: default avatarcalamity <calamity@chromium.org>
Reviewed-by: default avatarAlex Danilo <adanilo@chromium.org>
Commit-Queue: Luciano Pacheco <lucmult@chromium.org>
Cr-Commit-Position: refs/heads/master@{#661266}
parent cf4d5ea2
......@@ -942,6 +942,11 @@ WRAPPED_INSTANTIATE_TEST_SUITE_P(
TestCase("filesTooltipMouseOver"),
TestCase("filesTooltipClickHides")));
WRAPPED_INSTANTIATE_TEST_SUITE_P(
FileList, /* file_list.js */
FilesAppBrowserTest,
::testing::Values(TestCase("fileListAriaAttributes")));
WRAPPED_INSTANTIATE_TEST_SUITE_P(
Crostini, /* crostini.js */
FilesAppBrowserTest,
......@@ -1125,9 +1130,7 @@ class MultiProfileFilesAppBrowserTest : public FileManagerBrowserTestBase {
return test_case_name_.c_str();
}
std::string GetFullTestCaseName() const override {
return test_case_name_;
}
std::string GetFullTestCaseName() const override { return test_case_name_; }
const char* GetTestExtensionManifestName() const override {
return "file_manager_test_manifest.json";
......
......@@ -763,6 +763,10 @@ std::unique_ptr<base::DictionaryValue> GetFileManagerStrings() {
IDS_FILE_BROWSER_SHARE_ROOT_FOLDER_WITH_PLUGIN_VM_DRIVE);
SET_STRING("SIZE_BYTES", IDS_FILE_BROWSER_SIZE_BYTES);
SET_STRING("SIZE_COLUMN_LABEL", IDS_FILE_BROWSER_SIZE_COLUMN_LABEL);
SET_STRING("COLUMN_ASC_SORT_MESSAGE",
IDS_FILE_BROWSER_COLUMN_ASC_SORT_MESSAGE);
SET_STRING("COLUMN_DESC_SORT_MESSAGE",
IDS_FILE_BROWSER_COLUMN_DESC_SORT_MESSAGE);
SET_STRING("SIZE_GB", IDS_FILE_BROWSER_SIZE_GB);
SET_STRING("SIZE_KB", IDS_FILE_BROWSER_SIZE_KB);
SET_STRING("SIZE_MB", IDS_FILE_BROWSER_SIZE_MB);
......
......@@ -101,6 +101,12 @@
<message name="IDS_FILE_BROWSER_SIZE_COLUMN_LABEL" desc="Size column label.">
Size
</message>
<message name="IDS_FILE_BROWSER_COLUMN_ASC_SORT_MESSAGE" desc="Message read by Chromevox/screenreader when focusing file list table column, when clicking will sort in ascending order.">
Click to sort the column in ascending order.
</message>
<message name="IDS_FILE_BROWSER_COLUMN_DESC_SORT_MESSAGE" desc="Message read by Chromevox/screenreader when focusing file list table column, when clicking will sort in descending order.">
Click to sort the column in descending order.
</message>
<message name="IDS_FILE_BROWSER_SIZE_BYTES" desc="Size in bytes.">
<ph name="NUMBER_OF_BYTES">$1<ex>42</ex></ph> bytes
......
edae0f9cd79831643500fc679d3b0bc33c8b2ccf
\ No newline at end of file
1b4858c8acac7c2fcdf4e61e5a2779edfeb97c1c
\ No newline at end of file
......@@ -13,6 +13,32 @@ function FileTableColumnModel(tableColumns) {
cr.ui.table.TableColumnModel.call(this, tableColumns);
}
/**
* Customize the column header to decorate with a11y attributes that announces
* the sorting used when clicked.
*
* @this {cr.ui.table.TableColumn} Bound by cr.ui.table.TableHeader before
* calling.
* @param {Element} table Table being rendered.
* @return {Element}
*/
function renderHeader_(table) {
const column = /** @type {cr.ui.table.TableColumn} */ (this);
const textElement = table.ownerDocument.createElement('span');
textElement.textContent = column.name;
const dm = table.dataModel;
let sortOrder = column.defaultOrder;
if (dm && dm.sortStatus.field === column.id) {
// Here we have to flip, because clicking will perform the opposite sorting.
sortOrder = dm.sortStatus.direction === 'desc' ? 'asc' : 'desc';
}
textElement.setAttribute('aria-describedby', 'sort-column-' + sortOrder);
textElement.setAttribute('role', 'button');
return textElement;
}
/**
* Inherits from cr.ui.TableColumnModel.
*/
......@@ -397,25 +423,30 @@ FileTable.decorate =
const nameColumn = new cr.ui.table.TableColumn(
'name', str('NAME_COLUMN_LABEL'), fullPage ? 386 : 324);
nameColumn.renderFunction = self.renderName_.bind(self);
nameColumn.headerRenderFunction = renderHeader_;
const sizeColumn = new cr.ui.table.TableColumn(
'size', str('SIZE_COLUMN_LABEL'), 110, true);
sizeColumn.renderFunction = self.renderSize_.bind(self);
sizeColumn.defaultOrder = 'desc';
sizeColumn.headerRenderFunction = renderHeader_;
const statusColumn = new cr.ui.table.TableColumn(
'status', str('STATUS_COLUMN_LABEL'), 60, true);
statusColumn.renderFunction = self.renderStatus_.bind(self);
statusColumn.visible = self.importStatusVisible_;
statusColumn.headerRenderFunction = renderHeader_;
const typeColumn = new cr.ui.table.TableColumn(
'type', str('TYPE_COLUMN_LABEL'), fullPage ? 110 : 110);
typeColumn.renderFunction = self.renderType_.bind(self);
typeColumn.headerRenderFunction = renderHeader_;
const modTimeColumn = new cr.ui.table.TableColumn(
'modificationTime', str('DATE_COLUMN_LABEL'), fullPage ? 150 : 210);
modTimeColumn.renderFunction = self.renderDate_.bind(self);
modTimeColumn.defaultOrder = 'desc';
modTimeColumn.headerRenderFunction = renderHeader_;
const columns =
[nameColumn, sizeColumn, statusColumn, typeColumn, modTimeColumn];
......
......@@ -525,6 +525,8 @@
<div class="downloads-warning" hidden></div>
<div id="list-container" role="main">
<div id="more-actions-info" hidden>$i18n{SEE_MENU_FOR_ACTIONS}</div>
<div id="sort-column-asc" hidden>$i18n{COLUMN_ASC_SORT_MESSAGE}</div>
<div id="sort-column-desc" hidden>$i18n{COLUMN_DESC_SORT_MESSAGE}</div>
<div id="empty-folder" hidden>
<div class="image"></div>
<span id="empty-folder-label" class="label">$i18n{EMPTY_FOLDER}</span>
......
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
'use strict';
(() => {
/**
* Tests that file list column header have ARIA attributes.
*/
testcase.fileListAriaAttributes = async () => {
const appId = await setupAndWaitUntilReady(
RootPath.DOWNLOADS, [ENTRIES.beautiful], []);
// Fetch column header.
const columnHeadersQuery =
['#detail-table .table-header [aria-describedby]'];
const columnHeaders = await remoteCall.callRemoteTestUtil(
'deepQueryAllElements', appId, [columnHeadersQuery, ['display']]);
chrome.test.assertTrue(columnHeaders.length > 0);
for (const header of columnHeaders) {
// aria-describedby is used to tell users that they can click and which
// type of sort asc/desc will happen.
chrome.test.assertTrue('aria-describedby' in header.attributes);
// role button is used so users know that it's clickable.
chrome.test.assertEq('button', header.attributes.role);
}
};
})();
......@@ -13,17 +13,16 @@
"test_util.js",
"remote_call.js",
"file_manager/background.js",
"file_manager/test_util_unittest.js",
"file_manager/context_menu.js",
"file_manager/copy_between_windows.js",
"file_manager/create_new_folder.js",
"file_manager/crostini.js",
"file_manager/delete.js",
"file_manager/details_panel.js",
"file_manager/directory_tree_context_menu.js",
"file_manager/drive_specific.js",
"file_manager/file_dialog.js",
"file_manager/file_display.js",
"file_manager/file_list.js",
"file_manager/files_tooltip.js",
"file_manager/folder_shortcuts.js",
"file_manager/gear_menu.js",
......
......@@ -127,7 +127,7 @@ cr.define('cr.ui.table', function() {
/**
* The column header render function.
* @type {function(Element): Text}
* @type {function(Element):Node}
*/
cr.defineProperty(TableColumn, 'headerRenderFunction');
......
......@@ -17,6 +17,7 @@ cr.define('cr.ui.table', function() {
* @extends {cr.EventTarget}
*/
function TableColumnModel(tableColumns) {
/** @type {!Array<cr.ui.table.TableColumn>} */
this.columns_ = [];
for (let i = 0; i < tableColumns.length; i++) {
this.columns_.push(tableColumns[i].clone());
......@@ -75,7 +76,7 @@ cr.define('cr.ui.table', function() {
/**
* Returns width (in percent) of column at the given index.
* @param {number} index The index of the column.
* @return {string} Column width in pixels.
* @return {number} Column width in pixels.
*/
getWidth: function(index) {
return this.columns_[index].width;
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment