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 @@