[SelectMenu] Implement type-ahead

BUG=1422117,1432440

Change-Id: Id29c4281605c10c71ddcbc76fa1c7cc6b26c3aed
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4450748
Reviewed-by: Joey Arhar <jarhar@chromium.org>
Reviewed-by: Dominic Battre <battre@chromium.org>
Commit-Queue: Peter Kotwicz <pkotwicz@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1135552}
diff --git a/chrome/browser/autofill/autofill_interactive_uitest.cc b/chrome/browser/autofill/autofill_interactive_uitest.cc
index a0fe309..403e15e 100644
--- a/chrome/browser/autofill/autofill_interactive_uitest.cc
+++ b/chrome/browser/autofill/autofill_interactive_uitest.cc
@@ -1308,28 +1308,6 @@
     ASSERT_EQ(value, std::move(value_waiter).Wait());
   }
 
-  // TODO(crbug.com/1422117): Merge with FillElementWithValueAndBlur() once
-  // typeahead support is implemented for <selectmenu>. `option_index` is the
-  // index of the option to select. Assumes that the option at index 0 is
-  // initially selected.
-  void FillSelectMenuElementWithValue(const std::string& element_id,
-                                      const std::string& value,
-                                      size_t option_index) {
-    ValueWaiter value_waiter =
-        ListenForChangeToSpecificValue(element_id, value);
-
-    SimulateKeyPress(ui::DomKey::ENTER, ui::DomCode::ENTER, /*shift=*/false);
-
-    for (size_t i = 0; i < option_index; ++i) {
-      SimulateKeyPress(ui::DomKey::ARROW_DOWN, ui::DomCode::ARROW_DOWN,
-                       /*shift=*/false);
-    }
-
-    SimulateKeyPress(ui::DomKey::ENTER, ui::DomCode::ENTER,
-                     /*shift=*/false);
-    ASSERT_EQ(value, std::move(value_waiter).Wait());
-  }
-
   void DeleteElementValue(const ElementExpr& field) {
     std::string script = base::StringPrintf("%s.value = '';", field->c_str());
     ASSERT_TRUE(content::ExecJs(GetWebContents(), script));
@@ -1616,11 +1594,7 @@
   ASSERT_TRUE(FocusSelectOrSelectMenu("state", should_test_selectmenu,
                                       test->GetWebContents()));
   ASSERT_NE(kDefaultAddressValues.state_short, base::StringPiece("CA"));
-  if (should_test_selectmenu) {
-    test->FillSelectMenuElementWithValue("state", "CA", 1u);
-  } else {
-    test->FillElementWithValueAndBlur("state", "CA");
-  }
+  test->FillElementWithValueAndBlur("state", "CA");
 
   ASSERT_TRUE(AutofillFlow(GetElementById("firstname"), test));
   EXPECT_THAT(test->GetFormValuesIgnoringSelectMenuButtonSlot(),
diff --git a/third_party/blink/renderer/core/html/build.gni b/third_party/blink/renderer/core/html/build.gni
index 2afe354..8060255 100644
--- a/third_party/blink/renderer/core/html/build.gni
+++ b/third_party/blink/renderer/core/html/build.gni
@@ -681,6 +681,7 @@
   "forms/html_input_element_test.cc",
   "forms/html_output_element_test.cc",
   "forms/html_select_element_test.cc",
+  "forms/html_select_menu_element_test.cc",
   "forms/html_text_area_element_test.cc",
   "forms/internal_popup_menu_test.cc",
   "forms/option_list_test.cc",
diff --git a/third_party/blink/renderer/core/html/forms/date_time_symbolic_field_element.cc b/third_party/blink/renderer/core/html/forms/date_time_symbolic_field_element.cc
index c7fe51b..176262c8 100644
--- a/third_party/blink/renderer/core/html/forms/date_time_symbolic_field_element.cc
+++ b/third_party/blink/renderer/core/html/forms/date_time_symbolic_field_element.cc
@@ -85,9 +85,10 @@
 
   keyboard_event.SetDefaultHandled();
 
-  int index = type_ahead_.HandleEvent(
-      keyboard_event, TypeAhead::kMatchPrefix | TypeAhead::kCycleFirstChar |
-                          TypeAhead::kMatchIndex);
+  int index = type_ahead_.HandleEvent(keyboard_event, keyboard_event.charCode(),
+                                      TypeAhead::kMatchPrefix |
+                                          TypeAhead::kCycleFirstChar |
+                                          TypeAhead::kMatchIndex);
   if (index < 0)
     return;
   SetValueAsInteger(index, kDispatchEvent);
diff --git a/third_party/blink/renderer/core/html/forms/html_select_element.cc b/third_party/blink/renderer/core/html/forms/html_select_element.cc
index 91c2b40..9e0dc98 100644
--- a/third_party/blink/renderer/core/html/forms/html_select_element.cc
+++ b/third_party/blink/renderer/core/html/forms/html_select_element.cc
@@ -1148,7 +1148,8 @@
 
 void HTMLSelectElement::TypeAheadFind(const KeyboardEvent& event) {
   int index = type_ahead_.HandleEvent(
-      event, TypeAhead::kMatchPrefix | TypeAhead::kCycleFirstChar);
+      event, event.charCode(),
+      TypeAhead::kMatchPrefix | TypeAhead::kCycleFirstChar);
   if (index < 0)
     return;
   SelectOption(OptionAtListIndex(index), kDeselectOtherOptionsFlag |
diff --git a/third_party/blink/renderer/core/html/forms/html_select_menu_element.cc b/third_party/blink/renderer/core/html/forms/html_select_menu_element.cc
index 783db0ae..cbd8292 100644
--- a/third_party/blink/renderer/core/html/forms/html_select_menu_element.cc
+++ b/third_party/blink/renderer/core/html/forms/html_select_menu_element.cc
@@ -28,6 +28,7 @@
 #include "third_party/blink/renderer/platform/instrumentation/use_counter.h"
 #include "third_party/blink/renderer/platform/keyboard_codes.h"
 #include "third_party/blink/renderer/platform/text/platform_locale.h"
+#include "third_party/blink/renderer/platform/wtf/text/unicode.h"
 
 namespace blink {
 
@@ -183,7 +184,8 @@
 }
 
 HTMLSelectMenuElement::HTMLSelectMenuElement(Document& document)
-    : HTMLFormControlElementWithState(html_names::kSelectmenuTag, document) {
+    : HTMLFormControlElementWithState(html_names::kSelectmenuTag, document),
+      type_ahead_(this) {
   DCHECK(RuntimeEnabledFeatures::HTMLSelectMenuElementEnabled());
   DCHECK(RuntimeEnabledFeatures::RuntimeEnabledFeatures::
              HTMLPopoverAttributeEnabled(document.GetExecutionContext()));
@@ -225,15 +227,12 @@
   return PartType::kNone;
 }
 
-HTMLSelectMenuElement::ListItems HTMLSelectMenuElement::GetListItems() const {
-  ListItems list_items;
-  for (Node* node = SelectMenuPartTraversal::FirstChild(*this); node;
-       node = SelectMenuPartTraversal::Next(*node, this)) {
-    if (IsValidOptionPart(node, /*show_warning=*/false)) {
-      list_items.push_back(To<HTMLOptionElement>(node));
-    }
+const HTMLSelectMenuElement::ListItems& HTMLSelectMenuElement::GetListItems()
+    const {
+  if (should_recalc_list_items_) {
+    RecalcListItems();
   }
-  return list_items;
+  return list_items_;
 }
 
 void HTMLSelectMenuElement::DidAddUserAgentShadowRoot(ShadowRoot& root) {
@@ -251,10 +250,7 @@
   button_part_listener_ =
       MakeGarbageCollected<HTMLSelectMenuElement::ButtonPartEventListener>(
           this);
-  button_part_->addEventListener(event_type_names::kClick,
-                                 button_part_listener_, /*use_capture=*/false);
-  button_part_->addEventListener(event_type_names::kKeydown,
-                                 button_part_listener_, /*use_capture=*/false);
+  button_part_listener_->AddEventListeners(button_part_);
 
   selected_value_slot_ = MakeGarbageCollected<HTMLSlotElement>(document);
   selected_value_slot_->setAttribute(html_names::kNameAttr,
@@ -401,6 +397,28 @@
   }
 }
 
+bool HTMLSelectMenuElement::TypeAheadFind(const KeyboardEvent& event,
+                                          int charCode) {
+  if (event.ctrlKey() || event.altKey() || event.metaKey() ||
+      !WTF::unicode::IsPrintableChar(charCode)) {
+    return false;
+  }
+
+  int index = type_ahead_.HandleEvent(
+      event, charCode, TypeAhead::kMatchPrefix | TypeAhead::kCycleFirstChar);
+  if (index < 0) {
+    return false;
+  }
+
+  SetSelectedOption(OptionAtListIndex(index), /*send_events=*/true);
+  if (open() && selectedOption()) {
+    selectedOption()->Focus();
+  }
+
+  selected_option_->SetDirty(true);
+  return true;
+}
+
 void HTMLSelectMenuElement::ListboxWasClosed() {
   PseudoStateChanged(CSSSelector::kPseudoClosed);
   PseudoStateChanged(CSSSelector::kPseudoOpen);
@@ -412,6 +430,10 @@
   }
 }
 
+void HTMLSelectMenuElement::ResetTypeAheadSessionForTesting() {
+  type_ahead_.ResetSession();
+}
+
 bool HTMLSelectMenuElement::SetListboxPart(HTMLElement* new_listbox_part) {
   if (listbox_part_ == new_listbox_part)
     return false;
@@ -527,19 +549,11 @@
     return;
 
   if (button_part_) {
-    button_part_->removeEventListener(
-        event_type_names::kClick, button_part_listener_, /*use_capture=*/false);
-    button_part_->removeEventListener(event_type_names::kKeydown,
-                                      button_part_listener_,
-                                      /*use_capture=*/false);
+    button_part_listener_->RemoveEventListeners(button_part_);
   }
 
   if (new_button_part) {
-    new_button_part->addEventListener(
-        event_type_names::kClick, button_part_listener_, /*use_capture=*/false);
-    new_button_part->addEventListener(event_type_names::kKeydown,
-                                      button_part_listener_,
-                                      /*use_capture=*/false);
+    button_part_listener_->AddEventListeners(new_button_part);
   } else {
     QueueCheckForMissingParts();
   }
@@ -717,10 +731,7 @@
   }
 
   new_option_part->OptionInsertedIntoSelectMenuElement();
-  new_option_part->addEventListener(
-      event_type_names::kClick, option_part_listener_, /*use_capture=*/false);
-  new_option_part->addEventListener(
-      event_type_names::kKeydown, option_part_listener_, /*use_capture=*/false);
+  option_part_listener_->AddEventListeners(new_option_part);
 
   // TODO(crbug.com/1191131) The option part list should match the flat tree
   // order.
@@ -730,6 +741,7 @@
     SetSelectedOption(new_option_part);
   }
   SetNeedsValidityCheck();
+  should_recalc_list_items_ = true;
 }
 
 void HTMLSelectMenuElement::OptionPartRemoved(HTMLOptionElement* option_part) {
@@ -738,16 +750,14 @@
   }
 
   option_part->OptionRemovedFromSelectMenuElement();
-  option_part->removeEventListener(
-      event_type_names::kClick, option_part_listener_, /*use_capture=*/false);
-  option_part->removeEventListener(
-      event_type_names::kKeydown, option_part_listener_, /*use_capture=*/false);
+  option_part_listener_->RemoveEventListeners(option_part);
   option_parts_.erase(option_part);
 
   if (selected_option_ == option_part) {
     ResetToDefaultSelection();
   }
   SetNeedsValidityCheck();
+  should_recalc_list_items_ = true;
 }
 
 HTMLOptionElement* HTMLSelectMenuElement::FirstOptionPart() const {
@@ -804,6 +814,30 @@
   }
 }
 
+int HTMLSelectMenuElement::IndexOfSelectedOption() const {
+  int index = 0;
+  for (const auto& item : GetListItems()) {
+    auto* option_element = DynamicTo<HTMLOptionElement>(item.Get());
+    if (option_element && option_element->Selected()) {
+      return index;
+    }
+    ++index;
+  }
+  return -1;
+}
+
+int HTMLSelectMenuElement::OptionCount() const {
+  return GetListItems().size();
+}
+
+String HTMLSelectMenuElement::OptionAtIndex(int index) const {
+  HTMLOptionElement* option = OptionAtListIndex(index);
+  if (!option || option->IsDisabledFormControl()) {
+    return String();
+  }
+  return option->DisplayLabel();
+}
+
 HTMLOptionElement* HTMLSelectMenuElement::selectedOption() const {
   DCHECK(!selected_option_ ||
          IsValidOptionPart(selected_option_, /*show_warning=*/false));
@@ -902,37 +936,114 @@
   }
 }
 
