Bug 1830909 - Implement <hr> in <select> r=hsivonen,emilio,geckoview-reviewers,desktop-theme-reviewers,Jamie,owlish

Updated HTML parser to allow <hr> in <select>.

Updated internal toolkit UI for <select> dropdown to create
menuseperators for hrs.

Updated WPT expectations:
 - HTML5Lib WebKit parsing for it now passes 100%

Also includes Android support, but Fenix does not support separators
in the menus used (single/multiple) yet so they are not rendered.

Differential Revision: https://phabricator.services.mozilla.com/D189065
This commit is contained in:
CanadaHonk 2023-11-29 14:15:36 +00:00
Родитель a0c893d3ff
Коммит b7cb7a41f5
10 изменённых файлов: 195 добавлений и 119 удалений

Просмотреть файл

@ -32,3 +32,5 @@ fail-if = ["a11y_checks"] # Bug 1854233 select may not be labeled
["browser_selectpopup_width.js"]
["browser_selectpopup_xhtml.js"]
["browser_selectpopup_hr.js"]

Просмотреть файл

@ -0,0 +1,55 @@
add_task(async function test_hr() {
await SpecialPowers.pushPrefEnv({
set: [["dom.forms.select.customstyling", true]],
});
const PAGE_CONTENT = `
<!doctype html>
<select>
<option>One</option>
<hr style="color: red; background-color: blue">
<option>Two</option>
</select>`;
const pageUrl = "data:text/html," + encodeURIComponent(PAGE_CONTENT);
let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl);
const selectPopup = await openSelectPopup("click");
const menulist = selectPopup.parentNode;
const optionOne = selectPopup.children[0];
const separator = selectPopup.children[1];
const optionTwo = selectPopup.children[2];
is(optionOne.textContent, "One", "First option has expected text content");
is(separator.tagName, "menuseparator", "Separator is menuseparator");
const separatorStyle = getComputedStyle(separator);
is(
separatorStyle.color,
"rgb(255, 0, 0)",
"Separator color is specified CSS color"
);
is(
separatorStyle.backgroundColor,
"rgba(0, 0, 0, 0)",
"Separator background-color is not set to specified CSS color"
);
is(optionTwo.textContent, "Two", "Second option has expected text content");
is(menulist.activeChild, optionOne, "First option is selected to start");
EventUtils.synthesizeKey("KEY_ArrowDown");
is(
menulist.activeChild,
optionTwo,
"Second option is selected after arrow down"
);
BrowserTestUtils.removeTab(tab);
});

Просмотреть файл

@ -101,6 +101,8 @@ export class PromptFactory {
} else if (win.HTMLOptionElement.isInstance(child)) {
item.label = child.label || child.text;
item.selected = child.selected;
} else if (win.HTMLHRElement.isInstance(child)) {
item.separator = true;
} else {
continue;
}

Просмотреть файл

