diff --git a/browser/base/content/tabbrowser.xml b/browser/base/content/tabbrowser.xml index 8caedc54717e..b4001ac8c1f8 100644 --- a/browser/base/content/tabbrowser.xml +++ b/browser/base/content/tabbrowser.xml @@ -1160,44 +1160,96 @@ ]]> = visibleTabs.length) { + focusedTabIndex = 0; + } else if (focusedTabIndex < 0) { + focusedTabIndex = visibleTabs.length - 1; + } + } else { + focusedTabIndex = Math.min(visibleTabs.length - 1, Math.max(0, focusedTabIndex)); + } + + if (keyComboForFocus && + focusedTabIndex != lastFocusedTabIndex) { + this.ariaFocusedItem = visibleTabs[focusedTabIndex]; + } + event.preventDefault(); ]]> diff --git a/browser/base/content/test/tabs/browser.ini b/browser/base/content/test/tabs/browser.ini index a2dc9b9a7e4a..644548a9a60a 100644 --- a/browser/base/content/test/tabs/browser.ini +++ b/browser/base/content/test/tabs/browser.ini @@ -38,6 +38,8 @@ support-files = [browser_multiselect_tabs_reload.js] [browser_multiselect_tabs_reorder.js] [browser_multiselect_tabs_using_Ctrl.js] +[browser_multiselect_tabs_using_keyboard.js] +skip-if = os == 'mac' # Skipped because macOS keyboard support requires changing system settings [browser_multiselect_tabs_using_selectedTabs.js] [browser_multiselect_tabs_using_Shift_and_Ctrl.js] [browser_multiselect_tabs_using_Shift.js] diff --git a/browser/base/content/test/tabs/browser_multiselect_tabs_using_keyboard.js b/browser/base/content/test/tabs/browser_multiselect_tabs_using_keyboard.js new file mode 100644 index 000000000000..c5ad2f6f30af --- /dev/null +++ b/browser/base/content/test/tabs/browser_multiselect_tabs_using_keyboard.js @@ -0,0 +1,101 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const PREF_MULTISELECT_TABS = "browser.tabs.multiselect"; + +function synthesizeKeyAndWaitForFocus(element, keyCode, options) { + let focused = BrowserTestUtils.waitForEvent(element, "focus"); + EventUtils.synthesizeKey(keyCode, options); + return focused; +} + +function synthesizeKeyAndWaitForTabToGetKeyboardFocus(tab, keyCode, options) { + let focused = TestUtils.waitForCondition(() => { + return tab.classList.contains("keyboard-focused-tab"); + }, "Waiting for tab to get keyboard focus"); + EventUtils.synthesizeKey(keyCode, options); + return focused; +} + +add_task(async function setup() { + await SpecialPowers.pushPrefEnv({ + set: [[PREF_MULTISELECT_TABS, true]], + }); + + let prevActiveElement = document.activeElement; + registerCleanupFunction(() => { + prevActiveElement.focus(); + }); +}); + +add_task(async function changeSelectionUsingKeyboard() { + await SpecialPowers.pushPrefEnv({ + set: [[PREF_MULTISELECT_TABS, true]], + }); + + const tab1 = await addTab("http://mochi.test:8888/1"); + const tab2 = await addTab("http://mochi.test:8888/2"); + const tab3 = await addTab("http://mochi.test:8888/3"); + const tab4 = await addTab("http://mochi.test:8888/4"); + const tab5 = await addTab("http://mochi.test:8888/5"); + + await BrowserTestUtils.switchTab(gBrowser, tab3); + info("Move focus to location bar using the keyboard"); + await synthesizeKeyAndWaitForFocus(gURLBar, "l", {accelKey: true}); + ok(document.activeElement, "urlbar should be focused"); + + info("Move focus to the selected tab using the keyboard"); + let identityBox = document.querySelector("#identity-box"); + await synthesizeKeyAndWaitForFocus(identityBox, "VK_TAB", {shiftKey: true}); + await synthesizeKeyAndWaitForFocus(tab3, "VK_TAB", {shiftKey: true}); + is(document.activeElement, tab3, "Tab3 should be focused"); + + info("Move focus to tab 1 using the keyboard"); + await synthesizeKeyAndWaitForTabToGetKeyboardFocus(tab2, "KEY_ArrowLeft", {accelKey: true}); + await synthesizeKeyAndWaitForTabToGetKeyboardFocus(tab1, "KEY_ArrowLeft", {accelKey: true}); + is(gBrowser.tabContainer.ariaFocusedItem, tab1, "Tab1 should be the ariaFocusedItem"); + + ok(!tab1.multiselected, "Tab1 shouldn't be multiselected"); + info("Select tab1 using keyboard"); + EventUtils.synthesizeKey("VK_SPACE", { accelKey: true }); + ok(tab1.multiselected, "Tab1 should be multiselected"); + + info("Move focus to tab 5 using the keyboard"); + await synthesizeKeyAndWaitForTabToGetKeyboardFocus(tab2, "KEY_ArrowRight", { accelKey: true }); + await synthesizeKeyAndWaitForTabToGetKeyboardFocus(tab3, "KEY_ArrowRight", { accelKey: true }); + await synthesizeKeyAndWaitForTabToGetKeyboardFocus(tab4, "KEY_ArrowRight", { accelKey: true }); + await synthesizeKeyAndWaitForTabToGetKeyboardFocus(tab5, "KEY_ArrowRight", { accelKey: true }); + + ok(!tab5.multiselected, "Tab5 shouldn't be multiselected"); + info("Select tab5 using keyboard"); + EventUtils.synthesizeKey("VK_SPACE", { accelKey: true }); + ok(tab5.multiselected, "Tab5 should be multiselected"); + + ok(tab1.multiselected && gBrowser._multiSelectedTabsSet.has(tab1), "Tab1 is (multi) selected"); + ok(tab3.multiselected && gBrowser._multiSelectedTabsSet.has(tab3), "Tab3 is (multi) selected"); + ok(tab5.multiselected && gBrowser._multiSelectedTabsSet.has(tab5), "Tab5 is (multi) selected"); + is(gBrowser.multiSelectedTabsCount, 3, "Three tabs (multi) selected"); + is(tab3, gBrowser.selectedTab, "Tab3 is still the selected tab"); + + await synthesizeKeyAndWaitForFocus(tab4, "KEY_ArrowLeft", {}); + is(tab4, gBrowser.selectedTab, "Tab4 is now selected tab since tab5 had keyboard focus"); + + is(tab4.previousElementSibling, tab3, "tab4 should be after tab3"); + is(tab4.nextElementSibling, tab5, "tab4 should be before tab5"); + + let tabsReordered = BrowserTestUtils.waitForCondition(() => { + return tab4.previousElementSibling == tab2 && + tab4.nextElementSibling == tab3; + }, "tab4 should now be after tab2 and before tab3"); + EventUtils.synthesizeKey("KEY_ArrowLeft", {accelKey: true, shiftKey: true}); + await tabsReordered; + + is(tab4.previousElementSibling, tab2, "tab4 should be after tab2"); + is(tab4.nextElementSibling, tab3, "tab4 should be before tab3"); + + BrowserTestUtils.removeTab(tab1); + BrowserTestUtils.removeTab(tab2); + BrowserTestUtils.removeTab(tab3); + BrowserTestUtils.removeTab(tab4); + BrowserTestUtils.removeTab(tab5); +}); diff --git a/browser/themes/linux/browser.css b/browser/themes/linux/browser.css index a8378defaa07..e75336ad00e6 100644 --- a/browser/themes/linux/browser.css +++ b/browser/themes/linux/browser.css @@ -498,7 +498,8 @@ notification[value="translation"] menulist > .menulist-dropmarker { } } -.tabbrowser-tab:focus > .tab-stack > .tab-content { +.keyboard-focused-tab > .tab-stack > .tab-content, +.tabbrowser-tab:focus:not([aria-activedescendant]) > .tab-stack > .tab-content { outline: 1px dotted; outline-offset: -6px; } diff --git a/browser/themes/osx/browser.css b/browser/themes/osx/browser.css index c37d65a35f6d..fce9fe74ba3b 100644 --- a/browser/themes/osx/browser.css +++ b/browser/themes/osx/browser.css @@ -669,9 +669,9 @@ html|input.urlbar-input { text-shadow: inherit; } -.tabbrowser-tab:focus > .tab-stack > .tab-content > .tab-label-container:not([pinned]), -.tabbrowser-tab:focus > .tab-stack > .tab-content > .tab-icon-image[pinned], -.tabbrowser-tab:focus > .tab-stack > .tab-content > .tab-throbber[pinned] { +:-moz-any(.keyboard-focused-tab, .tabbrowser-tab:focus:not([aria-activedescendant])) > .tab-stack > .tab-content > .tab-label-container:not([pinned]), +:-moz-any(.keyboard-focused-tab, .tabbrowser-tab:focus:not([aria-activedescendant])) > .tab-stack > .tab-content > .tab-icon-image[pinned], +:-moz-any(.keyboard-focused-tab, .tabbrowser-tab:focus:not([aria-activedescendant])) > .tab-stack > .tab-content > .tab-throbber[pinned] { box-shadow: var(--focus-ring-box-shadow); } diff --git a/browser/themes/windows/browser.css b/browser/themes/windows/browser.css index 81accc94252c..f0f58f78f29e 100644 --- a/browser/themes/windows/browser.css +++ b/browser/themes/windows/browser.css @@ -691,7 +691,8 @@ html|*.urlbar-input:-moz-lwtheme::placeholder, } /* tabbrowser-tab focus ring */ -.tabbrowser-tab:focus > .tab-stack > .tab-content { +.keyboard-focused-tab > .tab-stack > .tab-content, +.tabbrowser-tab:focus:not([aria-activedescendant]) > .tab-stack > .tab-content { outline: 1px dotted; outline-offset: -6px; } diff --git a/toolkit/content/widgets/tabbox.xml b/toolkit/content/widgets/tabbox.xml index 5418f9343d00..15e6678a8294 100644 --- a/toolkit/content/widgets/tabbox.xml +++ b/toolkit/content/widgets/tabbox.xml @@ -199,6 +199,53 @@ + + + + + + + + + + + + + + + + + + @@ -223,6 +270,8 @@