Files app: Enable keyboard control of file list sort direction
Adds a tab stop for the sort icon in the file list. This allows a user
to press 'Enter' to change the sort direction.
Note, the file list header is regenerated each time the sort is changed
(existing behaviour). If 'focus-outline-visible' is set on the root
document it indicates the keyboard is being used for navigation, we
use this and current focus position to decide if the sort button
should be focused after a sort of the file list.
Bug: b/255519075
Tests: browser_tests --gtest_filter="File*fileListSortWithKeyboard"
Change-Id: I110d8869613e12dc4c70b6d35f2f1f78b8270692
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4109950
Reviewed-by: Wenbo Jie <wenbojie@chromium.org>
Commit-Queue: Alex Danilo <adanilo@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1084838}
diff --git a/chrome/browser/ash/file_manager/file_manager_browsertest.cc b/chrome/browser/ash/file_manager/file_manager_browsertest.cc
index 25610a80..d89bb33 100644
--- a/chrome/browser/ash/file_manager/file_manager_browsertest.cc
+++ b/chrome/browser/ash/file_manager/file_manager_browsertest.cc
@@ -1588,6 +1588,7 @@
::testing::Values(TestCase("fileListAriaAttributes"),
TestCase("fileListFocusFirstItem"),
TestCase("fileListSelectLastFocusedItem"),
+ TestCase("fileListSortWithKeyboard"),
TestCase("fileListKeyboardSelectionA11y"),
TestCase("fileListMouseSelectionA11y"),
TestCase("fileListDeleteMultipleFiles"),
diff --git a/ui/file_manager/file_manager/foreground/css/file_manager.css b/ui/file_manager/file_manager/foreground/css/file_manager.css
index b23a163c..f022604 100644
--- a/ui/file_manager/file_manager/foreground/css/file_manager.css
+++ b/ui/file_manager/file_manager/foreground/css/file_manager.css
@@ -2097,6 +2097,7 @@
.table-header-label .sort-icon {
--cr-icon-button-fill-color: var(--cros-icon-color-secondary);
--cr-icon-button-icon-size: 16px;
+ --cr-icon-button-focus-outline-color: var(--cros-focus-ring-color);
--cr-icon-button-hover-background-color: var(--cros-ripple-color);
--cr-icon-button-size: 32px;
border-radius: 50%;
diff --git a/ui/file_manager/file_manager/foreground/css/file_manager_gm3.css b/ui/file_manager/file_manager/foreground/css/file_manager_gm3.css
index 707d7070..b5a1fe5f 100644
--- a/ui/file_manager/file_manager/foreground/css/file_manager_gm3.css
+++ b/ui/file_manager/file_manager/foreground/css/file_manager_gm3.css
@@ -2030,6 +2030,7 @@
.table-header-label .sort-icon {
--cr-icon-button-fill-color: var(--cros-icon-color-secondary);
--cr-icon-button-icon-size: 16px;
+ --cr-icon-button-focus-outline-color: var(--cros-focus-ring-color);
--cr-icon-button-hover-background-color: var(--cros-ripple-color);
--cr-icon-button-size: 32px;
border-radius: 50%;
diff --git a/ui/file_manager/file_manager/foreground/js/ui/file_table.js b/ui/file_manager/file_manager/foreground/js/ui/file_table.js
index 066831f..a978b863 100644
--- a/ui/file_manager/file_manager/foreground/js/ui/file_table.js
+++ b/ui/file_manager/file_manager/foreground/js/ui/file_table.js
@@ -291,8 +291,20 @@
const icon = document.createElement('cr-icon-button');
const iconName = sortOrder === 'desc' ? 'up' : 'down';
icon.setAttribute('iron-icon', `files16:arrow_${iconName}_small`);
- icon.setAttribute('tabindex', '-1');
- icon.setAttribute('aria-hidden', 'true');
+ // If we're the sorting column make the icon a tab target.
+ if (isSorted) {
+ icon.id = 'sort-direction-button';
+ icon.setAttribute('tabindex', '0');
+ icon.setAttribute('aria-hidden', 'false');
+ if (sortOrder === 'asc') {
+ icon.setAttribute('aria-label', str('COLUMN_ASC_SORT_MESSAGE'));
+ } else {
+ icon.setAttribute('aria-label', str('COLUMN_DESC_SORT_MESSAGE'));
+ }
+ } else {
+ icon.setAttribute('tabindex', '-1');
+ icon.setAttribute('aria-hidden', 'true');
+ }
icon.classList.add('sort-icon', 'no-overlap');
container.classList.toggle('not-sorted', !isSorted);
diff --git a/ui/file_manager/file_manager/foreground/js/ui/table/table.js b/ui/file_manager/file_manager/foreground/js/ui/table/table.js
index 31c2ae3..b1c1467 100644
--- a/ui/file_manager/file_manager/foreground/js/ui/table/table.js
+++ b/ui/file_manager/file_manager/foreground/js/ui/table/table.js
@@ -296,6 +296,18 @@
*/
handleSorted_(e) {
this.header_.redraw();
+ // If we have 'focus-outline-visible' on the root HTML element and focus
+ // has reverted to the body element it means this sort header creation
+ // was the result of a keyboard action so set focus to the (newly
+ // recreated) sort button in that case.
+ if (document.querySelector('html.focus-outline-visible') &&
+ (document.activeElement instanceof HTMLBodyElement)) {
+ const sortButton =
+ this.header_.querySelector('cr-icon-button[tabindex="0"]');
+ if (sortButton) {
+ sortButton.focus();
+ }
+ }
this.onDataModelSorted();
}
diff --git a/ui/file_manager/integration_tests/file_manager/file_list.js b/ui/file_manager/integration_tests/file_manager/file_list.js
index 8a7831b..d2871b4e 100644
--- a/ui/file_manager/integration_tests/file_manager/file_list.js
+++ b/ui/file_manager/integration_tests/file_manager/file_list.js
@@ -145,6 +145,56 @@
};
/**
+ * Tests that after a multiple selection, canceling the selection and using
+ * Tab to focus the files list it selects the item that was last focused.
+ */
+testcase.fileListSortWithKeyboard = async () => {
+ const appId = await setupAndWaitUntilReady(
+ RootPath.DOWNLOADS, BASIC_LOCAL_ENTRY_SET, []);
+
+ // Send shift-Tab key to tab into sort button.
+ const result = await sendTestMessage({name: 'dispatchTabKey', shift: true});
+ chrome.test.assertEq(result, 'tabKeyDispatched', 'Tab key dispatch failed');
+ // Check: sort button has focus.
+ let focusedElement =
+ await remoteCall.callRemoteTestUtil('getActiveElement', appId, []);
+ // Check: button is showing down arrow.
+ chrome.test.assertTrue(
+ focusedElement['attributes']['iron-icon'] === 'files16:arrow_down_small');
+ // Check: aria-label tells us to click to sort ascending.
+ chrome.test.assertTrue(
+ focusedElement['attributes']['aria-label'] ===
+ 'Click to sort the column in ascending order.');
+ // Press 'enter' on the sort button.
+ const key = ['cr-icon-button[tabindex="0"]', 'Enter', false, false, false];
+ chrome.test.assertTrue(
+ await remoteCall.callRemoteTestUtil('fakeKeyDown', appId, key));
+ // Get the state of the (focused) sort button.
+ focusedElement =
+ await remoteCall.callRemoteTestUtil('getActiveElement', appId, []);
+ // Check: button is showing up arrow.
+ chrome.test.assertTrue(
+ focusedElement['attributes']['iron-icon'] === 'files16:arrow_up_small');
+ // Check: aria-label tells us to click to sort descending.
+ chrome.test.assertTrue(
+ focusedElement['attributes']['aria-label'] ===
+ 'Click to sort the column in descending order.');
+ // Press 'enter' key on the sort button again.
+ chrome.test.assertTrue(
+ await remoteCall.callRemoteTestUtil('fakeKeyDown', appId, key));
+ // Get the state of the (focused) sort button.
+ focusedElement =
+ await remoteCall.callRemoteTestUtil('getActiveElement', appId, []);
+ // Check: button is showing up arrow.
+ chrome.test.assertTrue(
+ focusedElement['attributes']['iron-icon'] === 'files16:arrow_down_small');
+ // Check: aria-label tells us to click to sort descending.
+ chrome.test.assertTrue(
+ focusedElement['attributes']['aria-label'] ===
+ 'Click to sort the column in ascending order.');
+};
+
+/**
* Verifies the total number of a11y messages and asserts the latest message
* is the expected one.
*
diff --git a/ui/file_manager/integration_tests/file_manager/tab_index.js b/ui/file_manager/integration_tests/file_manager/tab_index.js
index ba251eb5..6975f4f4 100644
--- a/ui/file_manager/integration_tests/file_manager/tab_index.js
+++ b/ui/file_manager/integration_tests/file_manager/tab_index.js
@@ -77,6 +77,8 @@
chrome.test.assertTrue(
await remoteCall.checkNextTabFocus(appId, 'dismiss-button'));
chrome.test.assertTrue(
+ await remoteCall.checkNextTabFocus(appId, 'sort-direction-button'));
+ chrome.test.assertTrue(
await remoteCall.checkNextTabFocus(appId, 'file-list'));
};
@@ -113,6 +115,8 @@
chrome.test.assertTrue(
await remoteCall.checkNextTabFocus(appId, 'dismiss-button'));
chrome.test.assertTrue(
+ await remoteCall.checkNextTabFocus(appId, 'sort-direction-button'));
+ chrome.test.assertTrue(
await remoteCall.checkNextTabFocus(appId, 'file-list'));
};
@@ -177,6 +181,8 @@
chrome.test.assertTrue(
await remoteCall.checkNextTabFocus(appId, 'dismiss-button'));
chrome.test.assertTrue(
+ await remoteCall.checkNextTabFocus(appId, 'sort-direction-button'));
+ chrome.test.assertTrue(
await remoteCall.checkNextTabFocus(appId, 'file-list'));
// Remove fakes.
@@ -258,6 +264,7 @@
'sort-button',
'gear-button',
'dismiss-button',
+ 'sort-direction-button',
'file-list',
];
return tabindexFocus(