@ -2573,6 +2573,20 @@ public abstract class TreeBuilder<T> implements TokenHandler,
startTagTemplateInHead(elementName, attributes);
attributes = null; // CPP
break starttagloop;
case HR:
if (isCurrent("option")) {
pop();
}
if (isCurrent("optgroup")) {
pop();
}
appendVoidElementToCurrent(elementName, attributes);
selfClosing = false;
// [NOCPP[
voidElement = true;
// ]NOCPP]
attributes = null; // CPP
break starttagloop;
default:
errStrayStartTag(name);
break starttagloop;
@ -5457,6 +5471,25 @@ public abstract class TreeBuilder<T> implements TokenHandler,
push(node);
}
private void appendVoidElementToCurrent(
ElementName elementName, HtmlAttributes attributes)
throws SAXException {
@Local String popName = elementName.getName();
// [NOCPP[
checkAttributes(attributes, "http://www.w3.org/1999/xhtml");
if (!elementName.isInterned()) {
popName = checkPopName(popName);
}
// ]NOCPP]
T currentNode = nodeFromStackWithBlinkCompat(currentPtr);
T elt = createElement("http://www.w3.org/1999/xhtml", popName, attributes, currentNode
// CPPONLY: , htmlCreator(elementName.getHtmlCreator())
);
appendElement(elt, currentNode);
elementPushed("http://www.w3.org/1999/xhtml", popName, elt);
elementPopped("http://www.w3.org/1999/xhtml", popName, elt);
}
private void appendVoidElementToCurrentMayFoster(
ElementName elementName, HtmlAttributes attributes, T form) throws SAXException {
@Local String name = elementName.getName();

Просмотреть файл

@ -1759,6 +1759,18 @@ starttagloop:
attributes = nullptr;
NS_HTML5_BREAK(starttagloop);
}
case HR: {
if (isCurrent(nsGkAtoms::option)) {
pop();
}
if (isCurrent(nsGkAtoms::optgroup)) {
pop();
}
appendVoidElementToCurrent(elementName, attributes);
selfClosing = false;
attributes = nullptr;
NS_HTML5_BREAK(starttagloop);
}
default: {
errStrayStartTag(name);
NS_HTML5_BREAK(starttagloop);
@ -4311,6 +4323,18 @@ void nsHtml5TreeBuilder::appendToCurrentNodeAndPushElementMayFoster(
push(node);
}
void nsHtml5TreeBuilder::appendVoidElementToCurrent(
nsHtml5ElementName* elementName, nsHtml5HtmlAttributes* attributes) {
nsAtom* popName = elementName->getName();
nsIContentHandle* currentNode = nodeFromStackWithBlinkCompat(currentPtr);
nsIContentHandle* elt =
createElement(kNameSpaceID_XHTML, popName, attributes, currentNode,
htmlCreator(elementName->getHtmlCreator()));
appendElement(elt, currentNode);
elementPushed(kNameSpaceID_XHTML, popName, elt);
elementPopped(kNameSpaceID_XHTML, popName, elt);
}
void nsHtml5TreeBuilder::appendVoidElementToCurrentMayFoster(
nsHtml5ElementName* elementName, nsHtml5HtmlAttributes* attributes,
nsIContentHandle* form) {

Просмотреть файл

@ -477,6 +477,8 @@ class nsHtml5TreeBuilder : public nsAHtml5TreeBuilderState {
void appendToCurrentNodeAndPushElementMayFoster(
nsHtml5ElementName* elementName, nsHtml5HtmlAttributes* attributes,
nsIContentHandle* form);
void appendVoidElementToCurrent(nsHtml5ElementName* elementName,
nsHtml5HtmlAttributes* attributes);
void appendVoidElementToCurrentMayFoster(nsHtml5ElementName* elementName,
nsHtml5HtmlAttributes* attributes,
nsIContentHandle* form);

Просмотреть файл

@ -1,100 +0,0 @@
[html5lib_webkit02.html?run_type=write]
expected:
if (os == "android") and fission: [TIMEOUT, OK]
[html5lib_webkit02.html 5463526d91a8677b27b6967866d6605f1bb03aac]
expected: FAIL
[html5lib_webkit02.html 4879f476053094cf5602d325724675378856a902]
expected: FAIL
[html5lib_webkit02.html 44c88b90236f01ebc8e0123363b527640a07070c]
expected: FAIL
[html5lib_webkit02.html cfb304e8f2d3cbdecc362226e7775cab452d5489]
expected: FAIL
[html5lib_webkit02.html 3fc625e7cb9b6ea72a9e252ede84c6fdd9680d87]
expected: FAIL
[html5lib_webkit02.html ecd089f9b5193fad306c5b475c4711547fe5e209]
expected: FAIL
[html5lib_webkit02.html cee2230c74671c594a1140a68d16e3d3e5ae005a]
expected: FAIL
[html5lib_webkit02.html 22b9fe36797d70a3b71a6aadc6ad7cff23c3fc90]
expected: FAIL
[html5lib_webkit02.html a82c3bf49c381b5f58c5c8a4bbbe0cef2458e28a]
expected: FAIL
[html5lib_webkit02.html 61f8d527795dc8044a95a3e2437de81e16597ceb]
expected: FAIL
[html5lib_webkit02.html?run_type=uri]
expected:
if (os == "android") and fission: [OK, TIMEOUT]
[html5lib_webkit02.html 5463526d91a8677b27b6967866d6605f1bb03aac]
expected: FAIL
[html5lib_webkit02.html 4879f476053094cf5602d325724675378856a902]
expected: FAIL
[html5lib_webkit02.html 44c88b90236f01ebc8e0123363b527640a07070c]
expected: FAIL
[html5lib_webkit02.html cfb304e8f2d3cbdecc362226e7775cab452d5489]
expected: FAIL
[html5lib_webkit02.html 3fc625e7cb9b6ea72a9e252ede84c6fdd9680d87]
expected: FAIL
[html5lib_webkit02.html ecd089f9b5193fad306c5b475c4711547fe5e209]
expected: FAIL
[html5lib_webkit02.html cee2230c74671c594a1140a68d16e3d3e5ae005a]
expected: FAIL
[html5lib_webkit02.html 22b9fe36797d70a3b71a6aadc6ad7cff23c3fc90]
expected: FAIL
[html5lib_webkit02.html a82c3bf49c381b5f58c5c8a4bbbe0cef2458e28a]
expected: FAIL
[html5lib_webkit02.html 61f8d527795dc8044a95a3e2437de81e16597ceb]
expected: FAIL
[html5lib_webkit02.html?run_type=write_single]
expected:
if (os == "android") and fission: [OK, TIMEOUT]
[html5lib_webkit02.html 5463526d91a8677b27b6967866d6605f1bb03aac]
expected: FAIL
[html5lib_webkit02.html 4879f476053094cf5602d325724675378856a902]
expected: FAIL
[html5lib_webkit02.html 44c88b90236f01ebc8e0123363b527640a07070c]
expected: FAIL
[html5lib_webkit02.html cfb304e8f2d3cbdecc362226e7775cab452d5489]
expected: FAIL
[html5lib_webkit02.html 3fc625e7cb9b6ea72a9e252ede84c6fdd9680d87]
expected: FAIL
[html5lib_webkit02.html ecd089f9b5193fad306c5b475c4711547fe5e209]
expected: FAIL
[html5lib_webkit02.html cee2230c74671c594a1140a68d16e3d3e5ae005a]
expected: FAIL
[html5lib_webkit02.html 22b9fe36797d70a3b71a6aadc6ad7cff23c3fc90]
expected: FAIL
[html5lib_webkit02.html a82c3bf49c381b5f58c5c8a4bbbe0cef2458e28a]
expected: FAIL
[html5lib_webkit02.html 61f8d527795dc8044a95a3e2437de81e16597ceb]
expected: FAIL

Просмотреть файл

@ -382,17 +382,46 @@ function uniqueStylesIndex(cs, uniqueStyles) {
function buildOptionListForChildren(node, uniqueStyles) {
let result = [];
let lastWasHR = false;
for (let child of node.children) {
let className = ChromeUtils.getClassName(child);
let isOption = className == "HTMLOptionElement";
let isOptGroup = className == "HTMLOptGroupElement";
if (!isOption && !isOptGroup) {
let isHR = className == "HTMLHRElement";
if (!isOption && !isOptGroup && !isHR) {
continue;
}
if (child.hidden) {
continue;
}
let cs = getComputedStyles(child);
if (isHR) {
// https://html.spec.whatwg.org/#the-select-element-2
// "Each sequence of one or more child hr element siblings may be rendered as a single separator."
if (lastWasHR) {
continue;
}
let info = {
index: child.index,
display: cs.display,
isHR,
};
const defaultHRStyle = node.ownerGlobal.getDefaultComputedStyle(child);
if (cs.color != defaultHRStyle.color) {
info.color = cs.color;
}
result.push(info);
lastWasHR = true;
continue;
}
lastWasHR = false;
// The option code-path should match HTMLOptionElement::GetRenderedLabel.
let textContent = isOptGroup
? child.getAttribute("label")
@ -401,7 +430,6 @@ function buildOptionListForChildren(node, uniqueStyles) {
textContent = "";
}
let cs = getComputedStyles(child);
let info = {
index: child.index,
isOptGroup,

Просмотреть файл

@ -274,7 +274,13 @@ export var SelectParentHelper = {
this._currentZoom = zoom;
this._currentMenulist = menulist;
this.populateChildren(menulist, items, uniqueItemStyles, selectedIndex);
this.populateChildren(
menulist,
custom,
items,
uniqueItemStyles,
selectedIndex
);
},
open(browser, menulist, rect, isOpenedViaTouch, selectParentActor) {
@ -476,6 +482,7 @@ export var SelectParentHelper = {
*/
populateChildren(
menulist,
custom,
options,
uniqueOptionStyles,
selectedIndex,
@ -489,26 +496,19 @@ export var SelectParentHelper = {
let ariaOwns = "";
for (let option of options) {
let isOptGroup = option.isOptGroup;
let item = element.ownerDocument.createXULElement(
isOptGroup ? "menucaption" : "menuitem"
);
let isHR = option.isHR;
let xulElement = "menuitem";
if (isOptGroup) {
item.setAttribute("role", "group");
xulElement = "menucaption";
}
item.setAttribute("label", option.textContent);
item.className = `ContentSelectDropdown-item-${option.styleIndex}`;
if (isHR) {
xulElement = "menuseparator";
}
let item = element.ownerDocument.createXULElement(xulElement);
item.hidden =
option.display == "none" || (parentElement && parentElement.hidden);
// Keep track of which options are hidden by page content, so we can avoid
// showing them on search input.
item.hiddenByContent = item.hidden;
item.setAttribute("tooltiptext", option.tooltip);
if (uniqueOptionStyles[option.styleIndex].customStyling) {
item.setAttribute("customoptionstyling", "true");
} else {
item.removeAttribute("customoptionstyling");
}
if (parentElement) {
// In the menupopup, the optgroup is a sibling of its contained options.
@ -523,6 +523,30 @@ export var SelectParentHelper = {
element.appendChild(item);
nthChildIndex++;
if (isHR) {
item.style.color = (custom && option.color) || "";
// Continue early as HRs do not have other attributes.
continue;
}
item.className = `ContentSelectDropdown-item-${option.styleIndex}`;
if (isOptGroup) {
item.setAttribute("role", "group");
}
item.setAttribute("label", option.textContent);
// Keep track of which options are hidden by page content, so we can avoid
// showing them on search input.
item.hiddenByContent = item.hidden;
item.setAttribute("tooltiptext", option.tooltip);
if (uniqueOptionStyles[option.styleIndex].customStyling) {
item.setAttribute("customoptionstyling", "true");
} else {
item.removeAttribute("customoptionstyling");
}
// A disabled optgroup disables all of its child options.
let isDisabled = isGroupDisabled || option.disabled;
if (isDisabled) {
@ -532,6 +556,7 @@ export var SelectParentHelper = {
if (isOptGroup) {
nthChildIndex = this.populateChildren(
menulist,
custom,
option.children,
uniqueOptionStyles,
selectedIndex,

Просмотреть файл

@ -323,6 +323,11 @@ button.text-link .button-text {
background-color: -moz-Combobox;
}
/* Full width separator in select */
#ContentSelectDropdown menuseparator {
padding-inline: 0;
}
/* Indent options in optgroups */
.contentSelectDropdown-ingroup .menu-iconic-text {
padding-inline-start: 2em;