Bug 1921174: Move focus to Dedicated Search button by Tab key from urlbar r=daleharvey,urlbar-reviewers,adw,dao

Differential Revision: https://phabricator.services.mozilla.com/D225176
This commit is contained in:
Daisuke Akatsuka
2024-11-05 22:20:03 +00:00
parent 1e17e36737
commit 176b51d0b0
4 changed files with 203 additions and 7 deletions

View File

@@ -104,6 +104,16 @@ export class SearchModeSwitcher {
{ once: true } { once: true }
); );
} }
if (event.type == "keypress") {
// If open the panel by key, set urlbar input filed as focusedElement to
// move the focus to the input field it when popup will be closed.
// Please see _prevFocus element in toolkit/content/widgets/panel.js about
// the implementation.
this.#input.document.commandDispatcher.focusedElement =
this.#input.inputField;
}
lazy.PanelMultiView.openPopup(this.#popup, anchor, { lazy.PanelMultiView.openPopup(this.#popup, anchor, {
position: "bottomleft topleft", position: "bottomleft topleft",
triggerEvent: event, triggerEvent: event,
@@ -150,6 +160,19 @@ export class SearchModeSwitcher {
} }
handleEvent(event) { handleEvent(event) {
if (
event.keyCode == KeyEvent.DOM_VK_TAB &&
event.shiftKey &&
this.#input.view.isOpen
) {
// In this case, switcher button got focus by shift+tab from urlbar.
// So, move the focus on the last element of urlbar view to make cyclable.
this.#input.focus();
this.#input.view.selectBy(1, { reverse: true, userPressedTab: true });
event.preventDefault();
return;
}
let action = event.currentTarget.dataset.action ?? event.type; let action = event.currentTarget.dataset.action ?? event.type;
switch (action) { switch (action) {

View File

@@ -348,6 +348,36 @@ export class UrlbarController {
event.preventDefault(); event.preventDefault();
break; break;
case KeyEvent.DOM_VK_TAB: { case KeyEvent.DOM_VK_TAB: {
// Change the tab behavior when urlbar view is open.
if (
lazy.UrlbarPrefs.get("scotchBonnet.enableOverride") &&
this.view.isOpen
) {
if (
(event.shiftKey && !this.view.selectedElement) ||
(!event.shiftKey &&
this.view.selectedElement == this.view.getLastSelectableElement())
) {
// If type tab + shift when no selected element or when the last
// element has been selecting, move the focus on Dedicated Search
// button.
event.preventDefault();
this.view.selectedRowIndex = -1;
this.#focusOnDedicatedSearchButton();
break;
} else if (
event.shiftKey &&
this.view.selectedElement == this.view.getFirstSelectableElement()
) {
// Else, if type tab when the first element has been selecting, move
// the focus on the input field of urlbar.
event.preventDefault();
this.view.selectedRowIndex = -1;
this.input.focus();
break;
}
}
// It's always possible to tab through results when the urlbar was // It's always possible to tab through results when the urlbar was
// focused with the mouse or has a search string, or when the view // focused with the mouse or has a search string, or when the view
// already has a selection. // already has a selection.
@@ -701,6 +731,27 @@ export class UrlbarController {
} }
} }
} }
#focusOnDedicatedSearchButton() {
const switcher = this.input.document.getElementById(
"urlbar-searchmode-switcher"
);
// Set tabindex to be focusable.
switcher.setAttribute("tabindex", "-1");
// Remove blur listener to avoid closing urlbar view panel.
this.input.removeEventListener("blur", this.input);
// Move the focus.
switcher.focus();
// Restore all.
this.input.addEventListener("blur", this.input);
switcher.addEventListener(
"blur",
() => {
switcher.removeAttribute("tabindex");
},
{ once: true }
);
}
} }
/** /**

View File

@@ -380,10 +380,10 @@ export class UrlbarView {
// We cache the first and last rows since they will not change while // We cache the first and last rows since they will not change while
// selectBy is running. // selectBy is running.
let firstSelectableElement = this.#getFirstSelectableElement(); let firstSelectableElement = this.getFirstSelectableElement();
// #getLastSelectableElement will not return an element that is over // getLastSelectableElement will not return an element that is over
// maxResults and thus may be hidden and not selectable. // maxResults and thus may be hidden and not selectable.
let lastSelectableElement = this.#getLastSelectableElement(); let lastSelectableElement = this.getLastSelectableElement();
if (!selectedElement) { if (!selectedElement) {
selectedElement = reverse selectedElement = reverse
@@ -803,7 +803,7 @@ export class UrlbarView {
// result added, which is why we do this check here when each result is // result added, which is why we do this check here when each result is
// added and not above. // added and not above.
if (this.#shouldShowHeuristic(firstResult)) { if (this.#shouldShowHeuristic(firstResult)) {
this.#selectElement(this.#getFirstSelectableElement(), { this.#selectElement(this.getFirstSelectableElement(), {
updateInput: false, updateInput: false,
setAccessibleFocus: setAccessibleFocus:
this.controller._userSelectionBehavior == "arrow", this.controller._userSelectionBehavior == "arrow",
@@ -2243,7 +2243,7 @@ export class UrlbarView {
} }
} }
let selectableElement = this.#getFirstSelectableElement(); let selectableElement = this.getFirstSelectableElement();
let uiIndex = 0; let uiIndex = 0;
while (selectableElement) { while (selectableElement) {
selectableElement.elementIndex = uiIndex++; selectableElement.elementIndex = uiIndex++;
@@ -2593,7 +2593,7 @@ export class UrlbarView {
* @returns {Element} * @returns {Element}
* The first selectable element in the view. * The first selectable element in the view.
*/ */
#getFirstSelectableElement() { getFirstSelectableElement() {
let element = this.#rows.firstElementChild; let element = this.#rows.firstElementChild;
if (element && !this.#isSelectableElement(element)) { if (element && !this.#isSelectableElement(element)) {
element = this.#getNextSelectableElement(element); element = this.#getNextSelectableElement(element);
@@ -2607,7 +2607,7 @@ export class UrlbarView {
* @returns {Element} * @returns {Element}
* The last selectable element in the view. * The last selectable element in the view.
*/ */
#getLastSelectableElement() { getLastSelectableElement() {
let element = this.#rows.lastElementChild; let element = this.#rows.lastElementChild;
if (element && !this.#isSelectableElement(element)) { if (element && !this.#isSelectableElement(element)) {
element = this.#getPreviousSelectableElement(element); element = this.#getPreviousSelectableElement(element);

View File

@@ -242,6 +242,7 @@ async function test_navigate_switcher(navKey, navTimes, searchMode) {
EventUtils.synthesizeKey(navKey); EventUtils.synthesizeKey(navKey);
} }
EventUtils.synthesizeKey("KEY_Enter"); EventUtils.synthesizeKey("KEY_Enter");
await UrlbarTestUtils.promiseSearchComplete(window);
await UrlbarTestUtils.assertSearchMode(window, searchMode); await UrlbarTestUtils.assertSearchMode(window, searchMode);
@@ -688,6 +689,127 @@ add_task(async function test_open_state() {
} }
}); });
add_task(async function test_focus_on_switcher_by_tab() {
for (const input of ["", "abc"]) {
info(`Open urlbar view with query [${input}]`);
await UrlbarTestUtils.promiseAutocompleteResultPopup({
window,
value: input,
});
if (input) {
info("Focus on input field by tab");
EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true });
}
info("Focus on Dedicated Search by tab");
EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true });
await TestUtils.waitForCondition(
() => document.activeElement.id == "urlbar-searchmode-switcher"
);
Assert.ok(true, "Dedicated Search button gets the focus");
let popup = UrlbarTestUtils.searchModeSwitcherPopup(window);
Assert.equal(popup.state, "closed", "Switcher popup should not be opened");
Assert.ok(gURLBar.view.isOpen, "Urlbar view panel has been opening");
Assert.equal(gURLBar.value, input, "Inputted value still be on urlbar");
info("Open the switcher popup by key");
let promiseMenuOpen = BrowserTestUtils.waitForEvent(popup, "popupshown");
EventUtils.synthesizeKey("KEY_Enter");
await promiseMenuOpen;
Assert.notEqual(
document.activeElement.id,
"urlbar-searchmode-switcher",
"Dedicated Search button loses the focus"
);
Assert.equal(
gURLBar.view.panel.hasAttribute("hide-temporarily"),
true,
"Urlbar view panel is closed"
);
Assert.equal(gURLBar.value, input, "Inputted value still be on urlbar");
info("Close the switcher popup by Escape");
let promiseMenuClose = BrowserTestUtils.waitForEvent(popup, "popuphidden");
EventUtils.synthesizeKey("KEY_Escape");
await promiseMenuClose;
Assert.equal(
document.activeElement.id,
"urlbar-input",
"Urlbar gets the focus"
);
Assert.equal(
gURLBar.view.panel.hasAttribute("hide-temporarily"),
false,
"Urlbar view panel is opened"
);
Assert.equal(gURLBar.value, input, "Inputted value still be on urlbar");
}
});
add_task(async function test_focus_order_by_tab() {
await PlacesTestUtils.addBookmarkWithDetails({
uri: "https://example.com/",
title: "abc",
});
const FOCUS_ORDER_ASSERTIONS = [
() =>
Assert.equal(
gURLBar.view.selectedElement,
gURLBar.view.getLastSelectableElement()
),
() =>
Assert.equal(
document.activeElement,
document.getElementById("urlbar-searchmode-switcher")
),
() => Assert.equal(document.activeElement, gURLBar.inputField),
() =>
Assert.equal(
gURLBar.view.selectedElement,
gURLBar.view.getFirstSelectableElement()
),
() =>
Assert.equal(
gURLBar.view.selectedElement,
gURLBar.view.getLastSelectableElement()
),
() =>
Assert.equal(
document.activeElement,
document.getElementById("urlbar-searchmode-switcher")
),
() => Assert.equal(document.activeElement, gURLBar.inputField),
];
for (const shiftKey of [false, true]) {
info("Open urlbar view");
await UrlbarTestUtils.promiseAutocompleteResultPopup({
window,
value: "abc",
});
Assert.equal(document.activeElement, gURLBar.inputField);
Assert.equal(
gURLBar.view.selectedElement,
gURLBar.view.getFirstSelectableElement()
);
let resultCount = UrlbarTestUtils.getResultCount(window);
Assert.equal(resultCount, 2, "This test needs exact 2 results");
for (const assert of shiftKey
? [...FOCUS_ORDER_ASSERTIONS].reverse()
: FOCUS_ORDER_ASSERTIONS) {
EventUtils.synthesizeKey("KEY_Tab", { shiftKey });
assert();
}
}
await PlacesUtils.bookmarks.eraseEverything();
});
add_task(async function nimbusScotchBonnetEnableOverride() { add_task(async function nimbusScotchBonnetEnableOverride() {
info("Setup initial local pref"); info("Setup initial local pref");
let defaultBranch = Services.prefs.getDefaultBranch("browser.urlbar."); let defaultBranch = Services.prefs.getDefaultBranch("browser.urlbar.");