Bug 1811507 - Fix highlighting of dynamic type rows. r=adw

Differential Revision: https://phabricator.services.mozilla.com/D167795
This commit is contained in:
Dão Gottwald
2023-01-27 19:21:10 +00:00
parent ea30079848
commit d7994183f4
13 changed files with 208 additions and 165 deletions

View File

@@ -31,7 +31,7 @@ XPCOMUtils.defineLazyServiceGetter(
);
// Query selector for selectable elements in tip and dynamic results.
const SELECTABLE_ELEMENT_SELECTOR = "[role=button], [selectable=true]";
const SELECTABLE_ELEMENT_SELECTOR = "[role=button], [selectable]";
const ZERO_PREFIX_HISTOGRAM_DWELL_TIME = "FX_URLBAR_ZERO_PREFIX_DWELL_TIME_MS";
const ZERO_PREFIX_SCALAR_ABANDONMENT = "urlbar.zeroprefix.abandonment";
@@ -164,12 +164,12 @@ export class UrlbarView {
throw new Error(`UrlbarView: Index ${val} is out of bounds.`);
}
// If the row itself isn't selectable, select the first element inside it
// that is. If it doesn't contain a selectable element, clear the selection.
let element = items[val];
if (!this.#isSelectableElement(element)) {
let next = this.#getNextSelectableElement(element);
element = this.#getRowFromElement(next) == element ? next : null;
// Select the first selectable element inside the row. If it doesn't
// contain a selectable element, clear the selection.
let row = items[val];
let element = this.#getNextSelectableElement(row);
if (this.#getRowFromElement(element) != row) {
element = null;
}
this.#selectElement(element);
@@ -329,8 +329,9 @@ export class UrlbarView {
// aria-activedescendant so the user doesn't think they have to press
// Enter to enter search mode. See bug 1647929.
const isSkippableTabToSearchAnnounce = selectedElt => {
let result = this.getResultFromElement(selectedElt);
let skipAnnouncement =
selectedElt?.result?.providerName == "TabToSearch" &&
result?.providerName == "TabToSearch" &&
!this.#announceTabToSearchOnSelection &&
lazy.UrlbarPrefs.get("accessibility.tabToSearch.announceResults");
if (skipAnnouncement) {
@@ -1109,6 +1110,21 @@ export class UrlbarView {
item.className = "urlbarView-row";
item._elements = new Map();
item._buttons = new Map();
// A note about row selection. Any element in a row that can be selected
// will have the `selectable` attribute set on it. For typical rows, the
// selectable element is not the `.urlbarView-row` itself but rather the
// `.urlbarView-row-inner` inside it. That's because the `.urlbarView-row`
// also contains the row's buttons, which should not be selected when the
// main part of the row -- `.urlbarView-row-inner` -- is selected.
//
// Since it's the row itself and not the row-inner that is a child of the
// `role=listbox` element (the rows container, `this.#rows`), screen readers
// will not automatically recognize the row-inner as a listbox option. To
// compensate, we set `role=option` on the row-inner and `role=presentation`
// on the row itself so that screen readers ignore it.
item.setAttribute("role", "presentation");
return item;
}
@@ -1187,6 +1203,7 @@ export class UrlbarView {
item._elements,
viewTemplate
);
this.#setRowSelectable(item, item._content.hasAttribute("selectable"));
}
#buildViewForDynamicType(type, parentNode, elementsByName, template) {
@@ -1220,6 +1237,8 @@ export class UrlbarView {
}
#createRowContentForBestMatch(item, result) {
item._content.toggleAttribute("selectable", true);
let favicon = this.#createElement("img");
favicon.className = "urlbarView-favicon";
item._content.appendChild(favicon);
@@ -1304,27 +1323,6 @@ export class UrlbarView {
}
#addRowButton(item, { name, l10n, url }) {
if (!item._buttons.size) {
// If the content is marked as selectable, the screen reader will not be
// able to read the text directly child of the "urlbarView-row". As the
// group label is shown as pseudo element of urlbarView-row now, it isn't
// readable. To avoid it, we add an element for aria-label explictly,
// and set group label that should be read into it in #updateIndices().
const groupAriaLabel = this.#createElement("span");
groupAriaLabel.className = "urlbarView-group-aria-label";
item._content.insertBefore(groupAriaLabel, item._content.firstChild);
item._elements.set("groupAriaLabel", groupAriaLabel);
// When the row has buttons we set role=option on row-inner since the
// latter is the selectable logical row element when buttons are present.
// Since row-inner is not a child of the role=listbox element (the row
// container, this.#rows), screen readers will not automatically recognize
// it as a listbox option. To compensate, set role=presentation on the row
// so that screen readers ignore it.
item.setAttribute("role", "presentation");
item._content.setAttribute("role", "option");
}
let button = this.#createElement("span");
button.id = `${item.id}-button-${name}`;
button.classList.add("urlbarView-button", "urlbarView-button-" + name);
@@ -1373,7 +1371,6 @@ export class UrlbarView {
while (item.lastChild) {
item.lastChild.remove();
}
item.setAttribute("role", "option");
item._elements.clear();
item._buttons.clear();
item._content = this.#createElement("span");
@@ -1556,13 +1553,7 @@ export class UrlbarView {
break;
}
if (isRowSelectable) {
let selectableElement = item._buttons.size ? item._content : item;
selectableElement.setAttribute("selectable", "true");
} else {
item.removeAttribute("selectable");
item._content.removeAttribute("selectable");
}
this.#setRowSelectable(item, isRowSelectable);
if (result.providerName == "TabToSearch") {
action.toggleAttribute("slide-in", true);
@@ -1648,6 +1639,16 @@ export class UrlbarView {
}
}
#setRowSelectable(item, isRowSelectable) {
item.toggleAttribute("row-selectable", isRowSelectable);
item._content.toggleAttribute("selectable", isRowSelectable);
if (isRowSelectable) {
item._content.setAttribute("role", "option");
} else {
item._content.removeAttribute("role");
}
}
#iconForResult(result, iconUrlOverride = null) {
return (
(result.source == lazy.UrlbarUtils.RESULT_SOURCE.HISTORY &&
@@ -1728,8 +1729,7 @@ export class UrlbarView {
}
#updateRowForBestMatch(item, result) {
let selectableElement = item._buttons.size ? item._content : item;
selectableElement.setAttribute("selectable", "true");
this.#setRowSelectable(item, true);
let favicon = item._elements.get("favicon");
favicon.src = this.#iconForResult(result);
@@ -1781,39 +1781,18 @@ export class UrlbarView {
// label in between.) Each row's label is determined by `#rowLabel()`.
let currentLabel;
let visibleRowsExist = false;
for (let i = 0; i < this.#rows.children.length; i++) {
let item = this.#rows.children[i];
item.result.rowIndex = i;
let visible = this.#isElementVisible(item);
visibleRowsExist = visibleRowsExist || visible;
let label;
if (visible) {
this.visibleResults.push(item.result);
label = this.#rowLabel(item, currentLabel);
if (label) {
if (lazy.ObjectUtils.deepEqual(label, currentLabel)) {
label = null;
} else {
currentLabel = label;
}
}
}
const groupAriaLabel = item._elements.get("groupAriaLabel");
if (label) {
this.#setElementL10n(item, {
attribute: "label",
id: label.id,
args: label.args,
});
groupAriaLabel?.setAttribute("aria-label", item.getAttribute("label"));
} else {
this.#removeElementL10n(item, { attribute: "label" });
groupAriaLabel?.removeAttribute("aria-label");
let newLabel = this.#updateRowLabel(item, visible, currentLabel);
if (newLabel) {
currentLabel = newLabel;
}
}
@@ -1823,13 +1802,79 @@ export class UrlbarView {
selectableElement.elementIndex = uiIndex++;
selectableElement = this.#getNextSelectableElement(selectableElement);
}
if (visibleRowsExist) {
if (this.visibleResults.length) {
this.panel.removeAttribute("noresults");
} else {
this.panel.setAttribute("noresults", "true");
}
}
/**
* Sets or removes the group label from a row. Designed to be called
* iteratively over each row.
*
* @param {Element} item
* A row in the view.
* @param {boolean} isVisible
* Whether the row is visible. This can be computed by the method itself,
* but it's a parameter as an optimization since the caller is expected to
* know it.
* @param {object} currentLabel
* The current group label during row iteration.
* @returns {object}
* If the given row should not have a label, returns null. Otherwise returns
* an l10n object for the label's l10n string: `{ id, args }`
*/
#updateRowLabel(item, isVisible, currentLabel) {
let label;
if (isVisible) {
label = this.#rowLabel(item, currentLabel);
if (label && lazy.ObjectUtils.deepEqual(label, currentLabel)) {
label = null;
}
}
// When the row-inner is selected, screen readers won't naturally read the
// label because it's a pseudo-element of the row, not the row-inner. To
// compensate, for rows that have labels we add an element to the row-inner
// with `aria-label` and no text content. Rows that don't have labels won't
// have this element.
let groupAriaLabel = item._elements.get("groupAriaLabel");
if (!label) {
this.#removeElementL10n(item, { attribute: "label" });
if (groupAriaLabel) {
groupAriaLabel.remove();
item._elements.delete("groupAriaLabel");
}
return null;
}
this.#setElementL10n(item, {
attribute: "label",
id: label.id,
args: label.args,
});
if (!groupAriaLabel) {
groupAriaLabel = this.#createElement("span");
groupAriaLabel.className = "urlbarView-group-aria-label";
item._content.insertBefore(groupAriaLabel, item._content.firstChild);
item._elements.set("groupAriaLabel", groupAriaLabel);
}
// `aria-label` must be a string, not an l10n ID, so first fetch the
// localized value and then set it as the attribute. There's no relevant
// aria attribute that uses l10n IDs.
this.#l10nCache.ensure(label).then(() => {
let message = this.#l10nCache.get(label);
groupAriaLabel.setAttribute("aria-label", message?.attributes.label);
});
return label;
}
/**
* Returns the group label to use for a row. Designed to be called iteratively
* over each row.
@@ -1967,7 +2012,9 @@ export class UrlbarView {
if (element) {
element.toggleAttribute("selected", true);
element.setAttribute("aria-selected", "true");
row?.toggleAttribute("selected", true);
if (row?.hasAttribute("row-selectable")) {
row?.toggleAttribute("selected", true);
}
}
this.#setAccessibleFocus(setAccessibleFocus && element);
this.#selectedElement = element;