diff options
Diffstat (limited to 'chromium/third_party/blink/renderer/core/html/forms/html_select_element.cc')
-rw-r--r-- | chromium/third_party/blink/renderer/core/html/forms/html_select_element.cc | 1211 |
1 files changed, 250 insertions, 961 deletions
diff --git a/chromium/third_party/blink/renderer/core/html/forms/html_select_element.cc b/chromium/third_party/blink/renderer/core/html/forms/html_select_element.cc index a02e8b059ac..a5f4eb0657c 100644 --- a/chromium/third_party/blink/renderer/core/html/forms/html_select_element.cc +++ b/chromium/third_party/blink/renderer/core/html/forms/html_select_element.cc @@ -30,6 +30,7 @@ #include "third_party/blink/renderer/core/html/forms/html_select_element.h" #include "build/build_config.h" +#include "third_party/blink/public/mojom/input/focus_type.mojom-blink.h" #include "third_party/blink/public/platform/task_type.h" #include "third_party/blink/public/strings/grit/blink_strings.h" #include "third_party/blink/renderer/bindings/core/v8/html_element_or_long.h" @@ -39,45 +40,38 @@ #include "third_party/blink/renderer/core/dom/attribute.h" #include "third_party/blink/renderer/core/dom/element_traversal.h" #include "third_party/blink/renderer/core/dom/events/scoped_event_queue.h" -#include "third_party/blink/renderer/core/dom/mutation_observer.h" -#include "third_party/blink/renderer/core/dom/mutation_observer_init.h" -#include "third_party/blink/renderer/core/dom/mutation_record.h" #include "third_party/blink/renderer/core/dom/node_computed_style.h" #include "third_party/blink/renderer/core/dom/node_lists_node_data.h" #include "third_party/blink/renderer/core/dom/node_traversal.h" -#include "third_party/blink/renderer/core/events/gesture_event.h" #include "third_party/blink/renderer/core/events/keyboard_event.h" -#include "third_party/blink/renderer/core/events/mouse_event.h" -#include "third_party/blink/renderer/core/frame/local_dom_window.h" -#include "third_party/blink/renderer/core/frame/local_frame.h" -#include "third_party/blink/renderer/core/frame/local_frame_view.h" #include "third_party/blink/renderer/core/frame/web_feature.h" #include "third_party/blink/renderer/core/html/forms/form_controller.h" #include "third_party/blink/renderer/core/html/forms/form_data.h" #include "third_party/blink/renderer/core/html/forms/html_form_element.h" #include "third_party/blink/renderer/core/html/forms/html_opt_group_element.h" #include "third_party/blink/renderer/core/html/forms/html_option_element.h" -#include "third_party/blink/renderer/core/html/forms/popup_menu.h" +#include "third_party/blink/renderer/core/html/forms/menu_list_inner_element.h" +#include "third_party/blink/renderer/core/html/forms/select_type.h" #include "third_party/blink/renderer/core/html/html_hr_element.h" #include "third_party/blink/renderer/core/html/html_slot_element.h" #include "third_party/blink/renderer/core/html/parser/html_parser_idioms.h" #include "third_party/blink/renderer/core/html_names.h" -#include "third_party/blink/renderer/core/input/event_handler.h" -#include "third_party/blink/renderer/core/input/input_device_capabilities.h" #include "third_party/blink/renderer/core/inspector/console_message.h" #include "third_party/blink/renderer/core/layout/hit_test_request.h" #include "third_party/blink/renderer/core/layout/hit_test_result.h" -#include "third_party/blink/renderer/core/layout/layout_list_box.h" -#include "third_party/blink/renderer/core/layout/layout_menu_list.h" +#include "third_party/blink/renderer/core/layout/layout_block_flow.h" +#include "third_party/blink/renderer/core/layout/layout_object_factory.h" #include "third_party/blink/renderer/core/layout/layout_theme.h" -#include "third_party/blink/renderer/core/page/autoscroll_controller.h" #include "third_party/blink/renderer/core/page/chrome_client.h" #include "third_party/blink/renderer/core/page/page.h" #include "third_party/blink/renderer/core/page/spatial_navigation.h" +#include "third_party/blink/renderer/core/paint/paint_layer.h" +#include "third_party/blink/renderer/core/paint/paint_layer_scrollable_area.h" #include "third_party/blink/renderer/platform/bindings/exception_state.h" #include "third_party/blink/renderer/platform/heap/heap.h" #include "third_party/blink/renderer/platform/instrumentation/tracing/trace_event.h" #include "third_party/blink/renderer/platform/text/platform_locale.h" +#include "ui/base/ui_base_features.h" namespace blink { @@ -86,18 +80,21 @@ namespace blink { // signed. static const unsigned kMaxListItems = INT_MAX; +// Default size when the multiple attribute is present but size attribute is +// absent. +const int kDefaultListBoxSize = 4; + HTMLSelectElement::HTMLSelectElement(Document& document) : HTMLFormControlElementWithState(html_names::kSelectTag, document), type_ahead_(this), size_(0), last_on_change_option_(nullptr), is_multiple_(false), - is_in_non_contiguous_selection_(false), - active_selection_state_(false), should_recalc_list_items_(false), is_autofilled_by_preview_(false), - index_to_select_on_cancel_(-1), - popup_is_visible_(false) { + index_to_select_on_cancel_(-1) { + // Make sure SelectType is created after initializing |uses_menu_list_|. + select_type_ = SelectType::Create(*this); SetHasCustomStyleCallbacks(); EnsureUserAgentShadowRoot(); } @@ -106,7 +103,7 @@ HTMLSelectElement::~HTMLSelectElement() = default; // static bool HTMLSelectElement::CanAssignToSelectSlot(const Node& node) { - // Even if options/optgroups are not rendered as children of LayoutMenuList, + // Even if options/optgroups are not rendered as children of menulist SELECT, // we still need to add them to the flat tree through slotting since we need // their ComputedStyle for popup rendering. return node.HasTagName(html_names::kOptionTag) || @@ -163,9 +160,6 @@ String HTMLSelectElement::validationMessage() const { } bool HTMLSelectElement::ValueMissing() const { - if (!willValidate()) - return false; - if (!IsRequired()) return false; @@ -187,21 +181,48 @@ void HTMLSelectElement::SelectMultipleOptionsByPopup( const Vector<int>& list_indices) { DCHECK(UsesMenuList()); DCHECK(IsMultiple()); - for (wtf_size_t i = 0; i < list_indices.size(); ++i) { - bool add_selection_if_not_first = i > 0; - if (HTMLOptionElement* option = OptionAtListIndex(list_indices[i])) - UpdateSelectedState(option, add_selection_if_not_first, false); + + HeapHashSet<Member<HTMLOptionElement>> old_selection; + for (auto* option : GetOptionList()) { + if (option->Selected()) { + old_selection.insert(option); + option->SetSelectedState(false); + } } + + bool has_new_selection = false; + for (int list_index : list_indices) { + if (auto* option = OptionAtListIndex(list_index)) { + option->SetSelectedState(true); + option->SetDirty(true); + auto iter = old_selection.find(option); + if (iter != old_selection.end()) + old_selection.erase(iter); + else + has_new_selection = true; + } + } + SetNeedsValidityCheck(); - // TODO(tkent): Using listBoxOnChange() is very confusing. - ListBoxOnChange(); + if (has_new_selection || !old_selection.IsEmpty()) { + DispatchInputEvent(); + DispatchChangeEvent(); + } } -bool HTMLSelectElement::UsesMenuList() const { - if (LayoutTheme::GetTheme().DelegatesMenuListRendering()) - return true; +unsigned HTMLSelectElement::ListBoxSize() const { + DCHECK(!UsesMenuList()); + const unsigned specified_size = size(); + if (specified_size >= 1) + return specified_size; + return kDefaultListBoxSize; +} - return !is_multiple_ && size_ <= 1; +void HTMLSelectElement::UpdateUsesMenuList() { + if (LayoutTheme::GetTheme().DelegatesMenuListRendering()) + uses_menu_list_ = true; + else + uses_menu_list_ = !is_multiple_ && size_ <= 1; } int HTMLSelectElement::ActiveSelectionEndListIndex() const { @@ -269,8 +290,8 @@ void HTMLSelectElement::setValue(const String& value, bool send_events) { flags |= kDispatchInputAndChangeEventFlag; SelectOption(option, flags); - if (send_events && previous_selected_option != option && !UsesMenuList()) - ListBoxOnChange(); + if (send_events && previous_selected_option != option) + select_type_->ListBoxOnChange(); } String HTMLSelectElement::SuggestedValue() const { @@ -314,9 +335,10 @@ void HTMLSelectElement::ParseAttribute( SetNeedsValidityCheck(); if (size_ != old_size) { ChangeRendering(); + UpdateUserAgentShadowTree(*UserAgentShadowRoot()); ResetToDefaultSelection(); - if (!UsesMenuList()) - SaveListboxActiveSelection(); + select_type_->UpdateTextStyleAndContent(); + select_type_->SaveListboxActiveSelection(); } } else if (params.name == html_names::kMultipleAttr) { ParseMultipleAttribute(params.new_value); @@ -332,15 +354,29 @@ bool HTMLSelectElement::MayTriggerVirtualKeyboard() const { return true; } +bool HTMLSelectElement::ShouldHaveFocusAppearance() const { + // For FormControlsRefresh don't draw focus ring for a select that has its + // popup open. + if (::features::IsFormControlsRefreshEnabled() && PopupIsVisible()) + return false; + + return HTMLFormControlElementWithState::ShouldHaveFocusAppearance(); +} + bool HTMLSelectElement::CanSelectAll() const { return !UsesMenuList(); } -LayoutObject* HTMLSelectElement::CreateLayoutObject(const ComputedStyle&, - LegacyLayout) { +bool HTMLSelectElement::TypeShouldForceLegacyLayout() const { + return UsesMenuList(); +} + +LayoutObject* HTMLSelectElement::CreateLayoutObject( + const ComputedStyle& style, + LegacyLayout legacy_layout) { if (UsesMenuList()) - return new LayoutMenuList(this); - return new LayoutListBox(this); + return LayoutObjectFactory::CreateFlexibleBox(*this, style, legacy_layout); + return LayoutObjectFactory::CreateBlockFlow(*this, style, legacy_layout); } HTMLCollection* HTMLSelectElement::selectedOptions() { @@ -355,9 +391,9 @@ void HTMLSelectElement::OptionElementChildrenChanged( const HTMLOptionElement& option) { SetNeedsValidityCheck(); + if (option.Selected()) + select_type_->UpdateTextStyleAndContent(); if (GetLayoutObject()) { - if (option.Selected() && UsesMenuList()) - GetLayoutObject()->UpdateFromElement(); if (AXObjectCache* cache = GetLayoutObject()->GetDocument().ExistingAXObjectCache()) cache->ChildrenChanged(this); @@ -385,7 +421,7 @@ void HTMLSelectElement::SetOption(unsigned index, // We should check |index >= maxListItems| first to avoid integer overflow. if (index >= kMaxListItems || GetListItems().size() + diff + 1 > kMaxListItems) { - GetDocument().AddConsoleMessage(ConsoleMessage::Create( + GetDocument().AddConsoleMessage(MakeGarbageCollected<ConsoleMessage>( mojom::ConsoleMessageSource::kJavaScript, mojom::ConsoleMessageLevel::kWarning, String::Format("Blocked to expand the option list and set an option at " @@ -418,7 +454,7 @@ void HTMLSelectElement::setLength(unsigned new_len, // We should check |newLen > maxListItems| first to avoid integer overflow. if (new_len > kMaxListItems || GetListItems().size() + new_len - length() > kMaxListItems) { - GetDocument().AddConsoleMessage(ConsoleMessage::Create( + GetDocument().AddConsoleMessage(MakeGarbageCollected<ConsoleMessage>( mojom::ConsoleMessageSource::kJavaScript, mojom::ConsoleMessageLevel::kWarning, String::Format("Blocked to expand the option list to %u items. The " @@ -469,246 +505,19 @@ HTMLOptionElement* HTMLSelectElement::OptionAtListIndex(int list_index) const { return DynamicTo<HTMLOptionElement>(items[list_index].Get()); } -// Returns the 1st valid OPTION |skip| items from |listIndex| in direction -// |direction| if there is one. -// Otherwise, it returns the valid OPTION closest to that boundary which is past -// |listIndex| if there is one. -// Otherwise, it returns nullptr. -// Valid means that it is enabled and visible. -HTMLOptionElement* HTMLSelectElement::NextValidOption(int list_index, - SkipDirection direction, - int skip) const { - DCHECK(direction == kSkipBackwards || direction == kSkipForwards); - const ListItems& list_items = GetListItems(); - HTMLOptionElement* last_good_option = nullptr; - int size = list_items.size(); - for (list_index += direction; list_index >= 0 && list_index < size; - list_index += direction) { - --skip; - HTMLElement* element = list_items[list_index]; - auto* option_element = DynamicTo<HTMLOptionElement>(element); - if (!option_element) - continue; - if (option_element->IsDisplayNone()) - continue; - if (element->IsDisabledFormControl()) - continue; - if (!UsesMenuList() && !element->GetLayoutObject()) - continue; - last_good_option = option_element; - if (skip <= 0) - break; - } - return last_good_option; -} - -HTMLOptionElement* HTMLSelectElement::NextSelectableOption( - HTMLOptionElement* start_option) const { - return NextValidOption(start_option ? start_option->ListIndex() : -1, - kSkipForwards, 1); -} - -HTMLOptionElement* HTMLSelectElement::PreviousSelectableOption( - HTMLOptionElement* start_option) const { - return NextValidOption( - start_option ? start_option->ListIndex() : GetListItems().size(), - kSkipBackwards, 1); -} - -HTMLOptionElement* HTMLSelectElement::FirstSelectableOption() const { - // TODO(tkent): This is not efficient. nextSlectableOption(nullptr) is - // faster. - return NextValidOption(GetListItems().size(), kSkipBackwards, INT_MAX); -} - -HTMLOptionElement* HTMLSelectElement::LastSelectableOption() const { - // TODO(tkent): This is not efficient. previousSlectableOption(nullptr) is - // faster. - return NextValidOption(-1, kSkipForwards, INT_MAX); -} - -// Returns the index of the next valid item one page away from |startIndex| in -// direction |direction|. -HTMLOptionElement* HTMLSelectElement::NextSelectableOptionPageAway( - HTMLOptionElement* start_option, - SkipDirection direction) const { - const ListItems& items = GetListItems(); - // Can't use size_ because LayoutObject forces a minimum size. - int page_size = 0; - if (GetLayoutObject()->IsListBox()) { - // -1 so we still show context. - page_size = ToLayoutListBox(GetLayoutObject())->size() - 1; - } - - // One page away, but not outside valid bounds. - // If there is a valid option item one page away, the index is chosen. - // If there is no exact one page away valid option, returns startIndex or - // the most far index. - int start_index = start_option ? start_option->ListIndex() : -1; - int edge_index = (direction == kSkipForwards) ? 0 : (items.size() - 1); - int skip_amount = - page_size + - ((direction == kSkipForwards) ? start_index : (edge_index - start_index)); - return NextValidOption(edge_index, direction, skip_amount); -} - void HTMLSelectElement::SelectAll() { - DCHECK(!UsesMenuList()); - if (!GetLayoutObject() || !is_multiple_) - return; - - // Save the selection so it can be compared to the new selectAll selection - // when dispatching change events. - SaveLastSelection(); - - active_selection_state_ = true; - SetActiveSelectionAnchor(NextSelectableOption(nullptr)); - SetActiveSelectionEnd(PreviousSelectableOption(nullptr)); - - UpdateListBoxSelection(false, false); - ListBoxOnChange(); - SetNeedsValidityCheck(); -} - -void HTMLSelectElement::SaveLastSelection() { - if (UsesMenuList()) { - last_on_change_option_ = SelectedOption(); - return; - } - - last_on_change_selection_.clear(); - for (auto& element : GetListItems()) { - auto* option_element = DynamicTo<HTMLOptionElement>(element.Get()); - last_on_change_selection_.push_back(option_element && - option_element->Selected()); - } + select_type_->SelectAll(); } void HTMLSelectElement::SetActiveSelectionAnchor(HTMLOptionElement* option) { active_selection_anchor_ = option; - if (!UsesMenuList()) - SaveListboxActiveSelection(); -} - -void HTMLSelectElement::SaveListboxActiveSelection() { - // Cache the selection state so we can restore the old selection as the new - // selection pivots around this anchor index. - // Example: - // 1. Press the mouse button on the second OPTION - // active_selection_anchor_ points the second OPTION. - // 2. Drag the mouse pointer onto the fifth OPTION - // active_selection_end_ points the fifth OPTION, OPTIONs at 1-4 indices - // are selected. - // 3. Drag the mouse pointer onto the fourth OPTION - // active_selection_end_ points the fourth OPTION, OPTIONs at 1-3 indices - // are selected. - // UpdateListBoxSelection needs to clear selection of the fifth OPTION. - cached_state_for_active_selection_.resize(0); - for (auto* const option : GetOptionList()) { - cached_state_for_active_selection_.push_back(option->Selected()); - } + select_type_->SaveListboxActiveSelection(); } void HTMLSelectElement::SetActiveSelectionEnd(HTMLOptionElement* option) { active_selection_end_ = option; } -void HTMLSelectElement::UpdateListBoxSelection(bool deselect_other_options, - bool scroll) { - DCHECK(GetLayoutObject()); - DCHECK(GetLayoutObject()->IsListBox() || is_multiple_); - - int active_selection_anchor_index = - active_selection_anchor_ ? active_selection_anchor_->index() : -1; - int active_selection_end_index = - active_selection_end_ ? active_selection_end_->index() : -1; - int start = - std::min(active_selection_anchor_index, active_selection_end_index); - int end = std::max(active_selection_anchor_index, active_selection_end_index); - - int i = 0; - for (auto* const option : GetOptionList()) { - if (option->IsDisabledFormControl() || !option->GetLayoutObject()) { - ++i; - continue; - } - if (i >= start && i <= end) { - option->SetSelectedState(active_selection_state_); - option->SetDirty(true); - } else if (deselect_other_options || - i >= static_cast<int>( - cached_state_for_active_selection_.size())) { - option->SetSelectedState(false); - option->SetDirty(true); - } else { - option->SetSelectedState(cached_state_for_active_selection_[i]); - } - ++i; - } - - UpdateMultiSelectListBoxFocus(); - SetNeedsValidityCheck(); - if (scroll) - ScrollToSelection(); - NotifyFormStateChanged(); -} - -void HTMLSelectElement::ListBoxOnChange() { - DCHECK(!UsesMenuList() || is_multiple_); - - const ListItems& items = GetListItems(); - - // If the cached selection list is empty, or the size has changed, then fire - // dispatchFormControlChangeEvent, and return early. - // FIXME: Why? This looks unreasonable. - if (last_on_change_selection_.IsEmpty() || - last_on_change_selection_.size() != items.size()) { - DispatchChangeEvent(); - return; - } - - // Update last_on_change_selection_ and fire a 'change' event. - bool fire_on_change = false; - for (unsigned i = 0; i < items.size(); ++i) { - HTMLElement* element = items[i]; - auto* option_element = DynamicTo<HTMLOptionElement>(element); - bool selected = option_element && option_element->Selected(); - if (selected != last_on_change_selection_[i]) - fire_on_change = true; - last_on_change_selection_[i] = selected; - } - - if (fire_on_change) { - DispatchInputEvent(); - DispatchChangeEvent(); - } -} - -void HTMLSelectElement::UpdateMultiSelectListBoxFocus() { - if (!is_multiple_) - return; - - for (auto* const option : GetOptionList()) { - if (option->IsDisabledFormControl() || !option->GetLayoutObject()) - continue; - bool is_focused = - (option == active_selection_end_) && is_in_non_contiguous_selection_; - option->SetMultiSelectFocusedState(is_focused); - } - ScrollToSelection(); -} - -void HTMLSelectElement::DispatchInputAndChangeEventForMenuList() { - DCHECK(UsesMenuList()); - - HTMLOptionElement* selected_option = SelectedOption(); - if (last_on_change_option_.Get() != selected_option) { - last_on_change_option_ = selected_option; - DispatchInputEvent(); - DispatchChangeEvent(); - } -} - void HTMLSelectElement::ScrollToSelection() { if (!IsFinishedParsingChildren()) return; @@ -719,16 +528,6 @@ void HTMLSelectElement::ScrollToSelection() { cache->ListboxActiveIndexChanged(this); } -void HTMLSelectElement::SetOptionsChangedOnLayoutObject() { - if (LayoutObject* layout_object = GetLayoutObject()) { - if (!UsesMenuList()) - return; - ToLayoutMenuList(layout_object) - ->SetNeedsLayoutAndPrefWidthsRecalc( - layout_invalidation_reason::kMenuOptionsChanged); - } -} - const HTMLSelectElement::ListItems& HTMLSelectElement::GetListItems() const { if (should_recalc_list_items_) { RecalcListItems(); @@ -755,7 +554,7 @@ void HTMLSelectElement::SetRecalcListItems() { should_recalc_list_items_ = true; - SetOptionsChangedOnLayoutObject(); + select_type_->MaximumOptionWidthMightBeChanged(); if (!isConnected()) { if (HTMLOptionsCollection* collection = CachedCollection<HTMLOptionsCollection>(kSelectOptions)) @@ -903,12 +702,7 @@ void HTMLSelectElement::SetSuggestedOption(HTMLOptionElement* option) { return; suggested_option_ = option; - if (LayoutObject* layout_object = GetLayoutObject()) { - layout_object->UpdateFromElement(); - ScrollToOption(option); - } - if (PopupIsVisible()) - popup_->UpdateFromElement(PopupMenu::kBySelectionChange); + select_type_->DidSetSuggestedOption(option); } void HTMLSelectElement::ScrollToOption(HTMLOptionElement* option) { @@ -936,11 +730,23 @@ void HTMLSelectElement::ScrollToOptionTask() { // OptionRemoved() makes sure option_to_scroll_to_ doesn't have an option with // another owner. DCHECK_EQ(option->OwnerSelectElement(), this); - GetDocument().UpdateStyleAndLayout(); - if (!GetLayoutObject() || !GetLayoutObject()->IsListBox()) + GetDocument().UpdateStyleAndLayout(DocumentUpdateReason::kScroll); + if (!GetLayoutObject() || UsesMenuList()) return; PhysicalRect bounds = option->BoundingBoxForScrollIntoView(); - ToLayoutListBox(GetLayoutObject())->ScrollToRect(bounds); + + // The following code will not scroll parent boxes unlike ScrollRectToVisible. + auto* box = GetLayoutBox(); + if (!box->HasOverflowClip()) + return; + DCHECK(box->Layer()); + DCHECK(box->Layer()->GetScrollableArea()); + box->Layer()->GetScrollableArea()->ScrollIntoView( + bounds, + ScrollAlignment::CreateScrollIntoViewParams( + ScrollAlignment::ToEdgeIfNeeded(), ScrollAlignment::ToEdgeIfNeeded(), + mojom::blink::ScrollType::kProgrammatic, false, + mojom::blink::ScrollBehavior::kInstant)); } void HTMLSelectElement::OptionSelectionStateChanged(HTMLOptionElement* option, @@ -954,6 +760,41 @@ void HTMLSelectElement::OptionSelectionStateChanged(HTMLOptionElement* option, ResetToDefaultSelection(); } +void HTMLSelectElement::ChildrenChanged(const ChildrenChange& change) { + HTMLFormControlElementWithState::ChildrenChanged(change); + if (change.type == ChildrenChangeType::kElementInserted) { + if (auto* option = DynamicTo<HTMLOptionElement>(change.sibling_changed)) { + OptionInserted(*option, option->Selected()); + } else if (auto* optgroup = + DynamicTo<HTMLOptGroupElement>(change.sibling_changed)) { + for (auto& option : Traversal<HTMLOptionElement>::ChildrenOf(*optgroup)) + OptionInserted(option, option.Selected()); + } + } else if (change.type == ChildrenChangeType::kElementRemoved) { + if (auto* option = DynamicTo<HTMLOptionElement>(change.sibling_changed)) { + OptionRemoved(*option); + } else if (auto* optgroup = + DynamicTo<HTMLOptGroupElement>(change.sibling_changed)) { + for (auto& option : Traversal<HTMLOptionElement>::ChildrenOf(*optgroup)) + OptionRemoved(option); + } + } else if (change.type == ChildrenChangeType::kAllChildrenRemoved) { + DCHECK(change.removed_nodes); + for (Node* node : *change.removed_nodes) { + if (auto* option = DynamicTo<HTMLOptionElement>(node)) { + OptionRemoved(*option); + } else if (auto* optgroup = DynamicTo<HTMLOptGroupElement>(node)) { + for (auto& option : Traversal<HTMLOptionElement>::ChildrenOf(*optgroup)) + OptionRemoved(option); + } + } + } +} + +bool HTMLSelectElement::ChildrenChangedAllChildrenRemovedNeedsList() const { + return true; +} + void HTMLSelectElement::OptionInserted(HTMLOptionElement& option, bool option_is_selected) { DCHECK_EQ(option.OwnerSelectElement(), this); @@ -966,7 +807,7 @@ void HTMLSelectElement::OptionInserted(HTMLOptionElement& option, ResetToDefaultSelection(); } SetNeedsValidityCheck(); - last_on_change_selection_.clear(); + select_type_->ClearLastOnChangeSelection(); if (!GetDocument().IsActive()) return; @@ -997,7 +838,7 @@ void HTMLSelectElement::OptionRemoved(HTMLOptionElement& option) { if (option.Selected()) SetAutofillState(WebAutofillState::kNotFilled); SetNeedsValidityCheck(); - last_on_change_selection_.clear(); + select_type_->ClearLastOnChangeSelection(); if (!GetDocument().IsActive()) return; @@ -1013,12 +854,12 @@ void HTMLSelectElement::OptGroupInsertedOrRemoved( HTMLOptGroupElement& optgroup) { SetRecalcListItems(); SetNeedsValidityCheck(); - last_on_change_selection_.clear(); + select_type_->ClearLastOnChangeSelection(); } void HTMLSelectElement::HrInsertedOrRemoved(HTMLHRElement& hr) { SetRecalcListItems(); - last_on_change_selection_.clear(); + select_type_->ClearLastOnChangeSelection(); } // TODO(tkent): This function is not efficient. It contains multiple O(N) @@ -1057,40 +898,7 @@ void HTMLSelectElement::SelectOption(HTMLOptionElement* element, SetActiveSelectionEnd(element); } - // Need to update last_on_change_option_ before - // LayoutMenuList::UpdateFromElement. - bool should_dispatch_events = false; - if (UsesMenuList()) { - should_dispatch_events = (flags & kDispatchInputAndChangeEventFlag) && - last_on_change_option_ != element; - last_on_change_option_ = element; - } - - // For the menu list case, this is what makes the selected element appear. - if (LayoutObject* layout_object = GetLayoutObject()) - layout_object->UpdateFromElement(); - // PopupMenu::UpdateFromElement() posts an O(N) task. - if (PopupIsVisible() && should_update_popup) - popup_->UpdateFromElement(PopupMenu::kBySelectionChange); - - ScrollToSelection(); - SetNeedsValidityCheck(); - - if (UsesMenuList()) { - if (should_dispatch_events) { - DispatchInputEvent(); - DispatchChangeEvent(); - } - if (LayoutObject* layout_object = GetLayoutObject()) { - // Need to check UsesMenuList() again because event handlers might - // change the status. - if (UsesMenuList()) { - // DidSelectOption() is O(N) because of HTMLOptionElement::index(). - ToLayoutMenuList(layout_object)->DidSelectOption(element); - } - } - } - + select_type_->DidSelectOption(element, flags, should_update_popup); NotifyFormStateChanged(); if (LocalFrame::HasTransientUserActivation(GetDocument().GetFrame()) && @@ -1104,29 +912,22 @@ void HTMLSelectElement::SelectOption(HTMLOptionElement* element, void HTMLSelectElement::DispatchFocusEvent( Element* old_focused_element, - WebFocusType type, + mojom::blink::FocusType type, InputDeviceCapabilities* source_capabilities) { // Save the selection so it can be compared to the new selection when // dispatching change events during blur event dispatch. if (UsesMenuList()) - SaveLastSelection(); + select_type_->SaveLastSelection(); HTMLFormControlElementWithState::DispatchFocusEvent(old_focused_element, type, source_capabilities); } void HTMLSelectElement::DispatchBlurEvent( Element* new_focused_element, - WebFocusType type, + mojom::blink::FocusType type, InputDeviceCapabilities* source_capabilities) { type_ahead_.ResetSession(); - // We only need to fire change events here for menu lists, because we fire - // change events for list boxes whenever the selection change is actually - // made. This matches other browsers' behavior. - if (UsesMenuList()) - DispatchInputAndChangeEventForMenuList(); - last_on_change_selection_.clear(); - if (PopupIsVisible()) - HidePopup(); + select_type_->DidBlur(); HTMLFormControlElementWithState::DispatchBlurEvent(new_focused_element, type, source_capabilities); } @@ -1240,6 +1041,7 @@ void HTMLSelectElement::RestoreFormControlState(const FormControlState& state) { } SetNeedsValidityCheck(); + select_type_->UpdateTextStyleAndContent(); } void HTMLSelectElement::ParseMultipleAttribute(const AtomicString& value) { @@ -1248,6 +1050,7 @@ void HTMLSelectElement::ParseMultipleAttribute(const AtomicString& value) { is_multiple_ = !value.IsNull(); SetNeedsValidityCheck(); ChangeRendering(); + UpdateUserAgentShadowTree(*UserAgentShadowRoot()); // Restore selectedIndex after changing the multiple flag to preserve // selection as single-line and multi-line has different defaults. if (old_multiple != is_multiple_) { @@ -1259,6 +1062,7 @@ void HTMLSelectElement::ParseMultipleAttribute(const AtomicString& value) { else ResetToDefaultSelection(); } + select_type_->UpdateTextStyleAndContent(); } void HTMLSelectElement::AppendToFormData(FormData& form_data) { @@ -1279,228 +1083,12 @@ void HTMLSelectElement::ResetImpl() { option->SetDirty(false); } ResetToDefaultSelection(); + select_type_->UpdateTextStyleAndContent(); SetNeedsValidityCheck(); } -void HTMLSelectElement::HandlePopupOpenKeyboardEvent(Event& event) { - focus(); - // Calling focus() may cause us to lose our layoutObject. Return true so - // that our caller doesn't process the event further, but don't set - // the event as handled. - if (!GetLayoutObject() || !GetLayoutObject()->IsMenuList() || - IsDisabledFormControl()) - return; - // Save the selection so it can be compared to the new selection when - // dispatching change events during selectOption, which gets called from - // selectOptionByPopup, which gets called after the user makes a selection - // from the menu. - SaveLastSelection(); - ShowPopup(); - event.SetDefaultHandled(); - return; -} - -bool HTMLSelectElement::ShouldOpenPopupForKeyDownEvent( - const KeyboardEvent& key_event) { - const String& key = key_event.key(); - LayoutTheme& layout_theme = LayoutTheme::GetTheme(); - - if (IsSpatialNavigationEnabled(GetDocument().GetFrame())) - return false; - - return ((layout_theme.PopsMenuByArrowKeys() && - (key == "ArrowDown" || key == "ArrowUp")) || - (layout_theme.PopsMenuByAltDownUpOrF4Key() && - (key == "ArrowDown" || key == "ArrowUp") && key_event.altKey()) || - (layout_theme.PopsMenuByAltDownUpOrF4Key() && - (!key_event.altKey() && !key_event.ctrlKey() && key == "F4"))); -} - -bool HTMLSelectElement::ShouldOpenPopupForKeyPressEvent( - const KeyboardEvent& event) { - LayoutTheme& layout_theme = LayoutTheme::GetTheme(); - int key_code = event.keyCode(); - - return ((layout_theme.PopsMenuBySpaceKey() && key_code == ' ' && - !type_ahead_.HasActiveSession(event)) || - (layout_theme.PopsMenuByReturnKey() && key_code == '\r')); -} - -void HTMLSelectElement::MenuListDefaultEventHandler(Event& event) { - // We need to make the layout tree up-to-date to have GetLayoutObject() give - // the correct result below. An author event handler may have set display to - // some element to none which will cause a layout tree detach. - GetDocument().UpdateStyleAndLayoutTree(); - - if (event.type() == event_type_names::kKeydown) { - if (!GetLayoutObject() || !event.IsKeyboardEvent()) - return; - - auto& key_event = ToKeyboardEvent(event); - if (ShouldOpenPopupForKeyDownEvent(key_event)) { - HandlePopupOpenKeyboardEvent(event); - return; - } - - // When using spatial navigation, we want to be able to navigate away - // from the select element when the user hits any of the arrow keys, - // instead of changing the selection. - if (IsSpatialNavigationEnabled(GetDocument().GetFrame())) { - if (!active_selection_state_) - return; - } - - // The key handling below shouldn't be used for non spatial navigation - // mode Mac - if (LayoutTheme::GetTheme().PopsMenuByArrowKeys() && - !IsSpatialNavigationEnabled(GetDocument().GetFrame())) - return; - - int ignore_modifiers = WebInputEvent::kShiftKey | - WebInputEvent::kControlKey | WebInputEvent::kAltKey | - WebInputEvent::kMetaKey; - if (key_event.GetModifiers() & ignore_modifiers) - return; - - const String& key = key_event.key(); - bool handled = true; - const ListItems& list_items = GetListItems(); - HTMLOptionElement* option = SelectedOption(); - int list_index = option ? option->ListIndex() : -1; - - if (key == "ArrowDown" || key == "ArrowRight") - option = NextValidOption(list_index, kSkipForwards, 1); - else if (key == "ArrowUp" || key == "ArrowLeft") - option = NextValidOption(list_index, kSkipBackwards, 1); - else if (key == "PageDown") - option = NextValidOption(list_index, kSkipForwards, 3); - else if (key == "PageUp") - option = NextValidOption(list_index, kSkipBackwards, 3); - else if (key == "Home") - option = NextValidOption(-1, kSkipForwards, 1); - else if (key == "End") - option = NextValidOption(list_items.size(), kSkipBackwards, 1); - else - handled = false; - - if (handled && option) { - SelectOption(option, kDeselectOtherOptionsFlag | kMakeOptionDirtyFlag | - kDispatchInputAndChangeEventFlag); - } - - if (handled) - event.SetDefaultHandled(); - } - - if (event.type() == event_type_names::kKeypress) { - if (!GetLayoutObject() || !event.IsKeyboardEvent()) - return; - - int key_code = ToKeyboardEvent(event).keyCode(); - if (key_code == ' ' && - IsSpatialNavigationEnabled(GetDocument().GetFrame())) { - // Use space to toggle arrow key handling for selection change or - // spatial navigation. - active_selection_state_ = !active_selection_state_; - event.SetDefaultHandled(); - return; - } - - auto& key_event = ToKeyboardEvent(event); - if (ShouldOpenPopupForKeyPressEvent(key_event)) { - HandlePopupOpenKeyboardEvent(event); - return; - } - - if (!LayoutTheme::GetTheme().PopsMenuByReturnKey() && key_code == '\r') { - if (Form()) - Form()->SubmitImplicitly(event, false); - DispatchInputAndChangeEventForMenuList(); - event.SetDefaultHandled(); - } - } - - if (event.type() == event_type_names::kMousedown && event.IsMouseEvent() && - ToMouseEvent(event).button() == - static_cast<int16_t>(WebPointerProperties::Button::kLeft)) { - InputDeviceCapabilities* source_capabilities = - GetDocument() - .domWindow() - ->GetInputDeviceCapabilities() - ->FiresTouchEvents(ToMouseEvent(event).FromTouch()); - focus(FocusParams(SelectionBehaviorOnFocus::kRestore, kWebFocusTypeNone, - source_capabilities)); - if (GetLayoutObject() && GetLayoutObject()->IsMenuList() && - !IsDisabledFormControl()) { - if (PopupIsVisible()) { - HidePopup(); - } else { - // Save the selection so it can be compared to the new selection - // when we call onChange during selectOption, which gets called - // from selectOptionByPopup, which gets called after the user - // makes a selection from the menu. - SaveLastSelection(); - // TODO(lanwei): Will check if we need to add - // InputDeviceCapabilities here when select menu list gets - // focus, see https://crbug.com/476530. - ShowPopup(); - } - } - event.SetDefaultHandled(); - } -} - -void HTMLSelectElement::UpdateSelectedState(HTMLOptionElement* clicked_option, - bool multi, - bool shift) { - DCHECK(clicked_option); - // Save the selection so it can be compared to the new selection when - // dispatching change events during mouseup, or after autoscroll finishes. - SaveLastSelection(); - - active_selection_state_ = true; - - bool shift_select = is_multiple_ && shift; - bool multi_select = is_multiple_ && multi && !shift; - - // Keep track of whether an active selection (like during drag selection), - // should select or deselect. - if (clicked_option->Selected() && multi_select) { - active_selection_state_ = false; - clicked_option->SetSelectedState(false); - clicked_option->SetDirty(true); - } - - // If we're not in any special multiple selection mode, then deselect all - // other items, excluding the clicked option. If no option was clicked, then - // this will deselect all items in the list. - if (!shift_select && !multi_select) - DeselectItemsWithoutValidation(clicked_option); - - // If the anchor hasn't been set, and we're doing a single selection or a - // shift selection, then initialize the anchor to the first selected index. - if (!active_selection_anchor_ && !multi_select) - SetActiveSelectionAnchor(SelectedOption()); - - // Set the selection state of the clicked option. - if (!clicked_option->IsDisabledFormControl()) { - clicked_option->SetSelectedState(true); - clicked_option->SetDirty(true); - } - - // If there was no selectedIndex() for the previous initialization, or If - // we're doing a single selection, or a multiple selection (using cmd or - // ctrl), then initialize the anchor index to the listIndex that just got - // clicked. - if (!active_selection_anchor_ || !shift_select) - SetActiveSelectionAnchor(clicked_option); - - SetActiveSelectionEnd(clicked_option); - UpdateListBoxSelection(!multi_select); -} - -HTMLOptionElement* HTMLSelectElement::EventTargetOption(const Event& event) { - return DynamicTo<HTMLOptionElement>(event.target()->ToNode()); +bool HTMLSelectElement::PopupIsVisible() const { + return select_type_->PopupIsVisible(); } int HTMLSelectElement::ListIndexForOption(const HTMLOptionElement& option) { @@ -1519,259 +1107,13 @@ AutoscrollController* HTMLSelectElement::GetAutoscrollController() const { return nullptr; } -void HTMLSelectElement::HandleMouseRelease() { - // We didn't start this click/drag on any options. - if (last_on_change_selection_.IsEmpty()) - return; - ListBoxOnChange(); -} - -void HTMLSelectElement::ListBoxDefaultEventHandler(Event& event) { - if (event.type() == event_type_names::kGesturetap && event.IsGestureEvent()) { - focus(); - // Calling focus() may cause us to lose our layoutObject or change the - // layoutObject type, in which case do not want to handle the event. - if (!GetLayoutObject() || !GetLayoutObject()->IsListBox()) - return; - - // Convert to coords relative to the list box if needed. - auto& gesture_event = ToGestureEvent(event); - if (HTMLOptionElement* option = EventTargetOption(gesture_event)) { - if (!IsDisabledFormControl()) { - UpdateSelectedState(option, true, gesture_event.shiftKey()); - ListBoxOnChange(); - } - event.SetDefaultHandled(); - } - - } else if (event.type() == event_type_names::kMousedown && - event.IsMouseEvent() && - ToMouseEvent(event).button() == - static_cast<int16_t>(WebPointerProperties::Button::kLeft)) { - focus(); - // Calling focus() may cause us to lose our layoutObject, in which case - // do not want to handle the event. - if (!GetLayoutObject() || !GetLayoutObject()->IsListBox() || - IsDisabledFormControl()) - return; - - // Convert to coords relative to the list box if needed. - auto& mouse_event = ToMouseEvent(event); - if (HTMLOptionElement* option = EventTargetOption(mouse_event)) { - if (!option->IsDisabledFormControl()) { -#if defined(OS_MACOSX) - UpdateSelectedState(option, mouse_event.metaKey(), - mouse_event.shiftKey()); -#else - UpdateSelectedState(option, mouse_event.ctrlKey(), - mouse_event.shiftKey()); -#endif - } - if (LocalFrame* frame = GetDocument().GetFrame()) - frame->GetEventHandler().SetMouseDownMayStartAutoscroll(); - - event.SetDefaultHandled(); - } - - } else if (event.type() == event_type_names::kMousemove && - event.IsMouseEvent()) { - auto& mouse_event = ToMouseEvent(event); - if (mouse_event.button() != - static_cast<int16_t>(WebPointerProperties::Button::kLeft) || - !mouse_event.ButtonDown()) - return; - - LayoutObject* layout_object = GetLayoutObject(); - if (layout_object) { - layout_object->GetFrameView()->UpdateAllLifecyclePhasesExceptPaint(); - - if (Page* page = GetDocument().GetPage()) { - page->GetAutoscrollController().StartAutoscrollForSelection( - layout_object); - } - } - // Mousedown didn't happen in this element. - if (last_on_change_selection_.IsEmpty()) - return; - - if (HTMLOptionElement* option = EventTargetOption(mouse_event)) { - if (!IsDisabledFormControl()) { - if (is_multiple_) { - // Only extend selection if there is something selected. - if (!active_selection_anchor_) - return; - - SetActiveSelectionEnd(option); - UpdateListBoxSelection(false); - } else { - SetActiveSelectionAnchor(option); - SetActiveSelectionEnd(option); - UpdateListBoxSelection(true); - } - } - } - - } else if (event.type() == event_type_names::kMouseup && - event.IsMouseEvent() && - ToMouseEvent(event).button() == - static_cast<int16_t>(WebPointerProperties::Button::kLeft) && - GetLayoutObject()) { - if (GetDocument().GetPage() && - GetDocument() - .GetPage() - ->GetAutoscrollController() - .AutoscrollInProgressFor(ToLayoutBox(GetLayoutObject()))) - GetDocument().GetPage()->GetAutoscrollController().StopAutoscroll(); - else - HandleMouseRelease(); - - } else if (event.type() == event_type_names::kKeydown) { - if (!event.IsKeyboardEvent()) - return; - const String& key = ToKeyboardEvent(event).key(); - - bool handled = false; - HTMLOptionElement* end_option = nullptr; - if (!active_selection_end_) { - // Initialize the end index - if (key == "ArrowDown" || key == "PageDown") { - HTMLOptionElement* start_option = LastSelectedOption(); - handled = true; - if (key == "ArrowDown") { - end_option = NextSelectableOption(start_option); - } else { - end_option = - NextSelectableOptionPageAway(start_option, kSkipForwards); - } - } else if (key == "ArrowUp" || key == "PageUp") { - HTMLOptionElement* start_option = SelectedOption(); - handled = true; - if (key == "ArrowUp") { - end_option = PreviousSelectableOption(start_option); - } else { - end_option = - NextSelectableOptionPageAway(start_option, kSkipBackwards); - } - } - } else { - // Set the end index based on the current end index. - if (key == "ArrowDown") { - end_option = NextSelectableOption(active_selection_end_.Get()); - handled = true; - } else if (key == "ArrowUp") { - end_option = PreviousSelectableOption(active_selection_end_.Get()); - handled = true; - } else if (key == "PageDown") { - end_option = NextSelectableOptionPageAway(active_selection_end_.Get(), - kSkipForwards); - handled = true; - } else if (key == "PageUp") { - end_option = NextSelectableOptionPageAway(active_selection_end_.Get(), - kSkipBackwards); - handled = true; - } - } - if (key == "Home") { - end_option = FirstSelectableOption(); - handled = true; - } else if (key == "End") { - end_option = LastSelectableOption(); - handled = true; - } - - if (IsSpatialNavigationEnabled(GetDocument().GetFrame())) { - // Check if the selection moves to the boundary. - if (key == "ArrowLeft" || key == "ArrowRight" || - ((key == "ArrowDown" || key == "ArrowUp") && - end_option == active_selection_end_)) - return; - } - - bool is_control_key = false; -#if defined(OS_MACOSX) - is_control_key = ToKeyboardEvent(event).metaKey(); -#else - is_control_key = ToKeyboardEvent(event).ctrlKey(); -#endif - - if (is_multiple_ && ToKeyboardEvent(event).keyCode() == ' ' && - is_control_key && active_selection_end_) { - // Use ctrl+space to toggle selection change. - ToggleSelection(*active_selection_end_); - event.SetDefaultHandled(); - return; - } - - if (end_option && handled) { - // Save the selection so it can be compared to the new selection - // when dispatching change events immediately after making the new - // selection. - SaveLastSelection(); - - SetActiveSelectionEnd(end_option); - - is_in_non_contiguous_selection_ = is_multiple_ && is_control_key; - bool select_new_item = - !is_multiple_ || ToKeyboardEvent(event).shiftKey() || - (!IsSpatialNavigationEnabled(GetDocument().GetFrame()) && - !is_in_non_contiguous_selection_); - if (select_new_item) - active_selection_state_ = true; - // If the anchor is uninitialized, or if we're going to deselect all - // other options, then set the anchor index equal to the end index. - bool deselect_others = - !is_multiple_ || - (!ToKeyboardEvent(event).shiftKey() && select_new_item); - if (!active_selection_anchor_ || deselect_others) { - if (deselect_others) - DeselectItemsWithoutValidation(); - SetActiveSelectionAnchor(active_selection_end_.Get()); - } - - ScrollToOption(end_option); - if (select_new_item || is_in_non_contiguous_selection_) { - if (select_new_item) { - UpdateListBoxSelection(deselect_others); - ListBoxOnChange(); - } - UpdateMultiSelectListBoxFocus(); - } else { - ScrollToSelection(); - } - - event.SetDefaultHandled(); - } - - } else if (event.type() == event_type_names::kKeypress) { - if (!event.IsKeyboardEvent()) - return; - int key_code = ToKeyboardEvent(event).keyCode(); - - if (key_code == '\r') { - if (Form()) - Form()->SubmitImplicitly(event, false); - event.SetDefaultHandled(); - } else if (is_multiple_ && key_code == ' ' && - (IsSpatialNavigationEnabled(GetDocument().GetFrame()) || - is_in_non_contiguous_selection_)) { - HTMLOptionElement* option = active_selection_end_; - // If there's no active selection, - // act as if "ArrowDown" had been pressed. - if (!option) - option = NextSelectableOption(LastSelectedOption()); - if (option) { - // Use space to toggle selection change. - ToggleSelection(*option); - event.SetDefaultHandled(); - } - } - } +LayoutBox* HTMLSelectElement::AutoscrollBox() { + return !UsesMenuList() ? GetLayoutBox() : nullptr; } -void HTMLSelectElement::ToggleSelection(HTMLOptionElement& option) { - active_selection_state_ = !active_selection_state_; - UpdateSelectedState(&option, true /*multi*/, false /*shift*/); - ListBoxOnChange(); +void HTMLSelectElement::StopAutoscroll() { + if (!IsDisabledFormControl()) + select_type_->HandleMouseRelease(); } void HTMLSelectElement::DefaultEventHandler(Event& event) { @@ -1788,19 +1130,17 @@ void HTMLSelectElement::DefaultEventHandler(Event& event) { return; } - if (UsesMenuList()) - MenuListDefaultEventHandler(event); - else - ListBoxDefaultEventHandler(event); - if (event.DefaultHandled()) + if (select_type_->DefaultEventHandler(event)) { + event.SetDefaultHandled(); return; + } - if (event.type() == event_type_names::kKeypress && event.IsKeyboardEvent()) { - auto& keyboard_event = ToKeyboardEvent(event); - if (!keyboard_event.ctrlKey() && !keyboard_event.altKey() && - !keyboard_event.metaKey() && - WTF::unicode::IsPrintableChar(keyboard_event.charCode())) { - TypeAheadFind(keyboard_event); + auto* keyboard_event = DynamicTo<KeyboardEvent>(event); + if (event.type() == event_type_names::kKeypress && keyboard_event) { + if (!keyboard_event->ctrlKey() && !keyboard_event->altKey() && + !keyboard_event->metaKey() && + WTF::unicode::IsPrintableChar(keyboard_event->charCode())) { + TypeAheadFind(*keyboard_event); event.SetDefaultHandled(); return; } @@ -1843,8 +1183,7 @@ void HTMLSelectElement::TypeAheadFind(const KeyboardEvent& event) { SelectOption(OptionAtListIndex(index), kDeselectOtherOptionsFlag | kMakeOptionDirtyFlag | kDispatchInputAndChangeEventFlag); - if (!UsesMenuList()) - ListBoxOnChange(); + select_type_->ListBoxOnChange(); } void HTMLSelectElement::SelectOptionByAccessKey(HTMLOptionElement* option) { @@ -1870,7 +1209,7 @@ void HTMLSelectElement::SelectOptionByAccessKey(HTMLOptionElement* option) { option->SetDirty(true); if (UsesMenuList()) return; - ListBoxOnChange(); + select_type_->ListBoxOnChange(); ScrollToSelection(); } @@ -1892,16 +1231,16 @@ void HTMLSelectElement::FinishParsingChildren() { cache->ListboxActiveIndexChanged(this); } -bool HTMLSelectElement::AnonymousIndexedSetter( +IndexedPropertySetterResult HTMLSelectElement::AnonymousIndexedSetter( unsigned index, HTMLOptionElement* value, ExceptionState& exception_state) { if (!value) { // undefined or null remove(index); - return true; + return IndexedPropertySetterResult::kIntercepted; } SetOption(index, value, exception_state); - return true; + return IndexedPropertySetterResult::kIntercepted; } bool HTMLSelectElement::IsInteractiveContent() const { @@ -1915,14 +1254,47 @@ void HTMLSelectElement::Trace(Visitor* visitor) { visitor->Trace(active_selection_end_); visitor->Trace(option_to_scroll_to_); visitor->Trace(suggested_option_); - visitor->Trace(popup_); - visitor->Trace(popup_updater_); + visitor->Trace(select_type_); HTMLFormControlElementWithState::Trace(visitor); } void HTMLSelectElement::DidAddUserAgentShadowRoot(ShadowRoot& root) { + // Even if UsesMenuList(), the <slot> is necessary to have ComputedStyles + // for <option>s. LayoutFlexibleBox::IsChildAllowed() rejects all of + // LayoutObject children except for MenuListInnerElement's. root.AppendChild( HTMLSlotElement::CreateUserAgentCustomAssignSlot(GetDocument())); + UpdateUserAgentShadowTree(root); + select_type_->UpdateTextStyleAndContent(); +} + +void HTMLSelectElement::UpdateUserAgentShadowTree(ShadowRoot& root) { + // Remove all children of the ShadowRoot except for <slot>. + Node* node = root.firstChild(); + while (node) { + if (IsA<HTMLSlotElement>(node)) { + node = node->nextSibling(); + } else { + auto* will_be_removed = node; + node = node->nextSibling(); + will_be_removed->remove(); + } + } + if (UsesMenuList()) { + Element* inner_element = + MakeGarbageCollected<MenuListInnerElement>(GetDocument()); + inner_element->setAttribute(html_names::kAriaHiddenAttr, "true"); + // Make sure InnerElement() always has a Text node. + inner_element->appendChild(Text::Create(GetDocument(), g_empty_string)); + root.insertBefore(inner_element, root.firstChild()); + } +} + +Element& HTMLSelectElement::InnerElement() const { + DCHECK(UsesMenuList()); + auto* inner_element = DynamicTo<Element>(UserAgentShadowRoot()->firstChild()); + DCHECK(inner_element); + return *inner_element; } HTMLOptionElement* HTMLSelectElement::SpatialNavigationFocusedOption() { @@ -1930,7 +1302,7 @@ HTMLOptionElement* HTMLSelectElement::SpatialNavigationFocusedOption() { return nullptr; HTMLOptionElement* focused_option = ActiveSelectionEnd(); if (!focused_option) - focused_option = FirstSelectableOption(); + focused_option = select_type_->FirstSelectableOption(); return focused_option; } @@ -1960,42 +1332,44 @@ const ComputedStyle* HTMLSelectElement::ItemComputedStyle( } LayoutUnit HTMLSelectElement::ClientPaddingLeft() const { - if (GetLayoutObject() && GetLayoutObject()->IsMenuList()) - return ToLayoutMenuList(GetLayoutObject())->ClientPaddingLeft(); - return LayoutUnit(); + DCHECK(UsesMenuList()); + auto* this_box = GetLayoutBox(); + if (!this_box || !InnerElement().GetLayoutBox()) + return LayoutUnit(); + LayoutTheme& theme = LayoutTheme::GetTheme(); + const ComputedStyle& style = this_box->StyleRef(); + int inner_padding = + style.IsLeftToRightDirection() + ? theme.PopupInternalPaddingStart(style) + : theme.PopupInternalPaddingEnd(GetDocument().GetFrame(), style); + return this_box->PaddingLeft() + inner_padding; } LayoutUnit HTMLSelectElement::ClientPaddingRight() const { - if (GetLayoutObject() && GetLayoutObject()->IsMenuList()) - return ToLayoutMenuList(GetLayoutObject())->ClientPaddingRight(); - return LayoutUnit(); + DCHECK(UsesMenuList()); + auto* this_box = GetLayoutBox(); + if (!this_box || !InnerElement().GetLayoutBox()) + return LayoutUnit(); + LayoutTheme& theme = LayoutTheme::GetTheme(); + const ComputedStyle& style = this_box->StyleRef(); + int inner_padding = + style.IsLeftToRightDirection() + ? theme.PopupInternalPaddingEnd(GetDocument().GetFrame(), style) + : theme.PopupInternalPaddingStart(style); + return this_box->PaddingRight() + inner_padding; } void HTMLSelectElement::PopupDidHide() { - popup_is_visible_ = false; - UnobserveTreeMutation(); - if (AXObjectCache* cache = GetDocument().ExistingAXObjectCache()) { - if (GetLayoutObject() && GetLayoutObject()->IsMenuList()) - cache->DidHideMenuListPopup(ToLayoutMenuList(GetLayoutObject())); - } + select_type_->PopupDidHide(); } void HTMLSelectElement::SetIndexToSelectOnCancel(int list_index) { index_to_select_on_cancel_ = list_index; - if (GetLayoutObject()) - GetLayoutObject()->UpdateFromElement(); + select_type_->UpdateTextStyleAndContent(); } -HTMLOptionElement* HTMLSelectElement::OptionToBeShown() const { - if (HTMLOptionElement* option = OptionAtListIndex(index_to_select_on_cancel_)) - return option; - if (suggested_option_) - return suggested_option_; - // TODO(tkent): We should not call optionToBeShown() in isMultiple() case. - if (IsMultiple()) - return SelectedOption(); - DCHECK_EQ(SelectedOption(), last_on_change_option_); - return last_on_change_option_; +HTMLOptionElement* HTMLSelectElement::OptionToBeShownForTesting() const { + return select_type_->OptionToBeShown(); } void HTMLSelectElement::SelectOptionByPopup(int list_index) { @@ -2031,44 +1405,28 @@ void HTMLSelectElement::ProvisionalSelectionChanged(unsigned list_index) { } void HTMLSelectElement::ShowPopup() { - if (PopupIsVisible()) - return; - if (GetDocument().GetPage()->GetChromeClient().HasOpenedPopup()) - return; - if (!GetLayoutObject() || !GetLayoutObject()->IsMenuList()) - return; - if (VisibleBoundsInVisualViewport().IsEmpty()) - return; - - if (!popup_) { - popup_ = GetDocument().GetPage()->GetChromeClient().OpenPopupMenu( - *GetDocument().GetFrame(), *this); - } - if (!popup_) - return; - - popup_is_visible_ = true; - ObserveTreeMutation(); - - LayoutMenuList* menu_list = ToLayoutMenuList(GetLayoutObject()); - popup_->Show(); - if (AXObjectCache* cache = GetDocument().ExistingAXObjectCache()) - cache->DidShowMenuListPopup(menu_list); + select_type_->ShowPopup(); } void HTMLSelectElement::HidePopup() { - if (popup_) - popup_->Hide(); + select_type_->HidePopup(); +} + +PopupMenu* HTMLSelectElement::PopupForTesting() const { + return select_type_->PopupForTesting(); } void HTMLSelectElement::DidRecalcStyle(const StyleRecalcChange change) { HTMLFormControlElementWithState::DidRecalcStyle(change); - if (!change.ReattachLayoutTree() && PopupIsVisible()) - popup_->UpdateFromElement(PopupMenu::kByStyleChange); + select_type_->DidRecalcStyle(change); } void HTMLSelectElement::AttachLayoutTree(AttachContext& context) { HTMLFormControlElementWithState::AttachLayoutTree(context); + // The call to UpdateTextStyle() needs to go after the call through + // to the base class's AttachLayoutTree() because that can sometimes do a + // close on the LayoutObject. + select_type_->UpdateTextStyle(); if (const ComputedStyle* style = GetComputedStyle()) { if (style->Visibility() != EVisibility::kHidden) { @@ -2082,90 +1440,13 @@ void HTMLSelectElement::AttachLayoutTree(AttachContext& context) { void HTMLSelectElement::DetachLayoutTree(bool performing_reattach) { HTMLFormControlElementWithState::DetachLayoutTree(performing_reattach); - if (popup_) - popup_->DisconnectClient(); - popup_is_visible_ = false; - popup_ = nullptr; - UnobserveTreeMutation(); + select_type_->DidDetachLayoutTree(); } void HTMLSelectElement::ResetTypeAheadSessionForTesting() { type_ahead_.ResetSession(); } -// PopupUpdater notifies updates of the specified SELECT element subtree to -// a PopupMenu object. -class HTMLSelectElement::PopupUpdater : public MutationObserver::Delegate { - public: - explicit PopupUpdater(HTMLSelectElement& select) - : select_(select), observer_(MutationObserver::Create(this)) { - MutationObserverInit* init = MutationObserverInit::Create(); - init->setAttributeOldValue(true); - init->setAttributes(true); - // Observe only attributes which affect popup content. - init->setAttributeFilter({"disabled", "label", "selected", "value"}); - init->setCharacterData(true); - init->setCharacterDataOldValue(true); - init->setChildList(true); - init->setSubtree(true); - observer_->observe(select_, init, ASSERT_NO_EXCEPTION); - } - - ExecutionContext* GetExecutionContext() const override { - return &select_->GetDocument(); - } - - void Deliver(const MutationRecordVector& records, - MutationObserver&) override { - // We disconnect the MutationObserver when a popup is closed. However - // MutationObserver can call back after disconnection. - if (!select_->PopupIsVisible()) - return; - for (const auto& record : records) { - if (record->type() == "attributes") { - const auto& element = *To<Element>(record->target()); - if (record->oldValue() == element.getAttribute(record->attributeName())) - continue; - } else if (record->type() == "characterData") { - if (record->oldValue() == record->target()->nodeValue()) - continue; - } - select_->DidMutateSubtree(); - return; - } - } - - void Dispose() { observer_->disconnect(); } - - void Trace(Visitor* visitor) override { - visitor->Trace(select_); - visitor->Trace(observer_); - MutationObserver::Delegate::Trace(visitor); - } - - private: - Member<HTMLSelectElement> select_; - Member<MutationObserver> observer_; -}; - -void HTMLSelectElement::ObserveTreeMutation() { - DCHECK(!popup_updater_); - popup_updater_ = MakeGarbageCollected<PopupUpdater>(*this); -} - -void HTMLSelectElement::UnobserveTreeMutation() { - if (!popup_updater_) - return; - popup_updater_->Dispose(); - popup_updater_ = nullptr; -} - -void HTMLSelectElement::DidMutateSubtree() { - DCHECK(PopupIsVisible()); - DCHECK(popup_); - popup_->UpdateFromElement(PopupMenu::kByDOMChange); -} - void HTMLSelectElement::CloneNonAttributePropertiesFrom( const Element& source, CloneChildrenFlag flag) { @@ -2175,6 +1456,10 @@ void HTMLSelectElement::CloneNonAttributePropertiesFrom( } void HTMLSelectElement::ChangeRendering() { + select_type_->DidDetachLayoutTree(); + UpdateUsesMenuList(); + select_type_->WillBeDestroyed(); + select_type_ = SelectType::Create(*this); if (!InActiveDocument()) return; // TODO(futhark): SetForceReattachLayoutTree() should be the correct way to @@ -2185,4 +1470,8 @@ void HTMLSelectElement::ChangeRendering() { style_change_reason::kControl)); } +const ComputedStyle* HTMLSelectElement::OptionStyle() const { + return select_type_->OptionStyle(); +} + } // namespace blink |