diff --git a/third_party/blink/renderer/core/dom/node.cc b/third_party/blink/renderer/core/dom/node.cc index 6936978b34ca0..f74496a0d44bb 100644 --- a/third_party/blink/renderer/core/dom/node.cc +++ b/third_party/blink/renderer/core/dom/node.cc @@ -57,6 +57,7 @@ #include "third_party/blink/renderer/core/dom/events/event_dispatcher.h" #include "third_party/blink/renderer/core/dom/events/event_listener.h" #include "third_party/blink/renderer/core/dom/events/event_path.h" +#include "third_party/blink/renderer/core/dom/events/scoped_event_queue.h" #include "third_party/blink/renderer/core/dom/flat_tree_node_data.h" #include "third_party/blink/renderer/core/dom/flat_tree_traversal.h" #include "third_party/blink/renderer/core/dom/focus_params.h" @@ -771,6 +772,12 @@ void Node::moveBefore(Node* new_child, // impossible for nested `moveBefore()` calls to occur. Assert that no atomic // move is already in progress. DCHECK(!GetDocument().StatePreservingAtomicMoveInProgress()); + + // Insertion can synchronously reach paths that emit scoped events (for + // example, form controls reacting to ancestor disabled-state changes). Defer + // any such events until the state-preserving atomic move has completed so + // that listeners never observe the intermediate state. + EventQueueScope queue_scope; GetDocument().SetStatePreservingAtomicMoveInProgress(true); ContainerNode* old_parent = new_child->parentNode(); diff --git a/third_party/blink/renderer/core/dom/node_test.cc b/third_party/blink/renderer/core/dom/node_test.cc index d68bcfeff76f8..d48f5c3b20c5b 100644 --- a/third_party/blink/renderer/core/dom/node_test.cc +++ b/third_party/blink/renderer/core/dom/node_test.cc @@ -5,11 +5,13 @@ #include "third_party/blink/renderer/core/dom/node.h" #include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_core.h" +#include "third_party/blink/renderer/bindings/core/v8/v8_mouse_event_init.h" #include "third_party/blink/renderer/bindings/core/v8/v8_shadow_root_init.h" #include "third_party/blink/renderer/core/css/resolver/style_resolver.h" #include "third_party/blink/renderer/core/css/style_engine.h" #include "third_party/blink/renderer/core/dom/comment.h" #include "third_party/blink/renderer/core/dom/element.h" +#include "third_party/blink/renderer/core/dom/events/native_event_listener.h" #include "third_party/blink/renderer/core/dom/flat_tree_traversal.h" #include "third_party/blink/renderer/core/dom/layout_tree_builder.h" #include "third_party/blink/renderer/core/dom/processing_instruction.h" @@ -18,7 +20,10 @@ #include "third_party/blink/renderer/core/dom/slot_assignment_engine.h" #include "third_party/blink/renderer/core/dom/slot_assignment_recalc_forbidden_scope.h" #include "third_party/blink/renderer/core/editing/testing/editing_test_base.h" +#include "third_party/blink/renderer/core/events/mouse_event.h" +#include "third_party/blink/renderer/core/html/forms/spin_button_element.h" #include "third_party/blink/renderer/core/html/html_div_element.h" +#include "third_party/blink/renderer/core/html/shadow/shadow_element_names.h" #include "third_party/blink/renderer/platform/heap/garbage_collected.h" namespace blink { @@ -610,6 +615,66 @@ TEST_F(NodeTest, moveBefore_DetachAfterSlotReassignment) { EXPECT_FALSE(span->GetComputedStyle()); } +namespace { + +class AtomicMoveStateRecorder : public NativeEventListener { + public: + void Invoke(ExecutionContext*, Event* event) override { + fired_ = true; + Node* target = event->target()->ToNode(); + in_atomic_move_ = + target->GetDocument().StatePreservingAtomicMoveInProgress(); + } + + bool fired_ = false; + bool in_atomic_move_ = false; +}; + +} // namespace + +TEST_F(NodeTest, moveBefore_DefersScopedEvents) { + GetDocument().body()->SetHTMLUnsafeWithoutTrustedTypes(R"HTML( + +
+ )HTML"); + + UpdateAllLifecyclePhasesForTest(); + + Element* fieldset = GetDocument().getElementById(AtomicString("fs")); + Element* legend = GetDocument().getElementById(AtomicString("lg")); + Element* input = GetDocument().getElementById(AtomicString("num")); + + // The number input is inside the first