diff --git a/third_party/blink/renderer/core/dom/node.cc b/third_party/blink/renderer/core/dom/node.cc index 6936978b34ca0..d48bfc50dce21 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" @@ -775,8 +776,13 @@ void Node::moveBefore(Node* new_child, ContainerNode* old_parent = new_child->parentNode(); - insertBefore(new_child, ref_child, exception_state); - GetDocument().SetStatePreservingAtomicMoveInProgress(false); + { + // Defer any scoped events that `ChildrenChanged()` overrides may dispatch + // until after the atomic move completes. + EventQueueScope event_queue_scope; + insertBefore(new_child, ref_child, exception_state); + GetDocument().SetStatePreservingAtomicMoveInProgress(false); + } if (exception_state.HadException()) { return; diff --git a/third_party/blink/renderer/core/html/forms/html_input_element_test.cc b/third_party/blink/renderer/core/html/forms/html_input_element_test.cc index c51cd2ea70b5e..d585b9b24e62a 100644 --- a/third_party/blink/renderer/core/html/forms/html_input_element_test.cc +++ b/third_party/blink/renderer/core/html/forms/html_input_element_test.cc @@ -12,6 +12,7 @@ #include "third_party/blink/renderer/bindings/core/v8/v8_keyboard_event_init.h" #include "third_party/blink/renderer/bindings/core/v8/v8_wheel_event_init.h" #include "third_party/blink/renderer/core/dom/document.h" +#include "third_party/blink/renderer/core/dom/events/native_event_listener.h" #include "third_party/blink/renderer/core/dom/shadow_root.h" #include "third_party/blink/renderer/core/events/keyboard_event.h" #include "third_party/blink/renderer/core/events/wheel_event.h" @@ -24,6 +25,7 @@ #include "third_party/blink/renderer/core/html/forms/file_input_type.h" #include "third_party/blink/renderer/core/html/forms/html_form_element.h" #include "third_party/blink/renderer/core/html/forms/html_option_element.h" +#include "third_party/blink/renderer/core/html/forms/slider_thumb_element.h" #include "third_party/blink/renderer/core/html/forms/spin_button_element.h" #include "third_party/blink/renderer/core/html/html_body_element.h" #include "third_party/blink/renderer/core/html/html_html_element.h" @@ -50,6 +52,22 @@ class PasswordResetChromeClient : public EmptyChromeClient { (override)); }; +class AtomicMoveCaptureListener final : public NativeEventListener { + public: + void Invoke(ExecutionContext*, Event* event) override { + invoked_ = true; + Node* node = event->target()->ToNode(); + in_atomic_move_ = + node && node->GetDocument().StatePreservingAtomicMoveInProgress(); + } + bool Invoked() const { return invoked_; } + bool InAtomicMove() const { return in_atomic_move_; } + + private: + bool invoked_ = false; + bool in_atomic_move_ = false; +}; + class HTMLInputElementTestChromeClient : public EmptyChromeClient { public: gfx::Rect LocalRootToScreenDIPs(const gfx::Rect& local_root_rect, @@ -230,6 +248,42 @@ TEST_F(HTMLInputElementTest, NoAssertWhenMovedInNewDocument) { document.body()->RemoveChild(document.body()->firstChild()); } +TEST_F(HTMLInputElementTest, RangeChangeEventDeferredDuringMoveBefore) { + GetDocument().body()->SetInnerHTMLWithoutTrustedTypes(R"HTML( +
+ +
+ + )HTML"); + UpdateAllLifecyclePhasesForTest(); + + Element* fieldset = GetDocument().getElementById(AtomicString("fs")); + Element* l0 = GetDocument().getElementById(AtomicString("l0")); + Element* l1 = GetDocument().getElementById(AtomicString("l1")); + HTMLInputElement& slider = TestElement(); + + // The slider is inside the first , so it is enabled. + ASSERT_FALSE(slider.IsDisabledFormControl()); + + auto* thumb = + To(slider.UserAgentShadowRoot()->getElementById( + shadow_element_names::kIdSliderThumb)); + thumb->DragFrom(PhysicalOffset()); + + auto* listener = MakeGarbageCollected(); + slider.addEventListener(event_type_names::kChange, listener); + + // Inserting another first means the slider's is no longer + // the first child of the disabled
, so the slider becomes disabled + // and dragging is stopped. + fieldset->moveBefore(l0, l1, ASSERT_NO_EXCEPTION); + + EXPECT_TRUE(slider.IsDisabledFormControl()); + EXPECT_TRUE(listener->Invoked()); + // The 'change' event must be dispatched after the atomic move completes. + EXPECT_FALSE(listener->InAtomicMove()); +} + TEST_F(HTMLInputElementTest, DefaultToolTip) { auto* input_without_form = MakeGarbageCollected(GetDocument());