[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">А</option>
+ <option value="1">АБ</option>
+ <option value="2">АБВ</option>
+ <option value="3">АБВГ</option>
+ <option value="4">АБВГД</option>
+ <option value="5">АБВГДЕ</option>
+ <option value="6">АБВГДЕЖ</option>
+ <option value="7">АБВГДЕЖЗ</option>
+ <option value="8">АБВГДЕЖЗИ</option>
+ <option value="9">АБВГДЕЖЗИЙ</option>
+ <option value="10">АБВГДЕЖЗИЙК</option>
+ <option value="11">АБВГДЕЖЗИЙКЛ</option>
+ <option value="12">АБВГДЕЖЗИЙКЛМ</option>
+ <option value="13">АБВГДЕЖЗИЙКЛМН</option>
+ <option value="14">АБВГДЕЖЗИЙКЛМНО</option>
+ <option value="15">АБВГДЕЖЗИЙКЛМНОП</option>
+ <option value="16">АБВГДЕЖЗИЙКЛМНОПР</option>
+ <option value="17">АБВГДЕЖЗИЙКЛМНОПРС</option>
+ <option value="18">АБВГДЕЖЗИЙКЛМНОПРСТ</option>
+ <option value="19">АБВГДЕЖЗИЙКЛМНОПРСТУ</option>
+ <option value="20">АБВГДЕЖЗИЙКЛМНОПРСТУФ</option>
+ <option value="21">АБВГДЕЖЗИЙКЛМНОПРСТУФХ</option>
+ <option value="22">АБВГДЕЖЗИЙКЛМНОПРСТУФХЦ</option>
+ <option value="23">АБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧ</option>
+ <option value="24">АБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШ</option>
+ <option value="25">АБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩ</option>
+ <option value="26">АБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪ</option>
+ <option value="27">АБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫ</option>
+ <option value="28">АБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬ</option>
+ <option value="29">АБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭ</option>
+ <option value="30">АБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮ</option>
+ <option value="31">АБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ</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">Α</option>
+ <option value="1">ΑΒ</option>
+ <option value="2">ΑΒΓ</option>
+ <option value="3">ΑΒΓΔ</option>
+ <option value="4">ΑΒΓΔΕ</option>
+ <option value="5">ΑΒΓΔΕΖ</option>
+ <option value="6">ΑΒΓΔΕΖΗ</option>
+ <option value="7">ΑΒΓΔΕΖΗΘ</option>
+ <option value="8">ΑΒΓΔΕΖΗΘΙ</option>
+ <option value="9">ΑΒΓΔΕΖΗΘΙΚ</option>
+ <option value="10">ΑΒΓΔΕΖΗΘΙΚΛ</option>
+ <option value="11">ΑΒΓΔΕΖΗΘΙΚΛΜ</option>
+ <option value="12">ΑΒΓΔΕΖΗΘΙΚΛΜΝ</option>
+ <option value="13">ΑΒΓΔΕΖΗΘΙΚΛΜΝΞ</option>
+ <option value="14">ΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟ</option>
+ <option value="15">ΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠ</option>
+ <option value="16">ΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡ</option>
+ <option value="18">ΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣ</option>
+ <option value="19">ΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤ</option>
+ <option value="20">ΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥ</option>
+ <option value="21">ΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦ</option>
+ <option value="22">ΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧ</option>
+ <option value="23">ΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΨ</option>
+ <option value="24">ΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΨΩ</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>ש</option>
+ <option>ד</option>
+ <option>ג</option>
+ <option>כ</option>
+ <option>ע</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>