+void HTMLSelectMenuElement::RecalcListItems() const {
+  list_items_.clear();
+  for (Node* node = SelectMenuPartTraversal::FirstChild(*this); node;
+       node = SelectMenuPartTraversal::Next(*node, this)) {
+    if (IsValidOptionPart(node, /*show_warning=*/false)) {
+      list_items_.push_back(To<HTMLOptionElement>(node));
+    }
+  }
+}
+
+HTMLOptionElement* HTMLSelectMenuElement::OptionAtListIndex(
+    int list_index) const {
+  if (list_index < 0) {
+    return nullptr;
+  }
+  const ListItems& items = GetListItems();
+  if (static_cast<wtf_size_t>(list_index) >= items.size()) {
+    return nullptr;
+  }
+
+  return DynamicTo<HTMLOptionElement>(items[list_index].Get());
+}
+
 void HTMLSelectMenuElement::ButtonPartEventListener::Invoke(ExecutionContext*,
                                                             Event* event) {
   if (event->defaultPrevented())
     return;
 
   if (event->type() == event_type_names::kClick &&
-      !select_menu_element_->open() &&
       !select_menu_element_->IsDisabledFormControl()) {
-    select_menu_element_->OpenListbox();
-  } else if (event->type() == event_type_names::kKeydown) {
-    bool handled = false;
-    auto* keyboard_event = DynamicTo<KeyboardEvent>(event);
-    if (!keyboard_event)
-      return;
-    switch (keyboard_event->keyCode()) {
-      case VKEY_RETURN:
-      case VKEY_SPACE:
-        if (!select_menu_element_->open() &&
-            !select_menu_element_->IsDisabledFormControl()) {
-          select_menu_element_->OpenListbox();
-        }
-        handled = true;
-        break;
+    if (!select_menu_element_->open()) {
+      select_menu_element_->OpenListbox();
     }
-    if (handled) {
-      event->stopPropagation();
+    // TODO(crbug.com/1408838) Close list box if dialog is open.
+  } else if (event->type() == event_type_names::kBlur) {
+    select_menu_element_->type_ahead_.ResetSession();
+  } else if (event->IsKeyboardEvent()) {
+    auto* keyboard_event = DynamicTo<KeyboardEvent>(event);
+    if (!keyboard_event) {
+      return;
+    }
+
+    if (!select_menu_element_->open() &&
+        !select_menu_element_->IsDisabledFormControl() &&
+        HandleKeyboardEvent(*keyboard_event)) {
       event->SetDefaultHandled();
     }
   }
 }
 
+void HTMLSelectMenuElement::ButtonPartEventListener::AddEventListeners(
+    HTMLElement* button_part) {
+  button_part->addEventListener(event_type_names::kClick, this,
+                                /*use_capture=*/false);
+  button_part->addEventListener(event_type_names::kBlur, this,
+                                /*use_capture=*/false);
+
+  // Observe keydown and keyup in order to override default HTMLButtonElement
+  // handling in HTMLElement::HandleKeyboardActivation() for VKEY_SPACE.
+  button_part->addEventListener(event_type_names::kKeydown, this,
+                                /*use_capture=*/false);
+  button_part->addEventListener(event_type_names::kKeyup, this,
+                                /*use_capture=*/false);
+  button_part->addEventListener(event_type_names::kKeypress, this,
+                                /*use_capture=*/false);
+}
+
+void HTMLSelectMenuElement::ButtonPartEventListener::RemoveEventListeners(
+    HTMLElement* button_part) {
+  button_part->removeEventListener(event_type_names::kClick, this,
+                                   /*use_capture=*/false);
+  button_part->removeEventListener(event_type_names::kBlur, this,
+                                   /*use_capture=*/false);
+  button_part->removeEventListener(event_type_names::kKeydown, this,
+                                   /*use_capture=*/false);
+  button_part->removeEventListener(event_type_names::kKeyup, this,
+                                   /*use_capture=*/false);
+  button_part->removeEventListener(event_type_names::kKeypress, this,
+                                   /*use_capture=*/false);
+}
+
+bool HTMLSelectMenuElement::ButtonPartEventListener::HandleKeyboardEvent(
+    const KeyboardEvent& event) {
+  if (event.keyCode() == VKEY_SPACE) {
+    if (event.type() == event_type_names::kKeydown) {
+      if (select_menu_element_->type_ahead_.HasActiveSession(event)) {
+        select_menu_element_->TypeAheadFind(event, ' ');
+      } else {
+        select_menu_element_->OpenListbox();
+      }
+    }
+    // Override default HTMLButtonElement handling in
+    // HTMLElement::HandleKeyboardActivation().
+    return true;
+  }
+  if (event.keyCode() == VKEY_RETURN &&
+      event.type() == event_type_names::kKeydown) {
+    // Handle <RETURN> because not all HTML elements synthesize a click when
+    // <RETURN> is pressed.
+    select_menu_element_->OpenListbox();
+    return true;
+  }
+  // Handled in event_type_names::kKeypress event handler because
+  // KeyboardEvent::charCode() == 0 for event_type_names::kKeydown.
+  return event.type() == event_type_names::kKeypress &&
+         select_menu_element_->TypeAheadFind(event, event.charCode());
+}
+
 void HTMLSelectMenuElement::OptionPartEventListener::Invoke(ExecutionContext*,
                                                             Event* event) {
   if (event->defaultPrevented())
@@ -948,15 +1059,42 @@
       select_menu_element_->DispatchInputEvent();
     }
     select_menu_element_->CloseListbox();
-  } else if (event->type() == event_type_names::kKeydown) {
-    bool handled = false;
+  } else if (event->IsKeyboardEvent()) {
     auto* keyboard_event = DynamicTo<KeyboardEvent>(event);
-    if (!keyboard_event)
-      return;
-    switch (keyboard_event->keyCode()) {
+    if (keyboard_event && HandleKeyboardEvent(*keyboard_event)) {
+      event->stopPropagation();
+      event->SetDefaultHandled();
+    }
+  }
+}
+
+void HTMLSelectMenuElement::OptionPartEventListener::AddEventListeners(
+    HTMLOptionElement* option_part) {
+  option_part->addEventListener(event_type_names::kClick, this,
+                                /*use_capture=*/false);
+  option_part->addEventListener(event_type_names::kKeydown, this,
+                                /*use_capture=*/false);
+  option_part->addEventListener(event_type_names::kKeypress, this,
+                                /*use_capture=*/false);
+}
+
+void HTMLSelectMenuElement::OptionPartEventListener::RemoveEventListeners(
+    HTMLOptionElement* option_part) {
+  option_part->removeEventListener(event_type_names::kClick, this,
+                                   /*use_capture=*/false);
+  option_part->removeEventListener(event_type_names::kKeydown, this,
+                                   /*use_capture=*/false);
+  option_part->removeEventListener(event_type_names::kKeypress, this,
+                                   /*use_capture=*/false);
+}
+
+bool HTMLSelectMenuElement::OptionPartEventListener::HandleKeyboardEvent(
+    const KeyboardEvent& event) {
+  if (event.type() == event_type_names::kKeydown) {
+    switch (event.keyCode()) {
       case VKEY_RETURN: {
         auto* target_element =
-            DynamicTo<HTMLOptionElement>(event->currentTarget()->ToNode());
+            DynamicTo<HTMLOptionElement>(event.currentTarget()->ToNode());
         DCHECK(target_element);
         DCHECK(select_menu_element_->option_parts_.Contains(target_element));
         if (target_element != select_menu_element_->selectedOption()) {
@@ -964,31 +1102,30 @@
           select_menu_element_->DispatchInputEvent();
         }
         select_menu_element_->CloseListbox();
-        handled = true;
-        break;
+        return true;
       }
       case VKEY_SPACE: {
+        select_menu_element_->TypeAheadFind(event, ' ');
         // Prevent the default behavior of scrolling the page on spacebar
         // that would cause the listbox to close.
-        handled = true;
-        break;
+        return true;
       }
       case VKEY_UP: {
         select_menu_element_->SelectPreviousOption();
-        handled = true;
-        break;
+        return true;
       }
       case VKEY_DOWN: {
         select_menu_element_->SelectNextOption();
-        handled = true;
-        break;
+        return true;
       }
     }
-    if (handled) {
-      event->stopPropagation();
-      event->SetDefaultHandled();
-    }
+  } else if (event.type() == event_type_names::kKeypress) {
+    // Handled in event_type_names::kKeypress event handler because
+    // KeyboardEvent::charCode() == 0 for event_type_names::kKeydown.
+    return select_menu_element_->TypeAheadFind(event, event.charCode());
   }
+
+  return false;
 }
 
 const AtomicString& HTMLSelectMenuElement::FormControlType() const {
@@ -1129,6 +1266,7 @@
   visitor->Trace(selected_value_slot_);
   visitor->Trace(selected_option_);
   visitor->Trace(selected_option_when_listbox_opened_);
+  visitor->Trace(list_items_);
   HTMLFormControlElementWithState::Trace(visitor);
 }
 
diff --git a/third_party/blink/renderer/core/html/forms/html_select_menu_element.h b/third_party/blink/renderer/core/html/forms/html_select_menu_element.h
index 1cbda0c..70ff1f50 100644
--- a/third_party/blink/renderer/core/html/forms/html_select_menu_element.h
+++ b/third_party/blink/renderer/core/html/forms/html_select_menu_element.h
@@ -10,6 +10,7 @@
 #include "third_party/blink/renderer/core/dom/events/native_event_listener.h"
 #include "third_party/blink/renderer/core/frame/local_frame_view.h"
 #include "third_party/blink/renderer/core/html/forms/html_form_control_element_with_state.h"
+#include "third_party/blink/renderer/core/html/forms/type_ahead.h"
 #include "third_party/blink/renderer/platform/wtf/vector.h"
 
 namespace blink {
@@ -26,7 +27,8 @@
 // for more details.
 class CORE_EXPORT HTMLSelectMenuElement final
     : public HTMLFormControlElementWithState,
-      public LocalFrameView::LifecycleNotificationObserver {
+      public LocalFrameView::LifecycleNotificationObserver,
+      public TypeAheadDataSource {
   DEFINE_WRAPPERTYPEINFO();
 
  public:
@@ -35,6 +37,11 @@
   // LocalFrameView::LifecycleNotificationObserver
   void DidFinishLifecycleUpdate(const LocalFrameView&) override;
 
+  // TypeAheadDataSource:
+  int IndexOfSelectedOption() const override;
+  int OptionCount() const override;
+  String OptionAtIndex(int index) const override;
+
   HTMLOptionElement* selectedOption() const;
   String value() const;
   void setValue(const String&,
@@ -78,14 +85,14 @@
 
   // Returns list of HTMLOptionElements which are direct children of the
   // HTMLSelectMenuElement.
-  // GetListItems() does not do any caching. Do not invoke this method from
-  // frequently called functions.
   // TODO(http://crbug.com/1422027): Expose iterator similar to
   // HTMLSelectElement::GetOptionList().
-  ListItems GetListItems() const;
+  const ListItems& GetListItems() const;
 
   void ListboxWasClosed();
 
+  void ResetTypeAheadSessionForTesting();
+
  private:
   class SelectMutationCallback;
 
@@ -93,6 +100,7 @@
   void DidMoveToNewDocument(Document& old_document) override;
   void OpenListbox();
   void CloseListbox();
+  bool TypeAheadFind(const KeyboardEvent& event, int charCode);
 
   HTMLOptionElement* FirstOptionPart() const;
   HTMLElement* FirstValidButtonPart() const;
@@ -108,6 +116,9 @@
   void SelectPreviousOption();
   void UpdateSelectedValuePartContents();
 
+  void RecalcListItems() const;
+  HTMLOptionElement* OptionAtListIndex(int list_index) const;
+
   void ButtonPartInserted(HTMLElement*);
   void ButtonPartRemoved(HTMLElement*);
   void UpdateButtonPart();
@@ -161,6 +172,10 @@
       NativeEventListener::Trace(visitor);
     }
 
+    void AddEventListeners(HTMLElement* button_part);
+    void RemoveEventListeners(HTMLElement* button_part);
+    bool HandleKeyboardEvent(const KeyboardEvent& event);
+
    private:
     Member<HTMLSelectMenuElement> select_menu_element_;
   };
@@ -176,6 +191,10 @@
       NativeEventListener::Trace(visitor);
     }
 
+    void AddEventListeners(HTMLOptionElement* option_part);
+    void RemoveEventListeners(HTMLOptionElement* option_part);
+    bool HandleKeyboardEvent(const KeyboardEvent& event);
+
    private:
     Member<HTMLSelectMenuElement> select_menu_element_;
   };
@@ -185,6 +204,8 @@
   static constexpr char kListboxPartName[] = "listbox";
   static constexpr char kMarkerPartName[] = "marker";
 
+  TypeAhead type_ahead_;
+
   Member<ButtonPartEventListener> button_part_listener_;
   Member<OptionPartEventListener> option_part_listener_;
 
@@ -201,6 +222,11 @@
   Member<HTMLOptionElement> selected_option_;
   Member<HTMLOptionElement> selected_option_when_listbox_opened_;
   bool queued_check_for_missing_parts_{false};
+
+  bool should_recalc_list_items_{true};
+
+  // Initialized lazily. Use GetListItems() to get up to date value.
+  mutable ListItems list_items_;
 };
 
 }  // namespace blink
