Chrome · HTML
CVE-2026-113036
Memory Corruption in HTML
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
StyleScopeDatathird_party/blink/renderer/core/dom/element.h |
modified | |
TextVisitorthird_party/blink/renderer/core/dom/element.h |
modified | |
TrustedHTMLParserOptionsthird_party/blink/renderer/core/dom/element.h |
modified | |
UIEventthird_party/blink/renderer/core/dom/element.h |
modified | |
V8UnionBooleanOrScrollIntoViewOptionsthird_party/blink/renderer/core/dom/element.h |
modified | |
V8UnionCSSPseudoElementOrDocumentOrElementOrTextthird_party/blink/renderer/core/dom/element.h |
modified | |
V8UnionKeyframeAnimationOptionsOrUnrestrictedDoublethird_party/blink/renderer/core/dom/element.h |
modified | |
ListAttributeTargetObserverthird_party/blink/renderer/core/html/forms/html_input_element.h |
modified | |
RadioButtonGroupScopethird_party/blink/renderer/core/html/forms/html_input_element.h |
modified | |
ScriptObjectthird_party/blink/renderer/core/html/forms/html_input_element.h |
modified | |
UIEventthird_party/blink/renderer/core/html/forms/html_input_element.h |
modified | |
CORE_EXPORTthird_party/blink/renderer/core/html/forms/html_input_element.h |
modified | |
V8UnionHTMLOptGroupElementOrHTMLOptionElementthird_party/blink/renderer/core/html/forms/html_select_element.h |
modified | |
HTMLSelectedContentElementthird_party/blink/renderer/core/html/forms/html_select_element.h |
modified | |
SelectMutationObserverthird_party/blink/renderer/core/html/forms/html_select_element.h |
modified | |
UIEventthird_party/blink/renderer/core/html/forms/html_select_element.h |
modified | |
UIEventthird_party/blink/renderer/core/html/html_details_element.h |
modified | |
HTMLDetailsElementthird_party/blink/renderer/core/html/html_details_element.h |
modified | |
Documentthird_party/blink/renderer/core/html/html_dialog_element.h |
modified | |
ExceptionStatethird_party/blink/renderer/core/html/html_dialog_element.h |
modified | |
PointerEventthird_party/blink/renderer/core/html/html_dialog_element.h |
modified | |
UIEventthird_party/blink/renderer/core/html/html_dialog_element.h |
modified | |
KeyboardEventthird_party/blink/renderer/core/html/html_element.h |
modified | |
PointerEventthird_party/blink/renderer/core/html/html_element.h |
modified | |
TextControlElementthird_party/blink/renderer/core/html/html_element.h |
modified | |
UIEventthird_party/blink/renderer/core/html/html_element.h |
modified | |
V8UnionStringLegacyNullToEmptyStringOrTrustedScriptthird_party/blink/renderer/core/html/html_element.h |
modified | |
V8UnionBooleanOrTogglePopoverOptionsthird_party/blink/renderer/core/html/html_element.h |
modified | |
ShowPopoverOptionsthird_party/blink/renderer/core/html/html_element.h |
modified | |
ifthird_party/blink/renderer/core/html/html_menu_item_element.cc |
modified |
Files Changed
third_party/blink/renderer/core/html/media/html_media_element.hthird_party/blink/web_tests/external/wpt/html/semantics/menu/tentative/resources/focus-without-user-activation-helper.htmlthird_party/blink/renderer/core/html/html_menu_item_element.hthird_party/blink/renderer/core/dom/element.hthird_party/blink/renderer/core/html/html_element.hthird_party/blink/renderer/core/html/html_details_element.hthird_party/blink/renderer/core/html/forms/html_form_control_element.ccthird_party/blink/renderer/core/html/html_menu_list_element.ccthird_party/blink/renderer/core/html/forms/html_select_element.ccthird_party/blink/renderer/core/html/media/html_media_element.ccthird_party/blink/renderer/core/html/html_element.ccthird_party/blink/renderer/core/html/forms/html_select_element.hthird_party/blink/renderer/core/html/forms/html_input_element.ccthird_party/blink/renderer/core/html/html_details_element.ccthird_party/blink/renderer/core/html/html_menu_list_element.hthird_party/blink/renderer/core/html/html_dialog_element.ccthird_party/blink/renderer/core/html/forms/html_input_element.hthird_party/blink/renderer/core/html/html_menu_item_element.ccthird_party/blink/web_tests/external/wpt/html/semantics/menu/tentative/focus-without-user-activation.htmlthird_party/blink/renderer/core/html/forms/html_button_element.ccthird_party/blink/renderer/core/html/html_dialog_element.h
Patch
From ba1db4b3065871bdabd2822b6980df4ea122cac6 Mon Sep 17 00:00:00 2001
From: David Baron <dbaron@chromium.org>
Date: Mon, 21 Sep 2026 11:34:33 -0700
Subject: [PATCH] Don't use FocusTrigger::kUserGesture when handling untrusted key events.
This makes menu elements stop using EventHandler::IsHandlingKeyEvent()
and instead make decisions based on the event we're actually handling.
Fixed: 553335083
Change-Id: Ie574a5595c9d476387ef3bcfa77a7244b00c83c0
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8427363
Reviewed-by: Joey Arhar <jarhar@chromium.org>
Commit-Queue: David Baron <dbaron@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1701919}
---
diff --git a/third_party/blink/renderer/core/dom/element.h b/third_party/blink/renderer/core/dom/element.h
index eaa37b0..5c5e8e77 100644
--- a/third_party/blink/renderer/core/dom/element.h
+++ b/third_party/blink/renderer/core/dom/element.h
@@ -174,6 +174,7 @@
class StyleScopeData;
class TextVisitor;
class TrustedHTMLParserOptions;
+class UIEvent;
class V8UnionBooleanOrScrollIntoViewOptions;
class V8UnionCSSPseudoElementOrDocumentOrElementOrText;
class V8UnionKeyframeAnimationOptionsOrUnrestrictedDouble;
@@ -1400,7 +1401,8 @@
return false;
}
virtual bool HandleCommandInternal(HTMLElement& invoker,
- CommandEventType command) {
+ CommandEventType command,
+ UIEvent* activate_event) {
CHECK(command != CommandEventType::kCustom &&
command != CommandEventType::kNone);
diff --git a/third_party/blink/renderer/core/html/forms/html_button_element.cc b/third_party/blink/renderer/core/html/forms/html_button_element.cc
index 243c838..4511cc9 100644
--- a/third_party/blink/renderer/core/html/forms/html_button_element.cc
+++ b/third_party/blink/renderer/core/html/forms/html_button_element.cc
@@ -35,6 +35,7 @@
#include "third_party/blink/renderer/core/dom/focus_params.h"
#include "third_party/blink/renderer/core/dom/qualified_name.h"
#include "third_party/blink/renderer/core/events/command_event.h"
+#include "third_party/blink/renderer/core/events/ui_event.h"
#include "third_party/blink/renderer/core/execution_context/execution_context.h"
#include "third_party/blink/renderer/core/frame/web_feature.h"
#include "third_party/blink/renderer/core/html/forms/form_data.h"
@@ -261,7 +262,7 @@
}
}
- if (HandleCommandForActivation()) {
+ if (HandleCommandForActivation(To<UIEvent>(&event))) {
return;
}
}
diff --git a/third_party/blink/renderer/core/html/forms/html_form_control_element.cc b/third_party/blink/renderer/core/html/forms/html_form_control_element.cc
index 45302f6..46bdaa6 100644
--- a/third_party/blink/renderer/core/html/forms/html_form_control_element.cc
+++ b/third_party/blink/renderer/core/html/forms/html_form_control_element.cc
@@ -38,6 +38,7 @@
#include "third_party/blink/renderer/core/event_type_names.h"
#include "third_party/blink/renderer/core/events/command_event.h"
#include "third_party/blink/renderer/core/events/keyboard_event.h"
+#include "third_party/blink/renderer/core/events/ui_event.h"
#include "third_party/blink/renderer/core/execution_context/execution_context.h"
#include "third_party/blink/renderer/core/frame/web_feature.h"
#include "third_party/blink/renderer/core/html/forms/html_button_element.h"
@@ -484,7 +485,8 @@
}
CHECK(popover.popover->IsValidBuiltinCommand(*this, action));
- popover.popover->HandleCommandInternal(*this, action);
+ popover.popover->HandleCommandInternal(*this, action,
+ To<UIEvent>(&event));
}
}
}
diff --git a/third_party/blink/renderer/core/html/forms/html_input_element.cc b/third_party/blink/renderer/core/html/forms/html_input_element.cc
index 76d2a460..fc27aad 100644
--- a/third_party/blink/renderer/core/html/forms/html_input_element.cc
+++ b/third_party/blink/renderer/core/html/forms/html_input_element.cc
@@ -2629,10 +2629,11 @@
}
bool HTMLInputElement::HandleCommandInternal(HTMLElement& invoker,
- CommandEventType command) {
+ CommandEventType command,
+ UIEvent* activate_event) {
CHECK(IsValidBuiltinCommand(invoker, command));
- if (HTMLElement::HandleCommandInternal(invoker, command)) {
+ if (HTMLElement::HandleCommandInternal(invoker, command, activate_event)) {
return true;
}
diff --git a/third_party/blink/renderer/core/html/forms/html_input_element.h b/third_party/blink/renderer/core/html/forms/html_input_element.h
index 408e755c..10cb096 100644
--- a/third_party/blink/renderer/core/html/forms/html_input_element.h
+++ b/third_party/blink/renderer/core/html/forms/html_input_element.h
@@ -53,6 +53,7 @@
class ListAttributeTargetObserver;
class RadioButtonGroupScope;
class ScriptObject;
+class UIEvent;
struct DateTimeChooserParameters;
class CORE_EXPORT HTMLInputElement
@@ -426,7 +427,8 @@
bool IsValidBuiltinCommand(HTMLElement& invoker,
CommandEventType command) override;
bool HandleCommandInternal(HTMLElement& invoker,
- CommandEventType command) override;
+ CommandEventType command,
+ UIEvent* activate_event) override;
void SetFocused(bool is_focused,
mojom::blink::FocusType,
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 a8c8c231..ec67bfe 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
@@ -1860,10 +1860,11 @@
}
bool HTMLSelectElement::HandleCommandInternal(HTMLElement& invoker,
- CommandEventType command) {
+ CommandEventType command,
+ UIEvent* activate_event) {
CHECK(IsValidBuiltinCommand(invoker, command));
- if (HTMLElement::HandleCommandInternal(invoker, command)) {
+ if (HTMLElement::HandleCommandInternal(invoker, command, activate_event)) {
return true;
}
diff --git a/third_party/blink/renderer/core/html/forms/html_select_element.h b/third_party/blink/renderer/core/html/forms/html_select_element.h
index 7826c9ea..86b48598 100644
--- a/third_party/blink/renderer/core/html/forms/html_select_element.h
+++ b/third_party/blink/renderer/core/html/forms/html_select_element.h
@@ -54,6 +54,7 @@
class V8UnionHTMLOptGroupElementOrHTMLOptionElement;
class HTMLSelectedContentElement;
class SelectMutationObserver;
+class UIEvent;
enum class SelectPopupHideBehavior {
kNormal,
@@ -295,7 +296,8 @@
bool IsValidBuiltinCommand(HTMLElement& invoker,
CommandEventType command) override;
bool HandleCommandInternal(HTMLElement& invoker,
- CommandEventType command) override;
+ CommandEventType command,
+ UIEvent* activate_event) override;
// SlottedButton returns the first child <button> in the light dom tree. If
// this select is in a state where the <button> can't be rendered, such as a
diff --git a/third_party/blink/renderer/core/html/html_details_element.cc b/third_party/blink/renderer/core/html/html_details_element.cc
index 2ad521f9..edc0262 100644
--- a/third_party/blink/renderer/core/html/html_details_element.cc
+++ b/third_party/blink/renderer/core/html/html_details_element.cc
@@ -305,11 +305,12 @@
}
bool HTMLDetailsElement::HandleCommandInternal(HTMLElement& invoker,
- CommandEventType command) {
+ CommandEventType command,
+ UIEvent* activate_event) {
if (!IsValidBuiltinCommand(invoker, command)) {
return false;
}
- if (HTMLElement::HandleCommandInternal(invoker, command)) {
+ if (HTMLElement::HandleCommandInternal(invoker, command, activate_event)) {
return true;
}
diff --git a/third_party/blink/renderer/core/html/html_details_element.h b/third_party/blink/renderer/core/html/html_details_element.h
index fee7abb1..51c389b2 100644
--- a/third_party/blink/renderer/core/html/html_details_element.h
+++ b/third_party/blink/renderer/core/html/html_details_element.h
@@ -27,6 +27,8 @@
namespace blink {
+class UIEvent;
+
class HTMLDetailsElement final : public HTMLElement {
DEFINE_WRAPPERTYPEINFO();
@@ -49,7 +51,8 @@
bool IsValidBuiltinCommand(HTMLElement& invoker,
CommandEventType command) override;
bool HandleCommandInternal(HTMLElement& invoker,
- CommandEventType command) override;
+ CommandEventType command,
+ UIEvent* activate_event) override;
// The name attribute for grouping of related details; empty string
// means no grouping.
diff --git a/third_party/blink/renderer/core/html/html_dialog_element.cc b/third_party/blink/renderer/core/html/html_dialog_element.cc
index 1f0d86e..5718379 100644
--- a/third_party/blink/renderer/core/html/html_dialog_element.cc
+++ b/third_party/blink/renderer/core/html/html_dialog_element.cc
@@ -383,11 +383,12 @@
}
bool HTMLDialogElement::HandleCommandInternal(HTMLElement& invoker,
- CommandEventType command) {
+ CommandEventType command,
+ UIEvent* activate_event) {
if (!IsValidBuiltinCommand(invoker, command)) {
return false;
}
- if (HTMLElement::HandleCommandInternal(invoker, command)) {
+ if (HTMLElement::HandleCommandInternal(invoker, command, activate_event)) {
return true;
}
diff --git a/third_party/blink/renderer/core/html/html_dialog_element.h b/third_party/blink/renderer/core/html/html_dialog_element.h
index 7ad67e4..38d56bf 100644
--- a/third_party/blink/renderer/core/html/html_dialog_element.h
+++ b/third_party/blink/renderer/core/html/html_dialog_element.h
@@ -39,6 +39,7 @@
class Document;
class ExceptionState;
class PointerEvent;
+class UIEvent;
enum class ClosedByState {
kAny,
@@ -130,7 +131,8 @@
bool IsValidBuiltinCommand(HTMLElement& invoker,
CommandEventType command) override;
bool HandleCommandInternal(HTMLElement& invoker,
- CommandEventType command) override;
+ CommandEventType command,
+ UIEvent* activate_event) override;
void AttributeChanged(const AttributeModificationParams&) override;
void ParseAttribute(const AttributeModificationParams&) override;
diff --git a/third_party/blink/renderer/core/html/html_element.cc b/third_party/blink/renderer/core/html/html_element.cc
index 86599e7..4e92bed 100644
--- a/third_party/blink/renderer/core/html/html_element.cc
+++ b/third_party/blink/renderer/core/html/html_element.cc
@@ -89,6 +89,7 @@
#include "third_party/blink/renderer/core/events/keyboard_event.h"
#include "third_party/blink/renderer/core/events/pointer_event.h"
#include "third_party/blink/renderer/core/events/toggle_event.h"
+#include "third_party/blink/renderer/core/events/ui_event.h"
#include "third_party/blink/renderer/core/frame/csp/content_security_policy.h"
#include "third_party/blink/renderer/core/frame/local_dom_window.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
@@ -3254,11 +3255,12 @@
}
bool HTMLElement::HandleCommandInternal(HTMLElement& invoker,
- CommandEventType command) {
+ CommandEventType command,
+ UIEvent* activate_event) {
if (!IsValidBuiltinCommand(invoker, command)) {
return false;
}
- if (Element::HandleCommandInternal(invoker, command)) {
+ if (Element::HandleCommandInternal(invoker, command, activate_event)) {
return true;
}
@@ -3389,7 +3391,7 @@
return false;
}
-bool HTMLElement::HandleCommandForActivation() {
+bool HTMLElement::HandleCommandForActivation(UIEvent* activate_event) {
if (!CanBeCommandInvoker()) {
return false;
}
@@ -3429,7 +3431,8 @@
command_target->DispatchEvent(*command_event);
if (!command_event->defaultPrevented() &&
command_event_type != CommandEventType::kCustom) {
- command_target->HandleCommandInternal(*this, command_event_type);
+ command_target->HandleCommandInternal(*this, command_event_type,
+ activate_event);
}
return true;
}
@@ -4205,7 +4208,7 @@
return;
}
- if (HandleCommandForActivation()) {
+ if (HandleCommandForActivation(To<UIEvent>(&event))) {
return;
}
}
diff --git a/third_party/blink/renderer/core/html/html_element.h b/third_party/blink/renderer/core/html/html_element.h
index cd12035..0a1a7ee 100644
--- a/third_party/blink/renderer/core/html/html_element.h
+++ b/third_party/blink/renderer/core/html/html_element.h
@@ -49,6 +49,7 @@
class KeyboardEvent;
class PointerEvent;
class TextControlElement;
+class UIEvent;
class V8UnionStringLegacyNullToEmptyStringOrTrustedScript;
class V8UnionBooleanOrTogglePopoverOptions;
class ShowPopoverOptions;
@@ -392,7 +393,8 @@
CommandEventType command) override;
bool IsValidBuiltinPopoverCommand(CommandEventType command);
bool HandleCommandInternal(HTMLElement& invoker,
- CommandEventType command) override;
+ CommandEventType command,
+ UIEvent* activate_event) override;
// This is true if this element *can* be a command invoker: it is an element
// type that supports command invokers (e.g. buttons and menuitems), and the
// element isn't in a state that disqualifies it (e.g. a disabled state). This
@@ -401,7 +403,7 @@
virtual bool CanBeCommandInvoker() const;
static CommandEventType GetCommandEventType(const AtomicString& type,
ExecutionContext*);
- virtual bool HandleCommandForActivation();
+ virtual bool HandleCommandForActivation(UIEvent* activate_event);
Element* commandForElement() const;
AtomicString command() const;
void setCommand(const AtomicString& type);
diff --git a/third_party/blink/renderer/core/html/html_menu_item_element.cc b/third_party/blink/renderer/core/html/html_menu_item_element.cc
index ed65bcc8..625cde4 100644
--- a/third_party/blink/renderer/core/html/html_menu_item_element.cc
+++ b/third_party/blink/renderer/core/html/html_menu_item_element.cc
@@ -15,6 +15,7 @@
#include "third_party/blink/renderer/core/events/command_event.h"
#include "third_party/blink/renderer/core/events/keyboard_event.h"
#include "third_party/blink/renderer/core/events/mouse_event.h"
+#include "third_party/blink/renderer/core/events/ui_event.h"
#include "third_party/blink/renderer/core/execution_context/execution_context.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/frame/web_feature.h"
@@ -26,7 +27,6 @@
#include "third_party/blink/renderer/core/html/html_sub_menu_element.h"
#include "third_party/blink/renderer/core/html_names.h"
#include "third_party/blink/renderer/core/input/event_handler.h"
-#include "third_party/blink/renderer/core/input/keyboard_event_manager.h"
#include "third_party/blink/renderer/core/keywords.h"
#include "third_party/blink/renderer/platform/instrumentation/use_counter.h"
#include "third_party/blink/renderer/platform/runtime_enabled_features.h"
@@ -231,7 +231,8 @@
}
}
-void HTMLMenuItemElement::ActivateMenuItem() {
+void HTMLMenuItemElement::ActivateMenuItem(
+ ActivationKeyboardEventType activation_type) {
// A menu item's checkability and ability to invoke a command are
// exclusive. If the item is checkable, that takes precedence, and the sub-
// menu invoker will NOT be respected.
@@ -248,13 +249,13 @@
submenu->InvokePopover(*this, PopoverInvokedVia::kCommand);
- bool handling_keyboard_event = false;
- if (LocalFrame* frame = GetDocument().GetFrame()) {
- handling_keyboard_event = frame->GetEventHandler().IsHandlingKeyEvent();
- }
if (submenu->popoverOpen()) {
- if (handling_keyboard_event) {
- submenu->FocusFirstItem();
+ if (activation_type != ActivationKeyboardEventType::kNotKeyboard) {
+ FocusTrigger trigger =
+ activation_type == ActivationKeyboardEventType::kTrustedKeyboard
+ ? FocusTrigger::kUserGesture
+ : FocusTrigger::kScript;
+ submenu->FocusFirstItem(trigger);
} else {
// If this is resulting from a click, this is happening *before* that
// click has had a chance to focus this menuitem. However, we should
@@ -311,7 +312,10 @@
return;
}
- FocusParams focus_params(FocusTrigger::kUserGesture);
+ CHECK(event.IsFullyTrusted())
+ << "FocusTrigger::kUserGesture depends on trusted events";
+ FocusTrigger key_focus_trigger = FocusTrigger::kUserGesture;
+ FocusParams focus_params(key_focus_trigger);
const AtomicString key(keyboard_event->key());
// Nothing else below does anything if we're not inside an owner menu that has
@@ -530,12 +534,12 @@
if (auto* invoked_menulist = GetInvokedSubmenu()) {
invoked_menulist->InvokePopover(*this, PopoverInvokedVia::kCommand);
if (key == keywords::kArrowDown) {
- if (invoked_menulist->FocusFirstItem()) {
+ if (invoked_menulist->FocusFirstItem(key_focus_trigger)) {
event.SetDefaultHandled();
return;
}
... (file diff truncated)
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/third_party/blink/web_tests/external/wpt/html/semantics/menu/tentative/focus-without-user-activation.html b/third_party/blink/web_tests/external/wpt/html/semantics/menu/tentative/focus-without-user-activation.html
new file mode 100644
index 0000000..3ff214a1
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/html/semantics/menu/tentative/focus-without-user-activation.html
@@ -0,0 +1,178 @@
+<!doctype html>
+<title>focusing first item in menu respects focus-without-user-activation</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<body>
+ <input type="text" id="top-input" />
+ <script>
+ const tabKey = "\uE004";
+ const enterKey = "\uE007";
+ const shiftKey = "\uE008";
+
+ const waitForMessage = (expectedAction) => {
+ return new Promise((resolve) => {
+ const handler = (e) => {
+ if (e.data && e.data.action === expectedAction) {
+ window.removeEventListener("message", handler);
+ resolve(e.data);
+ }
+ };
+ window.addEventListener("message", handler);
+ });
+ };
+
+ function createTestIframe(t, target) {
+ const iframe = document.createElement("iframe");
+ iframe.allow = "focus-without-user-activation 'none'";
+ iframe.src = `/html/semantics/menu/tentative/resources/focus-without-user-activation-helper.html?target=${target}`;
+ document.body.appendChild(iframe);
+ t.add_cleanup(() => iframe.remove());
+ return iframe;
+ }
+
+ // Test 1: Script click during key event on button commandfor menulist
+ promise_test(async (t) => {
+ const topInput = document.querySelector("#top-input");
+ const iframe = createTestIframe(t, "button");
+ await waitForMessage("ready");
+
+ const trigger = iframe.contentDocument.getElementById("trigger");
+ trigger.focus();
+ assert_equals(iframe.contentDocument.activeElement, trigger, "Trigger should have initial focus");
+
+ const shiftCompletePromise = waitForMessage("shift-click-complete");
+
+ // Press Shift key on the trigger in iframe.
+ // The Shift key handler will park focus on topInput and call trigger.click().
+ await new test_driver.Actions()
+ .keyDown(shiftKey)
+ .keyUp(shiftKey)
+ .send();
+
+ const msg = await shiftCompletePromise;
+
+ assert_true(msg.isPopoverOpen, "Popover should have opened");
+ assert_equals(
+ document.activeElement,
+ topInput,
+ "Top-level document should not lose focus on script click during key event",
+ );
+ assert_not_equals(
+ msg.activeElementId,
+ "first-item",
+ "First item should not have gained focus without user activation",
+ );
+ }, "Script click during key event on button commandfor menulist does not bypass focus-without-user-activation");
+
+ // Test 2: Script click during key event on menuitem activating submenu
+ promise_test(async (t) => {
+ const topInput = document.querySelector("#top-input");
+ const iframe = createTestIframe(t, "menuitem");
+ await waitForMessage("ready");
+
+ const trigger = iframe.contentDocument.getElementById("trigger");
+ trigger.focus();
+ assert_equals(iframe.contentDocument.activeElement, trigger, "Trigger should have initial focus");
+
+ const shiftCompletePromise = waitForMessage("shift-click-complete");
+
+ // Press Shift key on the trigger in iframe.
+ // The Shift key handler will park focus on topInput and call trigger.click().
+ await new test_driver.Actions()
+ .keyDown(shiftKey)
+ .keyUp(shiftKey)
+ .send();
+
+ const msg = await shiftCompletePromise;
+
+ assert_true(msg.isPopoverOpen, "Submenu should have opened");
+ assert_equals(
+ document.activeElement,
+ topInput,
+ "Top-level document should not lose focus on script click during key event",
+ );
+ assert_not_equals(
+ msg.activeElementId,
+ "first-item",
+ "Submenu first item should not have gained focus without user activation",
+ );
+ }, "Script click during key event on menuitem activating submenu does not bypass focus-without-user-activation");
+
+ // Test 3: Trusted key event on button commandfor menulist
+ promise_test(async (t) => {
+ const topInput = document.querySelector("#top-input");
+ const iframe = createTestIframe(t, "button");
+ await waitForMessage("ready");
+
+ topInput.focus();
+ assert_equals(document.activeElement, topInput, "Top input should have focus");
+
+ const itemFocusedPromise = waitForMessage("item-focused");
+
+ // Press Tab to navigate focus into the iframe's trigger button
+ await new test_driver.Actions()
+ .keyDown(tabKey)
+ .keyUp(tabKey)
+ .send();
+
+ // Press Enter on the focused trigger button
+ await new test_driver.Actions()
+ .keyDown(enterKey)
+ .keyUp(enterKey)
+ .send();
+
+ const msg = await itemFocusedPromise;
+ assert_true(msg.isPopoverOpen, "Popover should have opened");
+ assert_equals(
+ msg.activeElementId,
+ "first-item",
+ "First item in menulist should receive focus from trusted key event",
+ );
+ assert_equals(
+ document.activeElement,
+ iframe,
+ "Iframe should have focus after trusted key event",
+ );
+ }, "Trusted key event on button commandfor menulist focuses first item");
+
+ // Test 4: Trusted key event on menuitem activating submenu
+ promise_test(async (t) => {
+ const topInput = document.querySelector("#top-input");
+ const iframe = createTestIframe(t, "menuitem");
+ await waitForMessage("ready");
+
+ topInput.focus();
+ assert_equals(document.activeElement, topInput, "Top input should have focus");
+
+ const itemFocusedPromise = waitForMessage("item-focused");
+
+ // Press Tab to navigate focus into the iframe's trigger menuitem
+ await new test_driver.Actions()
+ .keyDown(tabKey)
+ .keyUp(tabKey)
+ .send();
+
+ // Press Enter on the focused trigger menuitem
+ await new test_driver.Actions()
+ .keyDown(enterKey)
+ .keyUp(enterKey)
+ .send();
+
+ const msg = await itemFocusedPromise;
+ assert_true(msg.isPopoverOpen, "Submenu should have opened");
+ assert_equals(
+ msg.activeElementId,
+ "first-item",
+ "First item in submenu should receive focus from trusted key event",
+ );
+ assert_equals(
+ document.activeElement,
+ iframe,
+ "Iframe should have focus after trusted key event",
+ );
+ }, "Trusted key event on menuitem activating submenu focuses first item");
+ </script>
+</body>
diff --git a/third_party/blink/web_tests/external/wpt/html/semantics/menu/tentative/resources/focus-without-user-activation-helper.html b/third_party/blink/web_tests/external/wpt/html/semantics/menu/tentative/resources/focus-without-user-activation-helper.html
new file mode 100644
index 0000000..9e182362
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/html/semantics/menu/tentative/resources/focus-without-user-activation-helper.html
@@ -0,0 +1,67 @@
+<!DOCTYPE html>
+<style>
+ body { margin: 0; padding: 0; }
+</style>
+<body>
+<script>
+const params = new URLSearchParams(location.search);
+const target = params.get("target") || "button";
+
+let trigger;
+let menulist;
+let firstItem;
+
+if (target === "button") {
+ document.body.innerHTML = `
+ <button id="trigger" commandfor="menulist" command="show-popover">Trigger</button>
+ <menulist id="menulist">
+ <menuitem id="first-item">First Item</menuitem>
+ <menuitem id="second-item">Second Item</menuitem>
+ </menulist>
+ `;
+} else {
+ document.body.innerHTML = `
+ <menubar>
+ <submenu>
+ <menuitem id="trigger">Trigger</menuitem>
+ <menulist id="menulist">
+ <menuitem id="first-item">First Item</menuitem>
+ <menuitem id="second-item">Second Item</menuitem>
+ </menulist>
+ </submenu>
+ </menubar>
+ `;
+}
+
+trigger = document.getElementById("trigger");
+menulist = document.getElementById("menulist");
+firstItem = document.getElementById("first-item");
+
+firstItem.addEventListener("focus", () => {
+ window.parent.postMessage({
+ action: "item-focused",
+ activeElementId: document.activeElement ? document.activeElement.id : null,
+ isPopoverOpen: menulist.matches(":popover-open")
+ }, "*");
+});
+
+trigger.addEventListener("keydown", (e) => {
+ if (e.key === "Shift") {
+ const parentInput = window.parent.document.getElementById("top-input");
+ if (parentInput) {
+ parentInput.focus();
+ }
+ trigger.click();
+ window.parent.postMessage({
+ action: "shift-click-complete",
+ activeElementId: document.activeElement ? document.activeElement.id : null,
+ isPopoverOpen: menulist.matches(":popover-open")
+ }, "*");
+ }
+});
+
+window.addEventListener("load", () => {
+ window.parent.postMessage({ action: "ready" }, "*");
+});
+</script>
+</body>
Loading diff…
Original Bug Report
The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.
References
On This Page