diff --git a/chrome/browser/ash/file_manager/file_manager_browsertest.cc b/chrome/browser/ash/file_manager/file_manager_browsertest.cc index 25610a806b9bb..d89bb33317368 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 @@ WRAPPED_INSTANTIATE_TEST_SUITE_P( ::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 b23a163c59336..f022604d1ff2f 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 @@ body.files-ng #list-container .table-header-cell:first-child .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 707d7070e6d7e..b5a1fe5f850b5 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 @@ body.files-ng #list-container .table-header-cell:first-child .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 066831ffd9cd6..a978b863efd6f 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 @@ export function renderHeader_(table) { 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 31c2ae3d53f25..b1c1467b9f623 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 @@ export class Table { */ 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 8a7831bd01eb7..d2871b4e589db 100644 --- a/ui/file_manager/integration_tests/file_manager/file_list.js +++ b/ui/file_manager/integration_tests/file_manager/file_list.js @@ -144,6 +144,56 @@ testcase.fileListSelectLastFocusedItem = async () => { chrome.test.assertEq(2, fileRows.indexOf(selectedRows[0])); }; +/** + * 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 ba251eb54ec34..6975f4f4b9d4f 100644 --- a/ui/file_manager/integration_tests/file_manager/tab_index.js +++ b/ui/file_manager/integration_tests/file_manager/tab_index.js @@ -76,6 +76,8 @@ testcase.tabindexFocus = async () => { await remoteCall.checkNextTabFocus(appId, 'drive-learn-more-button')); 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')); }; @@ -112,6 +114,8 @@ testcase.tabindexFocusDownloads = async () => { await remoteCall.checkNextTabFocus(appId, 'gear-button')); 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')); }; @@ -176,6 +180,8 @@ testcase.tabindexFocusDirectorySelected = async () => { await remoteCall.checkNextTabFocus(appId, 'drive-learn-more-button')); 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')); @@ -258,6 +264,7 @@ testcase.tabindexOpenDialogDownloads = async () => { 'sort-button', 'gear-button', 'dismiss-button', + 'sort-direction-button', 'file-list', ]; return tabindexFocus(