diff --git a/third_party/blink/renderer/core/html/forms/html_select_menu_element_test.cc b/third_party/blink/renderer/core/html/forms/html_select_menu_element_test.cc
new file mode 100644
index 0000000..7ff997f
--- /dev/null
+++ b/third_party/blink/renderer/core/html/forms/html_select_menu_element_test.cc
@@ -0,0 +1,84 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/html/forms/html_select_menu_element.h"
+
+#include "base/run_loop.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/public/web/web_script_source.h"
+#include "third_party/blink/renderer/core/frame/local_frame.h"
+#include "third_party/blink/renderer/core/frame/settings.h"
+#include "third_party/blink/renderer/core/html/forms/html_option_element.h"
+#include "third_party/blink/renderer/core/script/classic_script.h"
+#include "third_party/blink/renderer/core/testing/page_test_base.h"
+
+namespace blink {
+namespace {
+
+void CheckOptions(HeapVector<Member<HTMLOptionElement>> options,
+                  const std::vector<std::string>& expected_option_values) {
+  ASSERT_EQ(expected_option_values.size(), options.size());
+  for (wtf_size_t i = 0; i < options.size(); ++i) {
+    EXPECT_EQ(options[i]->value().Utf8(), expected_option_values[i]);
+  }
+}
+
+}  // anonymous namespace
+
+class HTMLSelectMenuElementTest : public PageTestBase {
+ public:
+  void SetUp() override {
+    PageTestBase::SetUp();
+    GetDocument().SetMimeType("text/html");
+    GetFrame().GetSettings()->SetScriptEnabled(true);
+  }
+
+  void ExecuteJs(const std::string& js) {
+    ClassicScript::CreateUnspecifiedScript(WebString::FromUTF8(js))
+        ->RunScript(GetFrame().DomWindow());
+  }
+};
+
+// Test that SelectMenuElement::GetListItems() return value is updated upon
+// adding <option>s.
+TEST_F(HTMLSelectMenuElementTest, GetListItemsAdd) {
+  SetHtmlInnerHTML(R"HTML(
+    <selectmenu id='selectmenu'>
+    <option selected>Default</option>
+    </selectmenu>
+  )HTML");
+  HTMLSelectMenuElement* element =
+      To<HTMLSelectMenuElement>(GetElementById("selectmenu"));
+
+  CheckOptions(element->GetListItems(), {"Default"});
+
+  ExecuteJs(
+      "let selectmenu = document.getElementById('selectmenu');"
+      "let option = document.createElement('option');"
+      "option.textContent = 'New';"
+      "selectmenu.appendChild(option);");
+  CheckOptions(element->GetListItems(), {"Default", "New"});
+}
+
+// Test that SelectMenuElement::GetListItems() return value is updated upon
+// removing <option>.
+TEST_F(HTMLSelectMenuElementTest, GetListItemsRemove) {
+  SetHtmlInnerHTML(R"HTML(
+    <selectmenu id='selectmenu'>
+    <option selected>First</option>
+    <option id="second_option">Second</option>
+    </selectmenu>
+  )HTML");
+  HTMLSelectMenuElement* element =
+      To<HTMLSelectMenuElement>(GetElementById("selectmenu"));
+
+  CheckOptions(element->GetListItems(), {"First", "Second"});
+  ExecuteJs(
+      "let selectmenu = document.getElementById('selectmenu');"
+      "let second_option = document.getElementById('second_option');"
+      "selectmenu.removeChild(second_option);");
+  CheckOptions(element->GetListItems(), {"First"});
+}
+
+}  // namespace blink
diff --git a/third_party/blink/renderer/core/html/forms/type_ahead.cc b/third_party/blink/renderer/core/html/forms/type_ahead.cc
index f7b723a..412cb5a 100644
--- a/third_party/blink/renderer/core/html/forms/type_ahead.cc
+++ b/third_party/blink/renderer/core/html/forms/type_ahead.cc
@@ -53,6 +53,7 @@
 }
 
 int TypeAhead::HandleEvent(const KeyboardEvent& event,
+                           UChar charCode,
                            MatchModeFlags match_mode) {
   if (last_type_time_) {
     if (event.PlatformTimeStamp() < *last_type_time_)
@@ -68,8 +69,7 @@
   }
   last_type_time_ = event.PlatformTimeStamp();
 
-  UChar c = event.charCode();
-  buffer_.Append(c);
+  buffer_.Append(charCode);
 
   int option_count = data_source_->OptionCount();
   if (option_count < 1)
@@ -77,18 +77,18 @@
 
   int search_start_offset = 1;
   String prefix;
-  if (match_mode & kCycleFirstChar && c == repeating_char_) {
+  if (match_mode & kCycleFirstChar && charCode == repeating_char_) {
     // The user is likely trying to cycle through all the items starting
     // with this character, so just search on the character.
-    prefix = String(&c, 1u);
-    repeating_char_ = c;
+    prefix = String(&charCode, 1u);
+    repeating_char_ = charCode;
   } else if (match_mode & kMatchPrefix) {
     prefix = buffer_.ToString();
     if (buffer_.length() > 1) {
       repeating_char_ = 0;
       search_start_offset = 0;
     } else {
-      repeating_char_ = c;
+      repeating_char_ = charCode;
     }
   }
 
diff --git a/third_party/blink/renderer/core/html/forms/type_ahead.h b/third_party/blink/renderer/core/html/forms/type_ahead.h
index 78da97b..a07ab15f 100644
--- a/third_party/blink/renderer/core/html/forms/type_ahead.h
+++ b/third_party/blink/renderer/core/html/forms/type_ahead.h
@@ -60,7 +60,7 @@
   using MatchModeFlags = unsigned;
 
   // Returns the index for the matching option.
-  int HandleEvent(const KeyboardEvent&, MatchModeFlags);
+  int HandleEvent(const KeyboardEvent&, UChar charCode, MatchModeFlags);
   bool HasActiveSession(const KeyboardEvent&);
   void ResetSession();
 
diff --git a/third_party/blink/renderer/core/html/forms/type_ahead_test.cc b/third_party/blink/renderer/core/html/forms/type_ahead_test.cc
index 3b34efe..437e42d 100644
--- a/third_party/blink/renderer/core/html/forms/type_ahead_test.cc
+++ b/third_party/blink/renderer/core/html/forms/type_ahead_test.cc
@@ -62,7 +62,8 @@
     web_event.text[0] = ' ';
     auto& event = *KeyboardEvent::Create(web_event, nullptr);
     type_ahead_.HandleEvent(
-        event, TypeAhead::kMatchPrefix | TypeAhead::kCycleFirstChar);
+        event, event.charCode(),
+        TypeAhead::kMatchPrefix | TypeAhead::kCycleFirstChar);
 
     // A session should now be in progress.
     EXPECT_TRUE(type_ahead_.HasActiveSession(event));
@@ -92,7 +93,7 @@
                              base::TimeTicks() + base::Milliseconds(500));
   web_event.text[0] = ' ';
   auto& event = *KeyboardEvent::Create(web_event, nullptr);
