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 }
|
{ 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) {
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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.");
|
||||||
|
|||||||
Reference in New Issue
Block a user