зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1575749: Implement toolbar navigation by typed character. r=Gijs
When focused on a toolbar button, users can now type the first (or first few) characters of another button's name to jump directly to that button. The search characters are cleared after 1 second or if a non-character key is pressed. This is similar to the typed character navigation implemented for HTML select controls. Differential Revision: https://phabricator.services.mozilla.com/D43187 --HG-- extra : moz-landing-system : lando
This commit is contained in:
Родитель
f03bae30af
Коммит
681ed6270e
|
@ -19,11 +19,17 @@
|
|||
* these gets focus, it redirects focus to the appropriate button. This avoids
|
||||
* the need to continually manage the tabindex of toolbar buttons in response to
|
||||
* toolbarchanges.
|
||||
* In addition to linear navigation with tab and arrows, users can also type
|
||||
* the first (or first few) characters of a button's name to jump directly to
|
||||
* that button.
|
||||
*/
|
||||
|
||||
ToolbarKeyboardNavigator = {
|
||||
// Toolbars we want to be keyboard navigable.
|
||||
kToolbars: [CustomizableUI.AREA_NAVBAR, CustomizableUI.AREA_BOOKMARKS],
|
||||
// Delay (in ms) after which to clear any search text typed by the user if
|
||||
// the user hasn't typed anything further.
|
||||
kSearchClearTimeout: 1000,
|
||||
|
||||
_isButton(aElem) {
|
||||
return (
|
||||
|
@ -194,6 +200,20 @@ ToolbarKeyboardNavigator = {
|
|||
|
||||
_onKeyDown(aEvent) {
|
||||
let focus = document.activeElement;
|
||||
if (
|
||||
aEvent.key != " " &&
|
||||
aEvent.key.length == 1 &&
|
||||
this._isButton(focus) &&
|
||||
// Don't handle characters if the user is focused in a panel anchored
|
||||
// to the toolbar.
|
||||
!focus.closest("panel")
|
||||
) {
|
||||
this._onSearchChar(aEvent.currentTarget, aEvent.key);
|
||||
return;
|
||||
}
|
||||
// Anything that doesn't trigger search should clear the search.
|
||||
this._clearSearch();
|
||||
|
||||
if (
|
||||
aEvent.altKey ||
|
||||
aEvent.controlKey ||
|
||||
|
@ -219,6 +239,83 @@ ToolbarKeyboardNavigator = {
|
|||
aEvent.preventDefault();
|
||||
},
|
||||
|
||||
_clearSearch() {
|
||||
this._searchText = "";
|
||||
if (this._clearSearchTimeout) {
|
||||
clearTimeout(this._clearSearchTimeout);
|
||||
this._clearSearchTimeout = null;
|
||||
}
|
||||
},
|
||||
|
||||
_onSearchChar(aToolbar, aChar) {
|
||||
if (this._clearSearchTimeout) {
|
||||
// The user just typed a character, so reset the timer.
|
||||
clearTimeout(this._clearSearchTimeout);
|
||||
}
|
||||
// Convert to lower case so we can do case insensitive searches.
|
||||
let char = aChar.toLowerCase();
|
||||
// If the user has only typed a single character and they type the same
|
||||
// character again, they want to move to the next item starting with that
|
||||
// same character. Effectively, it's as if there was no existing search.
|
||||
// In that case, we just leave this._searchText alone.
|
||||
if (!this._searchText) {
|
||||
this._searchText = char;
|
||||
} else if (this._searchText != char) {
|
||||
this._searchText += char;
|
||||
}
|
||||
// Clear the search if the user doesn't type anything more within the timeout.
|
||||
this._clearSearchTimeout = setTimeout(
|
||||
this._clearSearch.bind(this),
|
||||
this.kSearchClearTimeout
|
||||
);
|
||||
|
||||
let oldFocus = document.activeElement;
|
||||
let walker = this._getWalker(aToolbar);
|
||||
// Search forward after the current control.
|
||||
walker.currentNode = oldFocus;
|
||||
for (
|
||||
let newFocus = walker.nextNode();
|
||||
newFocus;
|
||||
newFocus = walker.nextNode()
|
||||
) {
|
||||
if (this._doesSearchMatch(newFocus)) {
|
||||
this._focusButton(newFocus);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// No match, so search from the start until the current control.
|
||||
walker.currentNode = walker.root;
|
||||
for (
|
||||
let newFocus = walker.firstChild();
|
||||
newFocus && newFocus != oldFocus;
|
||||
newFocus = walker.nextNode()
|
||||
) {
|
||||
if (this._doesSearchMatch(newFocus)) {
|
||||
this._focusButton(newFocus);
|
||||
return;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_doesSearchMatch(aElem) {
|
||||
if (!this._isButton(aElem)) {
|
||||
return false;
|
||||
}
|
||||
for (let attrib of ["aria-label", "label", "tooltiptext"]) {
|
||||
let label = aElem.getAttribute(attrib);
|
||||
if (!label) {
|
||||
continue;
|
||||
}
|
||||
// Convert to lower case so we do a case insensitive comparison.
|
||||
// (this._searchText is already lower case.)
|
||||
label = label.toLowerCase();
|
||||
if (label.startsWith(this._searchText)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
_onKeyPress(aEvent) {
|
||||
let focus = document.activeElement;
|
||||
if (
|
||||
|
|
|
@ -403,3 +403,52 @@ add_task(async function testArrowKeyForTPIconContainerandIdentityBox() {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
// Test navigation by typed characters.
|
||||
add_task(async function testCharacterNavigation() {
|
||||
await BrowserTestUtils.withNewTab("https://example.com", async function() {
|
||||
await waitUntilReloadEnabled();
|
||||
startFromUrlBar();
|
||||
await expectFocusAfterKey("Tab", "pageActionButton");
|
||||
await expectFocusAfterKey("h", "home-button");
|
||||
// There's no button starting with "hs", so pressing s should do nothing.
|
||||
EventUtils.synthesizeKey("s");
|
||||
is(
|
||||
document.activeElement.id,
|
||||
"home-button",
|
||||
"home-button still focused after s pressed"
|
||||
);
|
||||
// Escape should reset the search.
|
||||
EventUtils.synthesizeKey("KEY_Escape");
|
||||
// Now that the search is reset, pressing s should focus Save to Pocket.
|
||||
await expectFocusAfterKey("s", "pocket-button");
|
||||
// Pressing i makes the search "si", so it should focus Sidebars.
|
||||
await expectFocusAfterKey("i", "sidebar-button");
|
||||
// Reset the search.
|
||||
EventUtils.synthesizeKey("KEY_Escape");
|
||||
await expectFocusAfterKey("s", "pocket-button");
|
||||
// Pressing s again should find the next button starting with s: Sidebars.
|
||||
await expectFocusAfterKey("s", "sidebar-button");
|
||||
});
|
||||
});
|
||||
|
||||
// Test that toolbar character navigation doesn't trigger in PanelMultiView for
|
||||
// a panel anchored to the toolbar.
|
||||
// We do this by opening the Library menu and ensuring that pressing s
|
||||
// does nothing.
|
||||
// This test should be removed if PanelMultiView implements character
|
||||
// navigation.
|
||||
add_task(async function testCharacterInPanelMultiView() {
|
||||
let button = document.getElementById("library-button");
|
||||
forceFocus(button);
|
||||
let view = document.getElementById("appMenu-libraryView");
|
||||
let focused = BrowserTestUtils.waitForEvent(view, "focus", true);
|
||||
EventUtils.synthesizeKey(" ");
|
||||
let focusEvt = await focused;
|
||||
ok(true, "Focus inside Library menu after toolbar button pressed");
|
||||
EventUtils.synthesizeKey("s");
|
||||
is(document.activeElement, focusEvt.target, "s inside panel does nothing");
|
||||
let hidden = BrowserTestUtils.waitForEvent(document, "popuphidden", true);
|
||||
view.closest("panel").hidePopup();
|
||||
await hidden;
|
||||
});
|
||||
|
|
Загрузка…
Ссылка в новой задаче