-  type_ahead_.HandleEvent(event,
+  type_ahead_.HandleEvent(event, event.charCode(),
                           TypeAhead::kMatchPrefix | TypeAhead::kCycleFirstChar);
 
   // A session should now be in progress.
diff --git a/third_party/blink/renderer/core/testing/internals.cc b/third_party/blink/renderer/core/testing/internals.cc
index a765f023..597384f6 100644
--- a/third_party/blink/renderer/core/testing/internals.cc
+++ b/third_party/blink/renderer/core/testing/internals.cc
@@ -105,6 +105,7 @@
 #include "third_party/blink/renderer/core/html/forms/form_controller.h"
 #include "third_party/blink/renderer/core/html/forms/html_input_element.h"
 #include "third_party/blink/renderer/core/html/forms/html_select_element.h"
+#include "third_party/blink/renderer/core/html/forms/html_select_menu_element.h"
 #include "third_party/blink/renderer/core/html/forms/html_text_area_element.h"
 #include "third_party/blink/renderer/core/html/forms/text_control_inner_elements.h"
 #include "third_party/blink/renderer/core/html/html_iframe_element.h"
@@ -3378,6 +3379,12 @@
   select->ResetTypeAheadSessionForTesting();
 }
 
+void Internals::resetSelectMenuTypeAheadSession(
+    HTMLSelectMenuElement* selectmenu) {
+  DCHECK(selectmenu);
+  selectmenu->ResetTypeAheadSessionForTesting();
+}
+
 void Internals::forceCompositingUpdate(Document* document,
                                        ExceptionState& exception_state) {
   DCHECK(document);
diff --git a/third_party/blink/renderer/core/testing/internals.h b/third_party/blink/renderer/core/testing/internals.h
index 537db526..3691060 100644
--- a/third_party/blink/renderer/core/testing/internals.h
+++ b/third_party/blink/renderer/core/testing/internals.h
@@ -60,6 +60,7 @@
 class HTMLInputElement;
 class HTMLMediaElement;
 class HTMLSelectElement;
+class HTMLSelectMenuElement;
 class HTMLVideoElement;
 class HitTestLayerRectList;
 class HitTestLocation;
@@ -465,6 +466,8 @@
   int selectPopupItemStyleFontHeight(Node*, int);
   void resetTypeAheadSession(HTMLSelectElement*);
 
+  void resetSelectMenuTypeAheadSession(HTMLSelectMenuElement*);
+
   StaticSelection* getDragCaret();
   StaticSelection* getSelectionInFlatTree(DOMWindow*, ExceptionState&);
   Node* visibleSelectionAnchorNode();
diff --git a/third_party/blink/renderer/core/testing/internals.idl b/third_party/blink/renderer/core/testing/internals.idl
index 411d1e13..893cf91 100644
--- a/third_party/blink/renderer/core/testing/internals.idl
+++ b/third_party/blink/renderer/core/testing/internals.idl
@@ -284,6 +284,8 @@
     long selectPopupItemStyleFontHeight(Node select, long itemIndex);
     void resetTypeAheadSession(HTMLSelectElement select);
 
+    void resetSelectMenuTypeAheadSession(HTMLSelectMenuElement selectmenu);
+
     StaticSelection getDragCaret();
     [RaisesException] StaticSelection getSelectionInFlatTree(Window window);
     // TODO(editing-dev): We should change |visibleSelection*| to
diff --git a/third_party/blink/web_tests/fast/forms/select/select-typeahead-with-spacekey-expected.txt b/third_party/blink/web_tests/fast/forms/select/select-typeahead-with-spacekey-expected.txt
index 7ae6ed0103..9a5c1e0 100644
--- a/third_party/blink/web_tests/fast/forms/select/select-typeahead-with-spacekey-expected.txt
+++ b/third_party/blink/web_tests/fast/forms/select/select-typeahead-with-spacekey-expected.txt
@@ -10,12 +10,12 @@
 PASS popup.value is "Spain"
 3. space key to open popup menu.
 PASS internals.isSelectPopupVisible(popup) is true
-PASS popup.value is "United Arab Emerites"
+PASS popup.value is "United Arab Emirates"
 PASS successfullyParsed is true
 
 TEST COMPLETE
 
 Canada
 Spain
-United Arab Emerites
+United Arab Emirates
 United States
diff --git a/third_party/blink/web_tests/fast/forms/select/select-typeahead-with-spacekey.html b/third_party/blink/web_tests/fast/forms/select/select-typeahead-with-spacekey.html
index 9af9e49..add399d 100644
--- a/third_party/blink/web_tests/fast/forms/select/select-typeahead-with-spacekey.html
+++ b/third_party/blink/web_tests/fast/forms/select/select-typeahead-with-spacekey.html
@@ -7,7 +7,7 @@
 <select id="select">
     <option>Canada</option>
     <option>Spain</option>
-    <option>United Arab Emerites</option>
+    <option>United Arab Emirates</option>
     <option>United States</option>
 </select>
 <script>
@@ -68,7 +68,7 @@
     internals.resetTypeAheadSession(popup);
     keyDown(' ');
     shouldBeTrue('internals.isSelectPopupVisible(popup)');
-    shouldBeEqualToString('popup.value', 'United Arab Emerites');
+    shouldBeEqualToString('popup.value', 'United Arab Emirates');
     popup.blur();
     finishJSTest();
 }
