0

Remove display:block!important from customizable_select.css

1. Call UpdateStyleAndLayoutTree from IsAppearanceBaseButton and
   IsAppearanceBasePicker, unless no_update is set for cases which are
   calling from within a style/layout recalc.
2. Call EnsureComputedStyle when checking IsAppearanceBasePicker in
   order to get an appearance value despite the element being
   display:none due to it being a closed popover.

Since the picker might no longer have a computed style all the time,
this requires rewriting some accessibility code which was depending on
always having a computed style for the picker. I rewrote the
accessibility code to be more consistent across appearance:base-select
mode and appearance:auto mode by consistently using the UA popover
element as the MenuListPopup mapped element.

Bug: 364348901
Fixed: 364924715
Change-Id: I3fec57f2e02ce4200c95c256e5c1072b5c21ef8e
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5953628
Reviewed-by: David Baron <dbaron@chromium.org>
Reviewed-by: Philip Rogers <pdr@chromium.org>
Commit-Queue: David Baron <dbaron@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1399296}
This commit is contained in:
Joey Arhar
2024-12-20 11:31:50 -08:00
committed by Chromium LUCI CQ
parent 9540f36858
commit 2313906f2c
22 changed files with 338 additions and 115 deletions

@ -198,6 +198,45 @@ class SummaryAsHeadingDumpAccessibilityTreeTest
base::test::ScopedFeatureList feature_list_;
};
class CustomizableSelectEnabledDumpAccessibilityTreeTest
: public DumpAccessibilityTreeTest {
protected:
CustomizableSelectEnabledDumpAccessibilityTreeTest() {
feature_list_.InitWithFeatures({{blink::features::kCustomizableSelect,
blink::features::kSelectParserRelaxation}},
{/* disabled_features */});
}
~CustomizableSelectEnabledDumpAccessibilityTreeTest() override {
// Ensure that the feature lists are destroyed in the same order they
// were created in.
scoped_feature_list_.Reset();
feature_list_.Reset();
}
private:
base::test::ScopedFeatureList feature_list_;
};
class CustomizableSelectDisabledDumpAccessibilityTreeTest
: public DumpAccessibilityTreeTest {
protected:
CustomizableSelectDisabledDumpAccessibilityTreeTest() {
feature_list_.InitWithFeatures({/* enabled_features */},
{{blink::features::kCustomizableSelect}});
}
~CustomizableSelectDisabledDumpAccessibilityTreeTest() override {
// Ensure that the feature lists are destroyed in the same order they
// were created in.
scoped_feature_list_.Reset();
feature_list_.Reset();
}
private:
base::test::ScopedFeatureList feature_list_;
};
// TODO(crbug.com/40925629): We need to create a way to incrementally
// enable and create UIA tests.
INSTANTIATE_TEST_SUITE_P(
@ -224,6 +263,18 @@ INSTANTIATE_TEST_SUITE_P(
::testing::ValuesIn(DumpAccessibilityTestBase::TreeTestPasses()),
DumpAccessibilityTreeTestPassToString());
INSTANTIATE_TEST_SUITE_P(
All,
CustomizableSelectEnabledDumpAccessibilityTreeTest,
::testing::ValuesIn(DumpAccessibilityTestBase::TreeTestPasses()),
DumpAccessibilityTreeTestPassToString());
INSTANTIATE_TEST_SUITE_P(
All,
CustomizableSelectDisabledDumpAccessibilityTreeTest,
::testing::ValuesIn(DumpAccessibilityTestBase::TreeTestPasses()),
DumpAccessibilityTreeTestPassToString());
IN_PROC_BROWSER_TEST_P(DumpAccessibilityTreeTest, AccessibilityCSSAltText) {
RunCSSTest(FILE_PATH_LITERAL("alt-text.html"));
}
@ -2156,6 +2207,11 @@ IN_PROC_BROWSER_TEST_P(DumpAccessibilityTreeTest,
RunHtmlTest(FILE_PATH_LITERAL("custom-select-open.html"));
}
IN_PROC_BROWSER_TEST_P(CustomizableSelectEnabledDumpAccessibilityTreeTest,
AccessibilityCustomSelectMixedOptions) {
RunHtmlTest(FILE_PATH_LITERAL("custom-select-mixed-options.html"));
}
IN_PROC_BROWSER_TEST_P(DumpAccessibilityTreeTest, AccessibilityDd) {
RunHtmlTest(FILE_PATH_LITERAL("dd.html"));
}
@ -3516,7 +3572,18 @@ IN_PROC_BROWSER_TEST_P(DumpAccessibilityTreeTest,
RunHtmlTest(FILE_PATH_LITERAL("selection-container.html"));
}
IN_PROC_BROWSER_TEST_P(DumpAccessibilityTreeTest, AccessibilitySelect) {
// Times out on android: https://issues.chromium.org/issues/384959329
#if BUILDFLAG(IS_ANDROID)
#define MAYBE_AccessibilitySelect DISABLED_AccessibiltySelect
#else
#define MAYBE_AccessibilitySelect ENABLED_AccessibiltySelect
#endif
IN_PROC_BROWSER_TEST_P(CustomizableSelectEnabledDumpAccessibilityTreeTest,
MAYBE_AccessibilitySelect) {
RunHtmlTest(FILE_PATH_LITERAL("select.html"));
}
IN_PROC_BROWSER_TEST_P(CustomizableSelectDisabledDumpAccessibilityTreeTest,
AccessibilitySelect) {
RunHtmlTest(FILE_PATH_LITERAL("select.html"));
}
@ -3526,7 +3593,11 @@ IN_PROC_BROWSER_TEST_P(DumpAccessibilityTreeTest, AccessibilitySelect) {
#else
#define MAYBE_AccessibilitySelectOpen AccessibilitySelectOpen
#endif
IN_PROC_BROWSER_TEST_P(DumpAccessibilityTreeTest,
IN_PROC_BROWSER_TEST_P(CustomizableSelectEnabledDumpAccessibilityTreeTest,
MAYBE_AccessibilitySelectOpen) {
RunHtmlTest(FILE_PATH_LITERAL("select-open.html"));
}
IN_PROC_BROWSER_TEST_P(CustomizableSelectDisabledDumpAccessibilityTreeTest,
MAYBE_AccessibilitySelectOpen) {
RunHtmlTest(FILE_PATH_LITERAL("select-open.html"));
}
@ -3540,18 +3611,31 @@ IN_PROC_BROWSER_TEST_P(YieldingParserDumpAccessibilityTreeTest,
RunHtmlTest(FILE_PATH_LITERAL("select-in-canvas.html"));
}
IN_PROC_BROWSER_TEST_P(DumpAccessibilityTreeTest,
IN_PROC_BROWSER_TEST_P(CustomizableSelectEnabledDumpAccessibilityTreeTest,
AccessibilitySelectFollowsFocus) {
RunHtmlTest(FILE_PATH_LITERAL("select-follows-focus.html"));
}
IN_PROC_BROWSER_TEST_P(CustomizableSelectDisabledDumpAccessibilityTreeTest,
AccessibilitySelectFollowsFocus) {
RunHtmlTest(FILE_PATH_LITERAL("select-follows-focus.html"));
}
IN_PROC_BROWSER_TEST_P(DumpAccessibilityTreeTest,
IN_PROC_BROWSER_TEST_P(CustomizableSelectEnabledDumpAccessibilityTreeTest,
AccessibilitySelectFollowsFocusAriaSelectedFalse) {
RunHtmlTest(
FILE_PATH_LITERAL("select-follows-focus-aria-selected-false.html"));
}
IN_PROC_BROWSER_TEST_P(CustomizableSelectDisabledDumpAccessibilityTreeTest,
AccessibilitySelectFollowsFocusAriaSelectedFalse) {
RunHtmlTest(
FILE_PATH_LITERAL("select-follows-focus-aria-selected-false.html"));
}
IN_PROC_BROWSER_TEST_P(DumpAccessibilityTreeTest,
IN_PROC_BROWSER_TEST_P(CustomizableSelectEnabledDumpAccessibilityTreeTest,
AccessibilitySelectFollowsFocusMultiselect) {
RunHtmlTest(FILE_PATH_LITERAL("select-follows-focus-multiselect.html"));
}
IN_PROC_BROWSER_TEST_P(CustomizableSelectDisabledDumpAccessibilityTreeTest,
AccessibilitySelectFollowsFocusMultiselect) {
RunHtmlTest(FILE_PATH_LITERAL("select-follows-focus-multiselect.html"));
}

@ -3344,6 +3344,9 @@ data/accessibility/html/custom-select-expected-blink.txt
data/accessibility/html/custom-select-expected-mac.txt
data/accessibility/html/custom-select-expected-uia-win.txt
data/accessibility/html/custom-select-expected-win.txt
data/accessibility/html/custom-select-mixed-options-expected-auralinux.txt
data/accessibility/html/custom-select-mixed-options-expected-blink.txt
data/accessibility/html/custom-select-mixed-options.html
data/accessibility/html/custom-select-open-expected-android-assist-data.txt
data/accessibility/html/custom-select-open-expected-android-external.txt
data/accessibility/html/custom-select-open-expected-android.txt

@ -0,0 +1,18 @@
[document web]
++[section]
++++[combo box]
++++++[menu]
++++++++[menu item] name='only text' selectable selected
++++++++[menu item] name='with img ' selectable
++++[combo box]
++++++[menu]
++++++++[menu item] name='only text' selectable selected
++++++++[menu item] name='with img ' selectable
++++++++++[static] name='with img<newline> '
++++++++++[image]
++++[combo box]
++++++[menu]
++++++++[menu item] name='only text' selectable selected
++++++++[menu item] name='with img ' selectable
++++++++++[static] name='with img<newline> '
++++++++++[image]

@ -0,0 +1,21 @@
rootWebArea
++genericContainer ignored
++++genericContainer
++++++comboBoxSelect collapsed value='only text'
++++++++menuListPopup invisible ispopup=auto
++++++++++menuListOption name='only text' selected=true
++++++++++menuListOption invisible name='with img ' selected=false
++++++comboBoxSelect collapsed value='only text'
++++++++menuListPopup invisible ispopup=auto
++++++++++menuListOption name='only text' selected=true
++++++++++menuListOption invisible name='with img ' selected=false
++++++++++++genericContainer ignored invisible
++++++++++++++staticText invisible name='with img<newline> '
++++++++++++++image invisible
++++++comboBoxSelect collapsed value='only text'
++++++++menuListPopup invisible ispopup=auto
++++++++++menuListOption name='only text' selected=true
++++++++++menuListOption invisible name='with img ' selected=false
++++++++++++genericContainer ignored invisible
++++++++++++++staticText invisible name='with img<newline> '
++++++++++++++image invisible

@ -0,0 +1,30 @@
<!DOCTYPE html>
<style>
.custombutton {
appearance: base-select;
}
.custompicker::picker(select) {
appearance: base-select;
}
</style>
<select>
<option>only text</option>
<option>with img
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAAQklEQVQ4T2P8z8AARIQBI1GqGBhA6ohSOmogzkAfDUPc6XHgkg0wXROVsIF5gHB2AqoAqho1EHceGA1D7CEw+JMNALGaMffRFnRtAAAAAElFTkSuQmCC">
</option>
</select>
<select class=custombutton>
<option>only text</option>
<option>with img
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAAQklEQVQ4T2P8z8AARIQBI1GqGBhA6ohSOmogzkAfDUPc6XHgkg0wXROVsIF5gHB2AqoAqho1EHceGA1D7CEw+JMNALGaMffRFnRtAAAAAElFTkSuQmCC">
</option>
</select>
<select class="custombutton custompicker">
<option>only text</option>
<option>with img
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAAQklEQVQ4T2P8z8AARIQBI1GqGBhA6ohSOmogzkAfDUPc6XHgkg0wXROVsIF5gHB2AqoAqho1EHceGA1D7CEw+JMNALGaMffRFnRtAAAAAElFTkSuQmCC">
</option>
</select>

@ -18,6 +18,7 @@ namespace blink {
enum class DocumentUpdateReason {
kAccessibility,
kBaseColor,
kBaseSelect,
kBeginMainFrame,
kCanvas,
kCanvasPlaceElement,

@ -685,6 +685,7 @@ void LocalFrameUkmAggregator::EndForcedLayout(
case DocumentUpdateReason::kAccessibility:
case DocumentUpdateReason::kBaseColor:
case DocumentUpdateReason::kBaseSelect:
case DocumentUpdateReason::kComputedStyle:
case DocumentUpdateReason::kDisplayLock:
case DocumentUpdateReason::kViewTransition:

@ -130,7 +130,8 @@ FocusableState HTMLOptionElement::SupportsFocus(
UpdateBehavior update_behavior) const {
HTMLSelectElement* select = OwnerSelectElement();
if (select && select->UsesMenuList()) {
if (select->IsAppearanceBasePicker()) {
auto* popover = select->PopoverForAppearanceBase();
if (popover && popover->popoverOpen()) {
// If this option is being rendered as regular web content inside a
// base-select <select> popover, then we need this element to be
// focusable.

@ -106,7 +106,8 @@ class SelectDescendantsObserver : public MutationObserver::Delegate {
explicit SelectDescendantsObserver(HTMLSelectElement& select)
: select_(select), observer_(MutationObserver::Create(this)) {
CHECK(RuntimeEnabledFeatures::CustomizableSelectEnabled());
DCHECK(select_->IsAppearanceBasePicker());
DCHECK(select_->IsAppearanceBaseButton(
HTMLSelectElement::StyleUpdateBehavior::kDontUpdateStyle));
MutationObserverInit* init = MutationObserverInit::Create();
init->setChildList(true);
@ -1410,7 +1411,8 @@ void HTMLSelectElement::UpdateMutationObserver() {
if (!RuntimeEnabledFeatures::CustomizableSelectEnabled()) {
return;
}
if (UsesMenuList() && isConnected() && IsAppearanceBasePicker()) {
if (UsesMenuList() && isConnected() &&
IsAppearanceBaseButton(StyleUpdateBehavior::kDontUpdateStyle)) {
if (!descendants_observer_) {
descendants_observer_ =
MakeGarbageCollected<SelectDescendantsObserver>(*this);
@ -1962,6 +1964,14 @@ HTMLElement* HTMLSelectElement::PopoverForAppearanceBase() const {
return select_type_->PopoverForAppearanceBase();
}
// static
bool HTMLSelectElement::IsPopoverForAppearanceBase(const Node* node) {
if (auto* element = DynamicTo<Element>(node)) {
return IsPopoverForAppearanceBase(element);
}
return false;
}
// static
bool HTMLSelectElement::IsPopoverForAppearanceBase(const Element* element) {
if (auto* root = DynamicTo<ShadowRoot>(element->parentNode())) {
@ -1971,8 +1981,9 @@ bool HTMLSelectElement::IsPopoverForAppearanceBase(const Element* element) {
return false;
}
bool HTMLSelectElement::IsAppearanceBaseButton() const {
return select_type_->IsAppearanceBaseButton();
bool HTMLSelectElement::IsAppearanceBaseButton(
StyleUpdateBehavior update_behavior) const {
return select_type_->IsAppearanceBaseButton(update_behavior);
}
bool HTMLSelectElement::IsAppearanceBasePicker() const {
@ -2099,8 +2110,10 @@ void HTMLSelectElement::UpdateAllSelectedcontents() {
VectorOf<HTMLSelectedContentElement>(descendant_selectedcontents_)) {
selectedcontent->CloneContentsFromOptionElement(option);
}
if (auto* attr_selectedcontent = selectedContentElement()) {
attr_selectedcontent->CloneContentsFromOptionElement(option);
if (RuntimeEnabledFeatures::SelectedcontentelementAttributeEnabled()) {
if (auto* attr_selectedcontent = selectedContentElement()) {
attr_selectedcontent->CloneContentsFromOptionElement(option);
}
}
}

@ -245,6 +245,7 @@ class CORE_EXPORT HTMLSelectElement final
// Returns true if the provided element is some select element's
// PopoverForAppearanceBase.
static bool IsPopoverForAppearanceBase(const Node*);
static bool IsPopoverForAppearanceBase(const Element*);
// <select> supports appearance:base-select on both the main element and
@ -255,7 +256,14 @@ class CORE_EXPORT HTMLSelectElement final
// has appearance:base-select, IsAppearanceBasePicker will return true and the
// popup will be a popover element. The SelectType must also support base
// appearance, which is currently only MenuListSelectType.
bool IsAppearanceBaseButton() const;
// IsAppearanceBaseButton should be used for code which is concerned with the
// in-page rendering of the button, and IsAppearanceBasePicker should be used
// for code which is concerned with the popup/popover and the other elements
// which are rendered in it.
// |no_update| prevents these methods from running an UpdateStyleAndLayoutTree
// which is needed in some cases to prevent nested style/layout recalc.
enum class StyleUpdateBehavior { kUpdateStyle, kDontUpdateStyle };
bool IsAppearanceBaseButton(StyleUpdateBehavior) const;
bool IsAppearanceBasePicker() const;
void SelectedContentElementInserted(

@ -238,7 +238,8 @@ class MenuListSelectType final : public SelectType {
void ManuallyAssignSlots() override;
HTMLButtonElement* SlottedButton() const override;
HTMLElement* PopoverForAppearanceBase() const override;
bool IsAppearanceBaseButton() const override;
bool IsAppearanceBaseButton(
HTMLSelectElement::StyleUpdateBehavior) const override;
bool IsAppearanceBasePicker() const override;
HTMLSelectElement::SelectAutofillPreviewElement* GetAutofillPreviewElement()
const override;
@ -409,7 +410,9 @@ bool MenuListSelectType::DefaultEventHandler(const Event& event) {
// TODO(crbug.com/1511354): Reconsider making appearance:base-select affect
// keyboard behavior after a resolution here:
// https://github.com/openui/open-ui/issues/1087
if (IsAppearanceBaseButton() && key_code == '\r') {
if (IsAppearanceBaseButton(
HTMLSelectElement::StyleUpdateBehavior::kUpdateStyle) &&
key_code == '\r') {
// TODO(crbug.com/1511354): Consider making form->SubmitImplicitly work
// here instead of PrepareForSubmission and combine with the subsequent
// code.
@ -484,7 +487,8 @@ bool MenuListSelectType::ShouldOpenPopupForKeyDownEvent(
// TODO(crbug.com/1511354): Reconsider making appearance:base-select affect
// keyboard behavior after a resolution here:
// https://github.com/openui/open-ui/issues/1087
if (IsAppearanceBaseButton() &&
if (IsAppearanceBaseButton(
HTMLSelectElement::StyleUpdateBehavior::kUpdateStyle) &&
(key == keywords::kArrowDown || key == keywords::kArrowUp ||
key == keywords::kArrowLeft || key == keywords::kArrowRight)) {
return true;
@ -505,7 +509,9 @@ bool MenuListSelectType::ShouldOpenPopupForKeyPressEvent(
// TODO(crbug.com/1511354): Reconsider making appearance:base-select affect
// keyboard behavior after a resolution here:
// https://github.com/openui/open-ui/issues/1087
if (IsAppearanceBaseButton() && key_code == '\r') {
if (IsAppearanceBaseButton(
HTMLSelectElement::StyleUpdateBehavior::kUpdateStyle) &&
key_code == '\r') {
return false;
}
@ -648,22 +654,20 @@ HTMLButtonElement* MenuListSelectType::SlottedButton() const {
}
HTMLElement* MenuListSelectType::PopoverForAppearanceBase() const {
// LayoutFlexibleBox::IsChildAllowed needs to access popover_ even when the
// author doesn't put appearance:base-select on ::picker(select). In order to
// return popover_ in this case, we check IsAppearanceBaseButton instead of
// IsAppearanceBaseSelect.
if (!IsAppearanceBaseButton()) {
return nullptr;
}
CHECK(RuntimeEnabledFeatures::CustomizableSelectEnabled() || !popover_);
return popover_;
}
bool MenuListSelectType::IsAppearanceBaseButton() const {
bool MenuListSelectType::IsAppearanceBaseButton(
HTMLSelectElement::StyleUpdateBehavior update_behavior) const {
if (!RuntimeEnabledFeatures::CustomizableSelectEnabled()) {
return false;
}
// TODO(crbug.com/364348901): Update style and layout here.
DCHECK(select_);
if (update_behavior == HTMLSelectElement::StyleUpdateBehavior::kUpdateStyle) {
select_->GetDocument().UpdateStyleAndLayoutForNode(
select_, DocumentUpdateReason::kBaseSelect);
}
if (auto* style = select_->GetComputedStyle()) {
return style->EffectiveAppearance() == AppearanceValue::kBaseSelect;
}
@ -671,17 +675,15 @@ bool MenuListSelectType::IsAppearanceBaseButton() const {
}
bool MenuListSelectType::IsAppearanceBasePicker() const {
if (!IsAppearanceBaseButton()) {
if (!IsAppearanceBaseButton(
HTMLSelectElement::StyleUpdateBehavior::kUpdateStyle)) {
// The author is required to put appearance:base-select on the <select>
// before the ::picker is allowed to have appearance:base-select.
return false;
}
CHECK(RuntimeEnabledFeatures::CustomizableSelectEnabled());
// TODO(crbug.com/364348901): Consider using EnsureComputedStyle() here to get
// more reliable results, though it has the risk of causing more style
// computation, sometimes at bad times.
DCHECK(popover_);
if (auto* style = popover_->GetComputedStyle()) {
if (auto* style = popover_->EnsureComputedStyle()) {
return style->EffectiveAppearance() == AppearanceValue::kBaseSelect;
}
return false;
@ -780,11 +782,16 @@ void MenuListSelectType::PopupDidHide() {
}
bool MenuListSelectType::PopupIsVisible() const {
if (IsAppearanceBasePicker()) {
return popover_->popoverOpen();
} else {
return native_popup_is_visible_;
if (RuntimeEnabledFeatures::CustomizableSelectEnabled()) {
// This is called during style recalc, so we can't check
// IsAppearanceBasePicker to determine which picker to look at.
bool popover_open = popover_->popoverOpen();
CHECK(!(popover_open && native_popup_is_visible_))
<< " The base appearance and native pickers should never both be open "
"at the same time.";
return popover_open || native_popup_is_visible_;
}
return native_popup_is_visible_;
}
void MenuListSelectType::SetNativePopupIsVisible(bool popup_is_visible) {
@ -878,7 +885,8 @@ void MenuListSelectType::DidSetSuggestedOption(HTMLOptionElement* option) {
if (native_popup_is_visible_) {
popup_->UpdateFromElement(PopupMenu::kBySelectionChange);
}
if (IsAppearanceBaseButton()) {
if (IsAppearanceBaseButton(
HTMLSelectElement::StyleUpdateBehavior::kUpdateStyle)) {
if (option) {
autofill_popover_->ShowPopoverInternal(select_, &ASSERT_NO_EXCEPTION);
autofill_popover_text_->setInnerText(option->label());
@ -973,7 +981,8 @@ String MenuListSelectType::UpdateTextStyleInternal() {
// TODO(crbug.com/1511354): Ensure that this runs after switching appearance
// modes or consider splitting InnerElement into two elements, one for
// appearance:base-select and one for appearance:auto.
if (!IsAppearanceBaseButton()) {
if (!IsAppearanceBaseButton(
HTMLSelectElement::StyleUpdateBehavior::kDontUpdateStyle)) {
Element& inner_element = select_->InnerElement();
const ComputedStyle* inner_style = inner_element.GetComputedStyle();
if (inner_style && option_style &&
@ -1040,7 +1049,9 @@ HTMLOptionElement* MenuListSelectType::OptionToBeShown() const {
return option;
// In appearance:base-select mode, we don't want to reveal the suggested
// option anywhere except in autofill_popover_.
if (select_->suggested_option_ && !IsAppearanceBaseButton()) {
if (select_->suggested_option_ &&
!IsAppearanceBaseButton(
HTMLSelectElement::StyleUpdateBehavior::kDontUpdateStyle)) {
return select_->suggested_option_.Get();
}
// TODO(tkent): We should not call OptionToBeShown() in IsMultiple() case.
@ -1164,7 +1175,8 @@ class ListBoxSelectType final : public SelectType {
void ManuallyAssignSlots() override;
HTMLButtonElement* SlottedButton() const override;
HTMLElement* PopoverForAppearanceBase() const override;
bool IsAppearanceBaseButton() const override;
bool IsAppearanceBaseButton(
HTMLSelectElement::StyleUpdateBehavior) const override;
bool IsAppearanceBasePicker() const override;
HTMLSelectElement::SelectAutofillPreviewElement* GetAutofillPreviewElement()
const override;
@ -1837,7 +1849,8 @@ HTMLElement* ListBoxSelectType::PopoverForAppearanceBase() const {
return nullptr;
}
bool ListBoxSelectType::IsAppearanceBaseButton() const {
bool ListBoxSelectType::IsAppearanceBaseButton(
HTMLSelectElement::StyleUpdateBehavior) const {
return false;
}

@ -65,7 +65,8 @@ class SelectType : public GarbageCollected<SelectType> {
virtual void ManuallyAssignSlots() = 0;
virtual HTMLButtonElement* SlottedButton() const = 0;
virtual HTMLElement* PopoverForAppearanceBase() const = 0;
virtual bool IsAppearanceBaseButton() const = 0;
virtual bool IsAppearanceBaseButton(
HTMLSelectElement::StyleUpdateBehavior) const = 0;
virtual bool IsAppearanceBasePicker() const = 0;
virtual HTMLSelectElement::SelectAutofillPreviewElement*
GetAutofillPreviewElement() const = 0;

@ -85,20 +85,6 @@ select:not(:-internal-list-box)::picker(select) {
block-start span-inline-start;
}
/* This rule is here to ensure that we can get a ComputedStyle for
* ::picker(select). Without this, the [popover] UA style would make it
* display:none, which would mean that we wouldn't get a ComputedStyle, which
* would mean that we can't detect the author opting in with
* appearance:base-select on this element. This element is hidden when it isn't
* popover open via LayoutFlexibleBox::IsChildAllowed. This also means that the
* author can't force it to become visible while it is popover closed by
* putting display:block on it, but hopefully that's OK.
* TODO(crbug.com/364348901): Remove this.
*/
select::picker(select) {
display: block !important;
}
select:not(:-internal-list-box) option {
/* min-size rules ensure that we meet accessibility guidelines for minimum target size.
* https://github.com/openui/open-ui/issues/1026

@ -87,7 +87,8 @@ bool LayoutFlexibleBox::IsChildAllowed(LayoutObject* object,
const ComputedStyle& style) const {
const auto* select = DynamicTo<HTMLSelectElement>(GetNode());
if (select && select->UsesMenuList()) [[unlikely]] {
if (select->IsAppearanceBaseButton()) {
if (select->IsAppearanceBaseButton(
HTMLSelectElement::StyleUpdateBehavior::kDontUpdateStyle)) {
CHECK(RuntimeEnabledFeatures::CustomizableSelectEnabled());
if (IsA<HTMLOptionElement>(object->GetNode()) ||
IsA<HTMLOptGroupElement>(object->GetNode()) ||
@ -105,14 +106,6 @@ bool LayoutFlexibleBox::IsChildAllowed(LayoutObject* object,
// the InnerElement.
return false;
}
if (auto* popover = select->PopoverForAppearanceBase()) {
if (child == popover && !popover->popoverOpen()) {
// This is needed in order to keep the popover hidden after the UA
// sheet is forcing it to be display:block in order to get a computed
// style.
return false;
}
}
return true;
} else {
// For a size=1 appearance:auto <select>, we only render the active option

@ -23,7 +23,8 @@ static bool CanHaveGeneratedChildren(const LayoutObject& layout_object) {
if (RuntimeEnabledFeatures::CustomizableSelectEnabled() &&
layout_object.IsMenuList() &&
To<HTMLSelectElement>(layout_object.GetNode())
->IsAppearanceBaseButton()) {
->IsAppearanceBaseButton(
HTMLSelectElement::StyleUpdateBehavior::kDontUpdateStyle)) {
// appearance:base-select <select>s should be allowed to have ::after etc.
return true;
}

@ -1211,7 +1211,8 @@ LayoutUnit LayoutBox::DefaultIntrinsicContentInlineSize() const {
const bool apply_fixed_size = StyleRef().ApplyControlFixedSize(&element);
const auto* select = DynamicTo<HTMLSelectElement>(element);
if (select && select->UsesMenuList() && !select->IsAppearanceBaseButton())
if (select && select->UsesMenuList() &&
StyleRef().EffectiveAppearance() != AppearanceValue::kBaseSelect)
[[unlikely]] {
return apply_fixed_size ? MenuListIntrinsicInlineSize(*select, *this)
: kIndefiniteSize;
@ -1265,12 +1266,11 @@ LayoutUnit LayoutBox::DefaultIntrinsicContentBlockSize() const {
return kIndefiniteSize;
}
if (const auto* select = DynamicTo<HTMLSelectElement>(GetNode())) {
if (!select->IsAppearanceBaseButton()) {
if (select->UsesMenuList()) {
return MenuListIntrinsicBlockSize(*select, *this);
}
if (!select->UsesMenuList()) {
return ListBoxItemBlockSize(*select, *this) * select->ListBoxSize() -
ComputeLogicalScrollbars().BlockSum();
} else if (effective_appearance != AppearanceValue::kBaseSelect) {
return MenuListIntrinsicBlockSize(*select, *this);
}
}
if (IsTextField()) {

@ -2242,6 +2242,9 @@ ax::mojom::blink::Role AXNodeObject::NativeRoleIgnoringAria() const {
if (ParentObjectIfPresent() && ParentObjectIfPresent()->RoleValue() ==
ax::mojom::blink::Role::kComboBoxSelect) {
// Only the UA popover element should get the MenuListPopup role.
CHECK(!RuntimeEnabledFeatures::CustomizableSelectEnabled() ||
HTMLSelectElement::IsPopoverForAppearanceBase(GetNode()));
return ax::mojom::blink::Role::kMenuListPopup;
}
@ -5768,9 +5771,10 @@ void AXNodeObject::AddNodeChildren() {
void AXNodeObject::AddMenuListChildren() {
auto* select = To<HTMLSelectElement>(GetNode());
if (select->IsAppearanceBasePicker()) {
// In appearance: base-select (customizable select), the children of the
// combobox is the displayed data list.
if (RuntimeEnabledFeatures::CustomizableSelectEnabled()) {
CHECK(select->UsesMenuList());
// When CustomizableSelect is enabled, there will always be one
// MenuListPopup child which is the UA popover element.
AddNodeChild(select->PopoverForAppearanceBase());
return;
}
@ -5781,9 +5785,8 @@ void AXNodeObject::AddMenuListChildren() {
void AXNodeObject::AddMenuListPopupChildren() {
auto* select = To<HTMLSelectElement>(ParentObject()->GetNode());
if (select->IsAppearanceBasePicker()) {
// In appearance: base-select (customizable select), the children of the
// popup are all of the natural dom children of the <select>.
// With CustomizableSelect, MenuListPopups can have more interesting children.
if (RuntimeEnabledFeatures::CustomizableSelectEnabled()) {
for (Node* child = NodeTraversal::FirstChild(*select); child;
child = NodeTraversal::NextSibling(*child)) {
if (child == select->SlottedButton()) {
@ -5797,8 +5800,6 @@ void AXNodeObject::AddMenuListPopupChildren() {
return;
}
// In appearance: auto/none, the children of the popup are the flat tree
// children of the slot associated with the popup.
AddNodeChildren();
}
@ -5847,9 +5848,18 @@ void AXNodeObject::AddChildrenImpl() {
AddValidationMessageChild();
CHECK_ATTACHED();
if (RoleValue() == ax::mojom::blink::Role::kComboBoxSelect) {
auto* select = DynamicTo<HTMLSelectElement>(GetNode());
if (RuntimeEnabledFeatures::CustomizableSelectEnabled() && select &&
select->UsesMenuList() && !select->IsMultiple()) {
// When CustomizableSelect is enabled, then we need to enforce our custom AX
// tree structure for select elements even if the author changed the select
// element's role by setting the role attribute.
AddMenuListChildren();
} else if (RoleValue() == ax::mojom::blink::Role::kMenuListPopup) {
if (RuntimeEnabledFeatures::CustomizableSelectEnabled()) {
// Only the UA popover element should have the MenuListPopup mapping.
CHECK(HTMLSelectElement::IsPopoverForAppearanceBase(GetNode()));
}
AddMenuListPopupChildren();
} else if (HasValidHTMLTableStructureAndLayout()) {
AddTableChildren();
@ -6099,8 +6109,14 @@ bool AXNodeObject::CanHaveChildren() const {
// and improving stability in Blink.
bool result = !GetElement() || AXObject::CanHaveChildren(*GetElement());
switch (native_role_) {
case ax::mojom::blink::Role::kCheckBox:
case ax::mojom::blink::Role::kListBoxOption:
if (RuntimeEnabledFeatures::CustomizableSelectEnabled()) {
// When CustomizableSelect is enabled, then options are allowed to have
// children as per the new content model.
break;
}
[[fallthrough]];
case ax::mojom::blink::Role::kCheckBox:
case ax::mojom::blink::Role::kMenuItem:
case ax::mojom::blink::Role::kMenuItemCheckBox:
case ax::mojom::blink::Role::kMenuItemRadio:

@ -978,6 +978,30 @@ Node* AXObject::GetParentNodeForComputeParent(AXObjectCacheImpl& cache,
return node->OwnerShadowHost();
}
Node* parent = nullptr;
// Select elements have a complex architecture with two different slot
// elements which `node` may be slotted into. `node` might also not be slotted
// into anything at all (which is why we are doing this before using
// LayoutTreeBuilderTraversal). Despite this, we always want to expose a
// consistent structure for the select element with a MenuList and
// MenuListPopup with all the children exposed in the MenuListPopup. For
// consistency, we will use the select's PopoverForAppearanceBase element as
// the MenuListPopup and make it the parent of all the nodes inside the
// select.
if (RuntimeEnabledFeatures::CustomizableSelectEnabled()) {
if (auto* select = DynamicTo<HTMLSelectElement>(node->parentNode())) {
if (select->UsesMenuList()) {
if (node == select->SlottedButton()) {
// <select>'s author provided <button> should not have any
// accessibility mappings.
return nullptr;
}
parent = select->PopoverForAppearanceBase();
}
}
}
// Use LayoutTreeBuilderTraversal::Parent(), which handles pseudo content.
// This can return nullptr for a node that is never visited by
// LayoutTreeBuilderTraversal's child traversal. For example, while an element
@ -988,35 +1012,8 @@ Node* AXObject::GetParentNodeForComputeParent(AXObjectCacheImpl& cache,
// Whenever null is returned from this function, then a parent cannot be
// computed, and when a parent is not provided or computed, the accessible
// object will not be created.
Node* parent = LayoutTreeBuilderTraversal::Parent(*node);
// The parent of a customizable select's popup is the select.
if (IsA<HTMLDataListElement>(node)) {
if (auto* select = DynamicTo<HTMLSelectElement>(node->OwnerShadowHost())) {
if (node == select->PopoverForAppearanceBase()) {
return select;
}
}
}
// For the content of a customizable select, the parent must be the element
// assigned the role of kMenuListPopup. To accomplish this, it is necessary to
// adapt to unusual DOM structure. If no parent, or the parent has a <select>
// shadow host, then the actual parent should be the <select>.
// TODO(aleventhal, jarhar): try to simplify this code. @jarhar wrote in code
// review: "I don't think that a UA <slot> will ever get returned by
// LayoutTreeBuilderTraversal::Parent. In this case, I think
// LayoutTreeBuilderTraversal::Parent should just return the <select>."
HTMLSelectElement* owner_select = nullptr;
if (IsA<HTMLSlotElement>(parent) && parent->IsInUserAgentShadowRoot()) {
owner_select = DynamicTo<HTMLSelectElement>(parent->OwnerShadowHost());
} else if (!parent) {
owner_select = DynamicTo<HTMLSelectElement>(NodeTraversal::Parent(*node));
}
if (owner_select && owner_select->IsAppearanceBasePicker()) {
// Return the popup's <datalist> element.
return owner_select->PopoverForAppearanceBase();
if (!parent) {
parent = LayoutTreeBuilderTraversal::Parent(*node);
}
// No parent: this can occur when elements are not assigned a slot.
@ -1119,9 +1116,17 @@ bool AXObject::CanHaveChildren(Element& element) {
// For consistency with the past, options with a single text child are leaves.
// However, options can now sometimes have interesting children, for
// a <select> menulist that uses appearance:base-select.
// This code looks at IsAppearanceBaseButton instead of IsAppearanceBasePicker
// in order to have the same result whether the picker is open or closed.
// Ideally we would check to see if the picker is opted in to base appearance,
// but without a style update that would change based on whether the picker is
// open or not. When the button is opted in then the user will likely be able
// to see the child node structure of the option element in the button, so we
// can use that as a signal to allow options to have children.
if (auto* option = DynamicTo<HTMLOptionElement>(element)) {
return option->OwnerSelectElement() &&
option->OwnerSelectElement()->IsAppearanceBasePicker() &&
option->OwnerSelectElement()->IsAppearanceBaseButton(
HTMLSelectElement::StyleUpdateBehavior::kDontUpdateStyle) &&
!option->HasOneTextChild();
}
@ -7337,7 +7342,8 @@ bool AXObject::OnNativeClickAction() {
// Forward default action on custom select to its button.
if (auto* select = DynamicTo<HTMLSelectElement>(GetNode())) {
if (select->IsAppearanceBaseButton()) {
if (select->IsAppearanceBaseButton(
HTMLSelectElement::StyleUpdateBehavior::kDontUpdateStyle)) {
if (auto* button = select->SlottedButton()) {
element = button;
}

@ -165,7 +165,9 @@ Node* RetargetInput(Node* node) {
possible_select = NodeTraversal::Parent(*node);
}
if (auto* select = DynamicTo<HTMLSelectElement>(possible_select)) {
if (select->IsAppearanceBaseButton() && node == select->SlottedButton()) {
if (select->IsAppearanceBaseButton(
HTMLSelectElement::StyleUpdateBehavior::kDontUpdateStyle) &&
node == select->SlottedButton()) {
return select;
}
}
@ -1225,7 +1227,8 @@ bool AXObjectCacheImpl::IsRelevantSlotElement(const HTMLSlotElement& slot) {
DCHECK(AXObject::CanSafelyUseFlatTreeTraversalNow(slot.GetDocument()));
DCHECK(slot.SupportsAssignment());
if (slot.IsInUserAgentShadowRoot() &&
if (!RuntimeEnabledFeatures::CustomizableSelectEnabled() &&
slot.IsInUserAgentShadowRoot() &&
IsA<HTMLSelectElement>(slot.OwnerShadowHost())) {
return slot.GetIdAttribute() == shadow_element_names::kSelectOptions;
}

@ -3,6 +3,8 @@
<link rel=help href="https://issues.chromium.org/issues/361129480">
<script src="../../../../resources/testharness.js"></script>
<script src="../../../../resources/testharnessreport.js"></script>
<script src="../../../../resources/testdriver.js"></script>
<script src="../../../../resources/testdriver-vendor.js"></script>
<select>
<option>option</option>
@ -49,7 +51,13 @@ promise_test(async () => {
await new Promise(requestAnimationFrame);
assert_true(internals.isUseCounted(document, buttonUseCounterId),
'Button use counter should be hit with base-select on button and picker.');
assert_false(internals.isUseCounted(document, pickerUseCounterId),
"Picker use counter should not be hit if picker isn't open.");
await test_driver.click(select);
await new Promise(requestAnimationFrame);
await new Promise(requestAnimationFrame);
assert_true(internals.isUseCounted(document, pickerUseCounterId),
'Picker use counter should be hit with base-select on button and picker.');
'Picker use counter should be hit after picker is opened.');
}, 'UseCounters for base-select on button and picker should work.');
</script>

@ -423,7 +423,6 @@ RootWebArea
none
*combobox
MenuListPopup
MenuListPopup
{
childIds : <object>
chromeRole : {

@ -0,0 +1,17 @@
This is a testharness.js-based test.
[FAIL] Basic functionality of select picker with appearance:auto
assert_equals: expected "none" but got "base-select"
[FAIL] Basic functionality of select picker with appearance:none
assert_equals: expected "none" but got "base-select"
[FAIL] Switching appearance in popover-open should close the picker
assert_equals: expected "none" but got "base-select"
[FAIL] Switching appearance in JS after picker is open should close the picker
assert_equals: expected "none" but got "base-select"
[FAIL] The select picker is closed if the <select> appearance value is changed via CSS while the picker is open
assert_false: picker should get closed when the appearance value changes expected false got true
[FAIL] The select picker is closed if the ::picker() appearance value is changed via CSS while the picker is open
assert_false: setup expected false got true
[FAIL] The select picker is closed if the <select> inline appearance value is changed while the picker is open
assert_false: setup expected false got true
Harness: the test ran to completion.