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:
@@ -104,6 +104,16 @@ export class SearchModeSwitcher {
|
||||
{ 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, {
|
||||
position: "bottomleft topleft",
|
||||
triggerEvent: event,
|
||||
@@ -150,6 +160,19 @@ export class SearchModeSwitcher {
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
switch (action) {
|
||||
|
||||
@@ -348,6 +348,36 @@ export class UrlbarController {
|
||||
event.preventDefault();
|
||||
break;
|
||||
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
|
||||
// focused with the mouse or has a search string, or when the view
|
||||
// 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -380,10 +380,10 @@ export class UrlbarView {
|
||||
|
||||
// We cache the first and last rows since they will not change while
|
||||
// selectBy is running.
|
||||
let firstSelectableElement = this.#getFirstSelectableElement();
|
||||
// #getLastSelectableElement will not return an element that is over
|
||||
let firstSelectableElement = this.getFirstSelectableElement();
|
||||
// getLastSelectableElement will not return an element that is over
|
||||
// maxResults and thus may be hidden and not selectable.
|
||||
let lastSelectableElement = this.#getLastSelectableElement();
|
||||
let lastSelectableElement = this.getLastSelectableElement();
|
||||
|
||||
if (!selectedElement) {
|
||||
selectedElement = reverse
|
||||
@@ -803,7 +803,7 @@ export class UrlbarView {
|
||||
// result added, which is why we do this check here when each result is
|
||||
// added and not above.
|
||||
if (this.#shouldShowHeuristic(firstResult)) {
|
||||
this.#selectElement(this.#getFirstSelectableElement(), {
|
||||
this.#selectElement(this.getFirstSelectableElement(), {
|
||||
updateInput: false,
|
||||
setAccessibleFocus:
|
||||
this.controller._userSelectionBehavior == "arrow",
|
||||
@@ -2243,7 +2243,7 @@ export class UrlbarView {
|
||||
}
|
||||
}
|
||||
|
||||
let selectableElement = this.#getFirstSelectableElement();
|
||||
let selectableElement = this.getFirstSelectableElement();
|
||||
let uiIndex = 0;
|
||||
while (selectableElement) {
|
||||
selectableElement.elementIndex = uiIndex++;
|
||||
@@ -2593,7 +2593,7 @@ export class UrlbarView {
|
||||
* @returns {Element}
|
||||
* The first selectable element in the view.
|
||||
*/
|
||||
#getFirstSelectableElement() {
|
||||
getFirstSelectableElement() {
|
||||
let element = this.#rows.firstElementChild;
|
||||
if (element && !this.#isSelectableElement(element)) {
|
||||
element = this.#getNextSelectableElement(element);
|
||||
@@ -2607,7 +2607,7 @@ export class UrlbarView {
|
||||
* @returns {Element}
|
||||
* The last selectable element in the view.
|
||||
*/
|
||||
#getLastSelectableElement() {
|
||||
getLastSelectableElement() {
|
||||
let element = this.#rows.lastElementChild;
|
||||
if (element && !this.#isSelectableElement(element)) {
|
||||
element = this.#getPreviousSelectableElement(element);
|
||||
|
||||
@@ -242,6 +242,7 @@ async function test_navigate_switcher(navKey, navTimes, searchMode) {
|
||||
EventUtils.synthesizeKey(navKey);
|
||||
}
|
||||
EventUtils.synthesizeKey("KEY_Enter");
|
||||
await UrlbarTestUtils.promiseSearchComplete(window);
|
||||
|
||||
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() {
|
||||
info("Setup initial local pref");
|
||||
let defaultBranch = Services.prefs.getDefaultBranch("browser.urlbar.");
|
||||
|
||||
Reference in New Issue
Block a user