diff --git a/third_party/blink/web_tests/html/selectmenu/selectmenu-typeahead-cyrillic.html b/third_party/blink/web_tests/html/selectmenu/selectmenu-typeahead-cyrillic.html
new file mode 100644
index 0000000..c85b304
--- /dev/null
+++ b/third_party/blink/web_tests/html/selectmenu/selectmenu-typeahead-cyrillic.html
@@ -0,0 +1,68 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <title></title>
+    <script src="../../resources/testharness.js"></script>
+    <script src="../../resources/testharnessreport.js"></script>
+</head>
+<body>
+    <p>This test verifies a selectmenu can refine the selection when we send keydown events consisting of Cyrillic characters.</p>
+    <selectmenu id="selectmenu">
+       <button type="button" slot="button" behavior="button"
+               id="selectmenu-button">
+          Button
+        </button>
+        <option value="-1">should not see me</option>
+        <option value="0">&#x0410;</option>
+        <option value="1">&#x0410;&#x0411;</option>
+        <option value="2">&#x0410;&#x0411;&#x0412;</option>
+        <option value="3">&#x0410;&#x0411;&#x0412;&#x0413;</option>
+        <option value="4">&#x0410;&#x0411;&#x0412;&#x0413;&#x0414;</option>
+        <option value="5">&#x0410;&#x0411;&#x0412;&#x0413;&#x0414;&#x0415;</option>
+        <option value="6">&#x0410;&#x0411;&#x0412;&#x0413;&#x0414;&#x0415;&#x0416;</option>
+        <option value="7">&#x0410;&#x0411;&#x0412;&#x0413;&#x0414;&#x0415;&#x0416;&#x0417;</option>
+        <option value="8">&#x0410;&#x0411;&#x0412;&#x0413;&#x0414;&#x0415;&#x0416;&#x0417;&#x0418;</option>
+        <option value="9">&#x0410;&#x0411;&#x0412;&#x0413;&#x0414;&#x0415;&#x0416;&#x0417;&#x0418;&#x0419;</option>
+        <option value="10">&#x0410;&#x0411;&#x0412;&#x0413;&#x0414;&#x0415;&#x0416;&#x0417;&#x0418;&#x0419;&#x041A;</option>
+        <option value="11">&#x0410;&#x0411;&#x0412;&#x0413;&#x0414;&#x0415;&#x0416;&#x0417;&#x0418;&#x0419;&#x041A;&#x041B;</option>
+        <option value="12">&#x0410;&#x0411;&#x0412;&#x0413;&#x0414;&#x0415;&#x0416;&#x0417;&#x0418;&#x0419;&#x041A;&#x041B;&#x041C;</option>
+        <option value="13">&#x0410;&#x0411;&#x0412;&#x0413;&#x0414;&#x0415;&#x0416;&#x0417;&#x0418;&#x0419;&#x041A;&#x041B;&#x041C;&#x041D;</option>
+        <option value="14">&#x0410;&#x0411;&#x0412;&#x0413;&#x0414;&#x0415;&#x0416;&#x0417;&#x0418;&#x0419;&#x041A;&#x041B;&#x041C;&#x041D;&#x041E;</option>
+        <option value="15">&#x0410;&#x0411;&#x0412;&#x0413;&#x0414;&#x0415;&#x0416;&#x0417;&#x0418;&#x0419;&#x041A;&#x041B;&#x041C;&#x041D;&#x041E;&#x041F;</option>
+        <option value="16">&#x0410;&#x0411;&#x0412;&#x0413;&#x0414;&#x0415;&#x0416;&#x0417;&#x0418;&#x0419;&#x041A;&#x041B;&#x041C;&#x041D;&#x041E;&#x041F;&#x0420;</option>
+        <option value="17">&#x0410;&#x0411;&#x0412;&#x0413;&#x0414;&#x0415;&#x0416;&#x0417;&#x0418;&#x0419;&#x041A;&#x041B;&#x041C;&#x041D;&#x041E;&#x041F;&#x0420;&#x0421;</option>
+        <option value="18">&#x0410;&#x0411;&#x0412;&#x0413;&#x0414;&#x0415;&#x0416;&#x0417;&#x0418;&#x0419;&#x041A;&#x041B;&#x041C;&#x041D;&#x041E;&#x041F;&#x0420;&#x0421;&#x0422;</option>
+        <option value="19">&#x0410;&#x0411;&#x0412;&#x0413;&#x0414;&#x0415;&#x0416;&#x0417;&#x0418;&#x0419;&#x041A;&#x041B;&#x041C;&#x041D;&#x041E;&#x041F;&#x0420;&#x0421;&#x0422;&#x0423;</option>
+        <option value="20">&#x0410;&#x0411;&#x0412;&#x0413;&#x0414;&#x0415;&#x0416;&#x0417;&#x0418;&#x0419;&#x041A;&#x041B;&#x041C;&#x041D;&#x041E;&#x041F;&#x0420;&#x0421;&#x0422;&#x0423;&#x0424;</option>
+        <option value="21">&#x0410;&#x0411;&#x0412;&#x0413;&#x0414;&#x0415;&#x0416;&#x0417;&#x0418;&#x0419;&#x041A;&#x041B;&#x041C;&#x041D;&#x041E;&#x041F;&#x0420;&#x0421;&#x0422;&#x0423;&#x0424;&#x0425;</option>
+        <option value="22">&#x0410;&#x0411;&#x0412;&#x0413;&#x0414;&#x0415;&#x0416;&#x0417;&#x0418;&#x0419;&#x041A;&#x041B;&#x041C;&#x041D;&#x041E;&#x041F;&#x0420;&#x0421;&#x0422;&#x0423;&#x0424;&#x0425;&#x0426;</option>
+        <option value="23">&#x0410;&#x0411;&#x0412;&#x0413;&#x0414;&#x0415;&#x0416;&#x0417;&#x0418;&#x0419;&#x041A;&#x041B;&#x041C;&#x041D;&#x041E;&#x041F;&#x0420;&#x0421;&#x0422;&#x0423;&#x0424;&#x0425;&#x0426;&#x0427;</option>
+        <option value="24">&#x0410;&#x0411;&#x0412;&#x0413;&#x0414;&#x0415;&#x0416;&#x0417;&#x0418;&#x0419;&#x041A;&#x041B;&#x041C;&#x041D;&#x041E;&#x041F;&#x0420;&#x0421;&#x0422;&#x0423;&#x0424;&#x0425;&#x0426;&#x0427;&#x0428;</option>
+        <option value="25">&#x0410;&#x0411;&#x0412;&#x0413;&#x0414;&#x0415;&#x0416;&#x0417;&#x0418;&#x0419;&#x041A;&#x041B;&#x041C;&#x041D;&#x041E;&#x041F;&#x0420;&#x0421;&#x0422;&#x0423;&#x0424;&#x0425;&#x0426;&#x0427;&#x0428;&#x0429;</option>
+        <option value="26">&#x0410;&#x0411;&#x0412;&#x0413;&#x0414;&#x0415;&#x0416;&#x0417;&#x0418;&#x0419;&#x041A;&#x041B;&#x041C;&#x041D;&#x041E;&#x041F;&#x0420;&#x0421;&#x0422;&#x0423;&#x0424;&#x0425;&#x0426;&#x0427;&#x0428;&#x0429;&#x042A;</option>
+        <option value="27">&#x0410;&#x0411;&#x0412;&#x0413;&#x0414;&#x0415;&#x0416;&#x0417;&#x0418;&#x0419;&#x041A;&#x041B;&#x041C;&#x041D;&#x041E;&#x041F;&#x0420;&#x0421;&#x0422;&#x0423;&#x0424;&#x0425;&#x0426;&#x0427;&#x0428;&#x0429;&#x042A;&#x042B;</option>
+        <option value="28">&#x0410;&#x0411;&#x0412;&#x0413;&#x0414;&#x0415;&#x0416;&#x0417;&#x0418;&#x0419;&#x041A;&#x041B;&#x041C;&#x041D;&#x041E;&#x041F;&#x0420;&#x0421;&#x0422;&#x0423;&#x0424;&#x0425;&#x0426;&#x0427;&#x0428;&#x0429;&#x042A;&#x042B;&#x042C;</option>
+        <option value="29">&#x0410;&#x0411;&#x0412;&#x0413;&#x0414;&#x0415;&#x0416;&#x0417;&#x0418;&#x0419;&#x041A;&#x041B;&#x041C;&#x041D;&#x041E;&#x041F;&#x0420;&#x0421;&#x0422;&#x0423;&#x0424;&#x0425;&#x0426;&#x0427;&#x0428;&#x0429;&#x042A;&#x042B;&#x042C;&#x042D;</option>
+        <option value="30">&#x0410;&#x0411;&#x0412;&#x0413;&#x0414;&#x0415;&#x0416;&#x0417;&#x0418;&#x0419;&#x041A;&#x041B;&#x041C;&#x041D;&#x041E;&#x041F;&#x0420;&#x0421;&#x0422;&#x0423;&#x0424;&#x0425;&#x0426;&#x0427;&#x0428;&#x0429;&#x042A;&#x042B;&#x042C;&#x042D;&#x042E;</option>
+        <option value="31">&#x0410;&#x0411;&#x0412;&#x0413;&#x0414;&#x0415;&#x0416;&#x0417;&#x0418;&#x0419;&#x041A;&#x041B;&#x041C;&#x041D;&#x041E;&#x041F;&#x0420;&#x0421;&#x0422;&#x0423;&#x0424;&#x0425;&#x0426;&#x0427;&#x0428;&#x0429;&#x042A;&#x042B;&#x042C;&#x042D;&#x042E;&#x042F;</option>
+    </selectmenu>
+    <ul id="console"></ul>
+</body>
+<script>
+// Set the input focus to the <selectmenu> element.
+var selectmenu = document.getElementById("selectmenu");
+var button = document.getElementById("selectmenu-button");
+button.focus();
+
+var base = 0x0430;
+for (var i = base; i <= 0x044F; i++) {
+  test(() => {
+    // Send a key event consisting of a Cyrillic small character.
+    eventSender.keyDown(String.fromCharCode(i));
+
+    // Compare the value of this <selectmenu> element with the expected result.
+    assert_equals(selectmenu.value, (i - base).toString());
+  });
+}
+</script>
+</html>
diff --git a/third_party/blink/web_tests/html/selectmenu/selectmenu-typeahead-disabled-option-expected.txt b/third_party/blink/web_tests/html/selectmenu/selectmenu-typeahead-disabled-option-expected.txt
new file mode 100644
index 0000000..5304b39
--- /dev/null
+++ b/third_party/blink/web_tests/html/selectmenu/selectmenu-typeahead-disabled-option-expected.txt
@@ -0,0 +1,11 @@
+Typeahead does not select disabled selectmenu option
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+PASS selectmenu.open is false
+PASS selectmenu.value is "United States"
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
+Button
diff --git a/third_party/blink/web_tests/html/selectmenu/selectmenu-typeahead-disabled-option.html b/third_party/blink/web_tests/html/selectmenu/selectmenu-typeahead-disabled-option.html
new file mode 100644
index 0000000..aeb289a
--- /dev/null
+++ b/third_party/blink/web_tests/html/selectmenu/selectmenu-typeahead-disabled-option.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script src="../../resources/js-test.js"></script>
+</head>
+<body>
+<selectmenu id="selectmenu" tabindex="0">
+     <button type="button" slot="button" behavior="button"
+             id="selectmenu-button">
+       Button
+     </button>
+    <option>Canada</option>
+    <option>Spain</option>
+    <option disabled>United Arab Emirates</option>
+    <option>United States</option>
+</selectmenu>
+<script>
+description('Typeahead does not select disabled selectmenu option');
+
+window.jsTestIsAsync = true;
+
+function keyDown(key)
+{
+    if (!window.eventSender)
+      debug("No event sender");
+    eventSender.keyDown(key);
+}
+
+var selectmenu = document.getElementById("selectmenu");
+var button = document.getElementById("selectmenu-button");
+
+button.focus();
+selectmenu.value = "Canada";
+// Should skip disabled "United Arab Emirates" <option>
+keyDown('U');
+shouldBeFalse('selectmenu.open');
+shouldBeEqualToString('selectmenu.value', 'United States');
+finishJSTest();
+</script>
+</body>
diff --git a/third_party/blink/web_tests/html/selectmenu/selectmenu-typeahead-empty-expected.txt b/third_party/blink/web_tests/html/selectmenu/selectmenu-typeahead-empty-expected.txt
new file mode 100644
index 0000000..ce1bf4a5
--- /dev/null
+++ b/third_party/blink/web_tests/html/selectmenu/selectmenu-typeahead-empty-expected.txt
@@ -0,0 +1,4 @@
+Check that selectmenu does not crash on search when it's empty.
+
+Button
+Cannot run interactively.
diff --git a/third_party/blink/web_tests/html/selectmenu/selectmenu-typeahead-empty.html b/third_party/blink/web_tests/html/selectmenu/selectmenu-typeahead-empty.html
new file mode 100644
index 0000000..44fde4ce
--- /dev/null
+++ b/third_party/blink/web_tests/html/selectmenu/selectmenu-typeahead-empty.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<p>
+  Check that selectmenu does not crash on search when it's empty.
+</p>
+<selectmenu>
+ <button type="button" slot="button" behavior="button"
+         id="selectmenu-button">
+   Button
+ </button>
+</selectmenu>
+<div id="result">Cannot run interactively.</div>
+<script>
+    if (window.testRunner) {
+        testRunner.dumpAsText();
+        var button = document.getElementById("selectmenu-button");
+        button.focus();
+        eventSender.keyDown("z");
+        eventSender.keyDown("z");
+        eventSender.keyDown("z");
+        eventSender.keyDown("z");
+        eventSender.keyDown("z");
+        eventSender.keyDown("z");
+    }
+</script>
diff --git a/third_party/blink/web_tests/html/selectmenu/selectmenu-typeahead-find-expected.txt b/third_party/blink/web_tests/html/selectmenu/selectmenu-typeahead-find-expected.txt
new file mode 100644
index 0000000..4f307e1
--- /dev/null
+++ b/third_party/blink/web_tests/html/selectmenu/selectmenu-typeahead-find-expected.txt
@@ -0,0 +1,7 @@
+Verify type ahead selection fires onchange event.
+Set focus to selectmenu element
+Type "c"
+You see "cherry" in selectmenu element and "PASS" below selectmenu element.
+cherry
+
+PASS
diff --git a/third_party/blink/web_tests/html/selectmenu/selectmenu-typeahead-find.html b/third_party/blink/web_tests/html/selectmenu/selectmenu-typeahead-find.html
new file mode 100644
index 0000000..18b3f65
--- /dev/null
+++ b/third_party/blink/web_tests/html/selectmenu/selectmenu-typeahead-find.html
@@ -0,0 +1,54 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script>
+function recordIt()
+{
+    var selectmenu = document.getElementById("selectmenu");
+    var selectmenuButton = document.getElementById("selectmenu-button");
+    selectmenuButton.innerText = selectmenu.value;
+    var res = document.getElementById("res");
+    if (res.innerText != "FAIL")
+        res.innerText = "PASS";
+}
+
+function testIt(ch, expectedValue)
+{
+    var selectmenu = document.getElementById("selectmenu");
+    var selectmenuButton = document.getElementById("selectmenu-button");
+    selectmenuButton.focus();
+    eventSender.keyDown(ch);
+    if (selectmenu.value != expectedValue)
+        document.getElementById("res").innerText = "FAIL";
+}
+
+function test()
+{
+    if (!window.testRunner)
+        return;
+
+    testRunner.dumpAsText();
+    testIt("c", "cherry");
+}
+</script>
+</head>
+<body onload="test()">
+Verify type ahead selection fires onchange event.
+<ol>
+<li>Set focus to selectmenu element</li>
+<li>Type "c"</li>
+<li>You see "cherry" in selectmenu element and "PASS" below selectmenu element.</li>
+</ol>
+<selectmenu id="selectmenu" onchange="recordIt()">
+    <button type="button" slot="button" behavior="button"
+            id="selectmenu-button">
+        Button
+    </button>
+
+    <option>apple</option>
+    <option>banana</option>
+    <option>cherry</option>
+</selectmenu><br />
+<div id="res"></div>
+</html>
+
diff --git a/third_party/blink/web_tests/html/selectmenu/selectmenu-typeahead-greek.html b/third_party/blink/web_tests/html/selectmenu/selectmenu-typeahead-greek.html
new file mode 100644
index 0000000..f8668d23
--- /dev/null
+++ b/third_party/blink/web_tests/html/selectmenu/selectmenu-typeahead-greek.html
@@ -0,0 +1,63 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <title></title>
+    <script src="../../resources/testharness.js"></script>
+    <script src="../../resources/testharnessreport.js"></script>
+</head>
+<body>
+    <p>This test verifies a selectmenu can refine the selection when we send keydown events consisting of Greek small characters.</p>
+    <selectmenu id="selectmenu">
+       <button type="button" slot="button" behavior="button"
+               id="selectmenu-button">
+          Button
+        </button>
+        <option value="-1">should not see me</option>
+        <option value="0">&#x0391;</option>
+        <option value="1">&#x0391;&#x0392;</option>
+        <option value="2">&#x0391;&#x0392;&#x0393;</option>
+        <option value="3">&#x0391;&#x0392;&#x0393;&#x0394;</option>
+        <option value="4">&#x0391;&#x0392;&#x0393;&#x0394;&#x0395;</option>
+        <option value="5">&#x0391;&#x0392;&#x0393;&#x0394;&#x0395;&#x0396;</option>
+        <option value="6">&#x0391;&#x0392;&#x0393;&#x0394;&#x0395;&#x0396;&#x0397;</option>
+        <option value="7">&#x0391;&#x0392;&#x0393;&#x0394;&#x0395;&#x0396;&#x0397;&#x0398;</option>
+        <option value="8">&#x0391;&#x0392;&#x0393;&#x0394;&#x0395;&#x0396;&#x0397;&#x0398;&#x0399;</option>
+        <option value="9">&#x0391;&#x0392;&#x0393;&#x0394;&#x0395;&#x0396;&#x0397;&#x0398;&#x0399;&#x039A;</option>
+        <option value="10">&#x0391;&#x0392;&#x0393;&#x0394;&#x0395;&#x0396;&#x0397;&#x0398;&#x0399;&#x039A;&#x039B;</option>
+        <option value="11">&#x0391;&#x0392;&#x0393;&#x0394;&#x0395;&#x0396;&#x0397;&#x0398;&#x0399;&#x039A;&#x039B;&#x039C;</option>
+        <option value="12">&#x0391;&#x0392;&#x0393;&#x0394;&#x0395;&#x0396;&#x0397;&#x0398;&#x0399;&#x039A;&#x039B;&#x039C;&#x039D;</option>
+        <option value="13">&#x0391;&#x0392;&#x0393;&#x0394;&#x0395;&#x0396;&#x0397;&#x0398;&#x0399;&#x039A;&#x039B;&#x039C;&#x039D;&#x039E;</option>
+        <option value="14">&#x0391;&#x0392;&#x0393;&#x0394;&#x0395;&#x0396;&#x0397;&#x0398;&#x0399;&#x039A;&#x039B;&#x039C;&#x039D;&#x039E;&#x039F;</option>
+        <option value="15">&#x0391;&#x0392;&#x0393;&#x0394;&#x0395;&#x0396;&#x0397;&#x0398;&#x0399;&#x039A;&#x039B;&#x039C;&#x039D;&#x039E;&#x039F;&#x03A0;</option>
+        <option value="16">&#x0391;&#x0392;&#x0393;&#x0394;&#x0395;&#x0396;&#x0397;&#x0398;&#x0399;&#x039A;&#x039B;&#x039C;&#x039D;&#x039E;&#x039F;&#x03A0;&#x03A1;</option>
+        <option value="18">&#x0391;&#x0392;&#x0393;&#x0394;&#x0395;&#x0396;&#x0397;&#x0398;&#x0399;&#x039A;&#x039B;&#x039C;&#x039D;&#x039E;&#x039F;&#x03A0;&#x03A1;&#x03A3;</option>
+        <option value="19">&#x0391;&#x0392;&#x0393;&#x0394;&#x0395;&#x0396;&#x0397;&#x0398;&#x0399;&#x039A;&#x039B;&#x039C;&#x039D;&#x039E;&#x039F;&#x03A0;&#x03A1;&#x03A3;&#x03A4;</option>
+        <option value="20">&#x0391;&#x0392;&#x0393;&#x0394;&#x0395;&#x0396;&#x0397;&#x0398;&#x0399;&#x039A;&#x039B;&#x039C;&#x039D;&#x039E;&#x039F;&#x03A0;&#x03A1;&#x03A3;&#x03A4;&#x03A5;</option>
+        <option value="21">&#x0391;&#x0392;&#x0393;&#x0394;&#x0395;&#x0396;&#x0397;&#x0398;&#x0399;&#x039A;&#x039B;&#x039C;&#x039D;&#x039E;&#x039F;&#x03A0;&#x03A1;&#x03A3;&#x03A4;&#x03A5;&#x03A6;</option>
+        <option value="22">&#x0391;&#x0392;&#x0393;&#x0394;&#x0395;&#x0396;&#x0397;&#x0398;&#x0399;&#x039A;&#x039B;&#x039C;&#x039D;&#x039E;&#x039F;&#x03A0;&#x03A1;&#x03A3;&#x03A4;&#x03A5;&#x03A6;&#x03A7;</option>
+        <option value="23">&#x0391;&#x0392;&#x0393;&#x0394;&#x0395;&#x0396;&#x0397;&#x0398;&#x0399;&#x039A;&#x039B;&#x039C;&#x039D;&#x039E;&#x039F;&#x03A0;&#x03A1;&#x03A3;&#x03A4;&#x03A5;&#x03A6;&#x03A7;&#x03A8;</option>
+        <option value="24">&#x0391;&#x0392;&#x0393;&#x0394;&#x0395;&#x0396;&#x0397;&#x0398;&#x0399;&#x039A;&#x039B;&#x039C;&#x039D;&#x039E;&#x039F;&#x03A0;&#x03A1;&#x03A3;&#x03A4;&#x03A5;&#x03A6;&#x03A7;&#x03A8;&#x03A9;</option>
+    </selectmenu>
+    <ul id="console"></ul>
+</body>
+<script>
+// Set the input focus to the <select> element.
+var selectmenu = document.getElementById("selectmenu");
+var button = document.getElementById("selectmenu-button");
+button.focus();
+
+var base = 0x03B1;
+for (var i = base; i <= 0x03C9; i++) {
+  test(() => {
+    // We don't have to send U+03C2 (Greek Small Letter Final Sigma).
+    if (i != 0x03C2) {
+        // Send a key event consisting of a Greek small character.
+        eventSender.keyDown(String.fromCharCode(i));
+
+        // Compare the value of this <selectmenu> element with the expected result.
+        assert_equals(selectmenu.value, (i - base).toString());
+    }
+  });
+}
+</script>
+</html>
diff --git a/third_party/blink/web_tests/html/selectmenu/selectmenu-typeahead-non-latin-expected.txt b/third_party/blink/web_tests/html/selectmenu/selectmenu-typeahead-non-latin-expected.txt
new file mode 100644
index 0000000..81dd7b8
--- /dev/null
+++ b/third_party/blink/web_tests/html/selectmenu/selectmenu-typeahead-non-latin-expected.txt
@@ -0,0 +1,4 @@
+Check that type-to-select in unopened popups works with Hebrew.
+
+Button
+SUCCESS
diff --git a/third_party/blink/web_tests/html/selectmenu/selectmenu-typeahead-non-latin.html b/third_party/blink/web_tests/html/selectmenu/selectmenu-typeahead-non-latin.html
new file mode 100644
index 0000000..16b3138
--- /dev/null
+++ b/third_party/blink/web_tests/html/selectmenu/selectmenu-typeahead-non-latin.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<p>
+    Check that type-to-select in unopened popups works with Hebrew</i>.
+</p>
+<selectmenu id="selectmenu">
+    <button type="button" slot="button" behavior="button"
+            id="selectmenu-button">
+        Button
+    </button>
+    <option>&#x05e9;</option>
+    <option>&#x05d3;</option>
+    <option>&#x05d2;</option>
+    <option>&#x05db;</option>
+    <option>&#x05e2;</option>
+</selectmenu>
+<div id="result">Cannot run interactively.</div>
+<script>
+    if (window.testRunner) {
+        testRunner.dumpAsText();
+        var selectmenu = document.getElementById("selectmenu");
+        var button = document.getElementById("selectmenu-button");
+        button.focus();
+        eventSender.keyDown("\u05d2");
+        var result = document.getElementById("result");
+        result.innerText = selectmenu.value == "\u05d2" ? "SUCCESS" : "FAIL";
+    }
+</script>
diff --git a/third_party/blink/web_tests/html/selectmenu/selectmenu-typeahead-scroll-expected.txt b/third_party/blink/web_tests/html/selectmenu/selectmenu-typeahead-scroll-expected.txt
new file mode 100644
index 0000000..f748317
--- /dev/null
+++ b/third_party/blink/web_tests/html/selectmenu/selectmenu-typeahead-scroll-expected.txt
@@ -0,0 +1,15 @@
+PASS selectmenu.open is true
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
+
+Test that selection box scrolls to where the focus jumps when pressing an alphanumeric key.
+
+Button
+aardvark
+blue
+navy
+indigo
+azure
+SUCCESS
diff --git a/third_party/blink/web_tests/html/selectmenu/selectmenu-typeahead-scroll.html b/third_party/blink/web_tests/html/selectmenu/selectmenu-typeahead-scroll.html
new file mode 100644
index 0000000..20165c17
--- /dev/null
+++ b/third_party/blink/web_tests/html/selectmenu/selectmenu-typeahead-scroll.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script src="../../resources/js-test.js"></script>
+</head>
+<body>
+<p>
+  Test that selection box scrolls to where the focus jumps when pressing an alphanumeric key.
+</p>
+<selectmenu id="selectmenu">
+  <button type="button" slot="button" behavior="button"
+          id="selectmenu-button">
+    Button
+  </button>
+  <div popover slot="listbox" behavior="listbox">
+    <div style="max-height:10vh;overflow:auto" id="selectmenu-listbox-scroll">
+      <option>aardvark</option>
+      <option>blue</option>
+      <option>navy</option>
+      <option>indigo</option>
+      <option>azure</option>
+    </div>
+  </div>
+</selectmenu>
+<div id="result">Cannot run interactively.</div>
+<script>
+    if (window.testRunner) {
+        testRunner.dumpAsText();
+        testRunner.waitUntilDone();
+        var selectmenu = document.getElementById("selectmenu");
+        var button = document.getElementById("selectmenu-button");
+        var listboxScroll = document.getElementById("selectmenu-listbox-scroll");
+        button.focus();
+        eventSender.keyDown("\r");
+        shouldBeTrue('selectmenu.open');
+        eventSender.keyDown("a");
+        eventSender.keyDown("z");
+        setTimeout(function() {
+            var result = document.getElementById("result");
+            result.innerText = listboxScroll.scrollTop > 0 ? "SUCCESS" : "FAIL";
+            testRunner.notifyDone();
+        }, 1000);
+    }
+</script>
diff --git a/third_party/blink/web_tests/html/selectmenu/selectmenu-typeahead-with-spacekey-expected.txt b/third_party/blink/web_tests/html/selectmenu/selectmenu-typeahead-with-spacekey-expected.txt
new file mode 100644
index 0000000..18cd081
--- /dev/null
+++ b/third_party/blink/web_tests/html/selectmenu/selectmenu-typeahead-with-spacekey-expected.txt
@@ -0,0 +1,22 @@
+Two keystrokes are considered as part of one typehead session if time difference between them is less than 1 sec
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+1. space key as part of search string.
+PASS popup.open is false
+PASS popup.value is "United States"
+2. space key as part of search string with some delay.
+PASS popup.open is false
+PASS popup.value is "Spain"
+3. space key to open popup menu.
+PASS popup.open is true
+PASS popup.value is "United Arab Emirates"
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
+Button
+Canada
+Spain
+United Arab Emirates
+United States
diff --git a/third_party/blink/web_tests/html/selectmenu/selectmenu-typeahead-with-spacekey.html b/third_party/blink/web_tests/html/selectmenu/selectmenu-typeahead-with-spacekey.html
new file mode 100644
index 0000000..4ce90eb
--- /dev/null
+++ b/third_party/blink/web_tests/html/selectmenu/selectmenu-typeahead-with-spacekey.html
@@ -0,0 +1,84 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script src="../../resources/js-test.js"></script>
+</head>
+<body>
+<selectmenu id="selectmenu" tabindex="0">
+    <button type="button" slot="button" behavior="button"
+             id="selectmenu-button">
+        Button
+    </button>
+    <option>Canada</option>
+    <option>Spain</option>
+    <option>United Arab Emirates</option>
+    <option>United States</option>
+</selectmenu>
+<script>
+description('Two keystrokes are considered as part of one typehead session if time difference between them is less than 1 sec');
+
+window.jsTestIsAsync = true;
+
+function keyDown(key, modifiers)
+{
+    if (!window.eventSender)
+      debug("No event sender");
+    eventSender.keyDown(key, modifiers);
+}
+
+var popup = document.getElementById("selectmenu");
+var button = document.getElementById("selectmenu-button");
+
+function test1() {
+    debug('1. space key as part of search string.');
+    button.focus();
+    popup.value = "Canada";
+    keyDown('U');
+    keyDown('n');
+    keyDown('i');
+    keyDown('t');
+    keyDown('e');
+    keyDown('d');
+    keyDown(' ');
+    keyDown('S');
+    shouldBeFalse('popup.open');
+    shouldBeEqualToString('popup.value', 'United States');
+    button.blur();
+
+    debug('2. space key as part of search string with some delay.');
+    button.focus();
+    popup.value = "Canada";
+    keyDown('U');
+    keyDown('n');
+    keyDown('i');
+    keyDown('t');
+    keyDown('e');
+    keyDown('d');
+    keyDown(' ');
+    internals.resetSelectMenuTypeAheadSession(popup);
+    keyDown('S');
+    shouldBeFalse('popup.open');
+    shouldBeEqualToString('popup.value', 'Spain');
+    button.blur();
+
+    debug('3. space key to open popup menu.');
+    button.focus();
+    popup.value = "Canada";
+    keyDown('U');
+    keyDown('n');
+    keyDown('i');
+    keyDown('t');
+    keyDown('e');
+    keyDown('d');
+    internals.resetSelectMenuTypeAheadSession(popup);
+    keyDown(' ');
+    shouldBeTrue('popup.open');
+    shouldBeEqualToString('popup.value', 'United Arab Emirates');
+    button.blur();
+
+    finishJSTest();
+}
+
+test1();
+</script>
+</body>