From fc35229dabc14c4e8686cf4ccf4b25fc8e445c65 Mon Sep 17 00:00:00 2001 From: Gingeh <39150378+Gingeh@users.noreply.github.com> Date: Wed, 4 Jun 2025 15:35:43 +1000 Subject: [PATCH] LibWeb: Implement the ToggleEvent.source attribute See: https://github.com/whatwg/html/pull/11186 --- Libraries/LibWeb/HTML/HTMLButtonElement.cpp | 8 +- Libraries/LibWeb/HTML/HTMLDialogElement.cpp | 93 ++++++++++--------- Libraries/LibWeb/HTML/HTMLDialogElement.h | 9 +- Libraries/LibWeb/HTML/HTMLElement.cpp | 54 +++++------ Libraries/LibWeb/HTML/HTMLElement.h | 4 +- Libraries/LibWeb/HTML/HTMLFormElement.cpp | 4 +- .../LibWeb/HTML/PopoverInvokerElement.cpp | 4 +- Libraries/LibWeb/HTML/ToggleEvent.cpp | 13 ++- Libraries/LibWeb/HTML/ToggleEvent.h | 16 +++- Libraries/LibWeb/HTML/ToggleEvent.idl | 1 + .../popover-toggle-source.tentative.txt | 13 +++ .../popover-toggle-source.tentative.html | 90 ++++++++++++++++++ .../resources/toggle-event-source-test.js | 76 +++++++++++++++ 13 files changed, 297 insertions(+), 88 deletions(-) create mode 100644 Tests/LibWeb/Text/expected/wpt-import/html/semantics/popovers/popover-toggle-source.tentative.txt create mode 100644 Tests/LibWeb/Text/input/wpt-import/html/semantics/popovers/popover-toggle-source.tentative.html create mode 100644 Tests/LibWeb/Text/input/wpt-import/html/semantics/popovers/resources/toggle-event-source-test.js diff --git a/Libraries/LibWeb/HTML/HTMLButtonElement.cpp b/Libraries/LibWeb/HTML/HTMLButtonElement.cpp index 41390bb0760..140314b3f39 100644 --- a/Libraries/LibWeb/HTML/HTMLButtonElement.cpp +++ b/Libraries/LibWeb/HTML/HTMLButtonElement.cpp @@ -223,9 +223,9 @@ void HTMLButtonElement::activation_behavior(DOM::Event const& event) // 9. If command is in the Hide Popover state: if (command == "hide-popover") { // 1. If the result of running check popover validity given target, true, false, and null is true, - // then run the hide popover algorithm given target, true, true, and false. + // then run the hide popover algorithm given target, true, true, false, and element. if (MUST(target->check_popover_validity(ExpectedToBeShowing::Yes, ThrowExceptions::No, nullptr, IgnoreDomState::No))) { - MUST(target->hide_popover(FocusPreviousElement::Yes, FireEvents::Yes, ThrowExceptions::No, IgnoreDomState::No)); + MUST(target->hide_popover(FocusPreviousElement::Yes, FireEvents::Yes, ThrowExceptions::No, IgnoreDomState::No, this)); } } @@ -238,9 +238,9 @@ void HTMLButtonElement::activation_behavior(DOM::Event const& event) } // 2. Otheriwse, if the result of running check popover validity given target, true, false, and null is true, - // then run the hide popover algorithm given target, true, true, and false. + // then run the hide popover algorithm given target, true, true, false and element. else if (MUST(target->check_popover_validity(ExpectedToBeShowing::Yes, ThrowExceptions::No, nullptr, IgnoreDomState::No))) { - MUST(target->hide_popover(FocusPreviousElement::Yes, FireEvents::Yes, ThrowExceptions::No, IgnoreDomState::No)); + MUST(target->hide_popover(FocusPreviousElement::Yes, FireEvents::Yes, ThrowExceptions::No, IgnoreDomState::No, this)); } } diff --git a/Libraries/LibWeb/HTML/HTMLDialogElement.cpp b/Libraries/LibWeb/HTML/HTMLDialogElement.cpp index 3c00398230d..54adc35aaa8 100644 --- a/Libraries/LibWeb/HTML/HTMLDialogElement.cpp +++ b/Libraries/LibWeb/HTML/HTMLDialogElement.cpp @@ -42,6 +42,7 @@ void HTMLDialogElement::visit_edges(JS::Cell::Visitor& visitor) Base::visit_edges(visitor); visitor.visit(m_close_watcher); + visitor.visit(m_request_close_source_element); } void HTMLDialogElement::removed_from(Node* old_parent, Node& old_root) @@ -63,7 +64,7 @@ void HTMLDialogElement::removed_from(Node* old_parent, Node& old_root) } // https://html.spec.whatwg.org/multipage/interactive-elements.html#queue-a-dialog-toggle-event-task -void HTMLDialogElement::queue_a_dialog_toggle_event_task(AK::String old_state, AK::String new_state) +void HTMLDialogElement::queue_a_dialog_toggle_event_task(AK::String old_state, AK::String new_state, GC::Ptr source) { // 1. If element's dialog toggle task tracker is not null, then: if (m_dialog_toggle_task_tracker.has_value()) { @@ -80,14 +81,14 @@ void HTMLDialogElement::queue_a_dialog_toggle_event_task(AK::String old_state, A } // 2. Queue an element task given the DOM manipulation task source and element to run the following steps: - auto task_id = queue_an_element_task(Task::Source::DOMManipulation, [this, old_state, new_state = move(new_state)]() { + auto task_id = queue_an_element_task(Task::Source::DOMManipulation, [this, old_state, new_state = move(new_state), source]() { // 1. Fire an event named toggle at element, using ToggleEvent, with the oldState attribute initialized to - // oldState and the newState attribute initialized to newState. + // oldState, the newState attribute initialized to newState, and the source attribute initialized to source. ToggleEventInit event_init {}; event_init.old_state = move(old_state); event_init.new_state = move(new_state); - dispatch_event(ToggleEvent::create(realm(), HTML::EventNames::toggle, move(event_init))); + dispatch_event(ToggleEvent::create(realm(), HTML::EventNames::toggle, move(event_init), source)); // 2. Set element's dialog toggle task tracker to null. m_dialog_toggle_task_tracker = {}; @@ -127,8 +128,8 @@ WebIDL::ExceptionOr HTMLDialogElement::show() if (has_attribute(AttributeNames::open)) return {}; - // 5. Queue a dialog toggle event task given this, "closed", and "open". - queue_a_dialog_toggle_event_task("closed"_string, "open"_string); + // 5. Queue a dialog toggle event task given this, "closed", "open", and null. + queue_a_dialog_toggle_event_task("closed"_string, "open"_string, nullptr); // 6. Add an open attribute to this, whose value is the empty string. TRY(set_attribute(AttributeNames::open, {})); @@ -169,11 +170,11 @@ WebIDL::ExceptionOr HTMLDialogElement::show() // https://html.spec.whatwg.org/multipage/interactive-elements.html#dom-dialog-showmodal WebIDL::ExceptionOr HTMLDialogElement::show_modal() { - // The showModal() method steps are to show a modal dialog given this. - return show_a_modal_dialog(*this); + // The showModal() method steps are to show a modal dialog given this and null. + return show_a_modal_dialog(*this, nullptr); } -WebIDL::ExceptionOr HTMLDialogElement::show_a_modal_dialog(HTMLDialogElement& subject) +WebIDL::ExceptionOr HTMLDialogElement::show_a_modal_dialog(HTMLDialogElement& subject, GC::Ptr source) { // To show a modal dialog given a dialog element subject: auto& realm = subject.realm(); @@ -200,13 +201,13 @@ WebIDL::ExceptionOr HTMLDialogElement::show_a_modal_dialog(HTMLDialogEleme // 6. If the result of firing an event named beforetoggle, using ToggleEvent, // with the cancelable attribute initialized to true, the oldState attribute initialized to "closed", - // and the newState attribute initialized to "open" at subject is false, then return. + // the newState attribute initialized to "open", and the source attribute initialized to source at subject is false, then return. ToggleEventInit event_init {}; event_init.cancelable = true; event_init.old_state = "closed"_string; event_init.new_state = "open"_string; - auto beforetoggle_result = subject.dispatch_event(ToggleEvent::create(realm, EventNames::beforetoggle, move(event_init))); + auto beforetoggle_result = subject.dispatch_event(ToggleEvent::create(realm, EventNames::beforetoggle, move(event_init), source)); if (!beforetoggle_result) return {}; @@ -222,8 +223,8 @@ WebIDL::ExceptionOr HTMLDialogElement::show_a_modal_dialog(HTMLDialogEleme if (subject.popover_visibility_state() == PopoverVisibilityState::Showing) return {}; - // 10. Queue a dialog toggle event task given subject, "closed", and "open". - subject.queue_a_dialog_toggle_event_task("closed"_string, "open"_string); + // 10. Queue a dialog toggle event task given subject, "closed", "open", and source. + subject.queue_a_dialog_toggle_event_task("closed"_string, "open"_string, source); // 11. Add an open attribute to subject, whose value is the empty string. TRY(subject.set_attribute(AttributeNames::open, {})); @@ -277,31 +278,38 @@ WebIDL::ExceptionOr HTMLDialogElement::show_a_modal_dialog(HTMLDialogEleme void HTMLDialogElement::close(Optional return_value) { // 1. If returnValue is not given, then set it to null. - // 2. Close the dialog this with returnValue. - close_the_dialog(move(return_value)); + // 2. Close the dialog this with returnValue and null. + close_the_dialog(move(return_value), nullptr); } // https://html.spec.whatwg.org/multipage/interactive-elements.html#dom-dialog-requestclose void HTMLDialogElement::request_close(Optional return_value) { + // AD-HOC: This method is an amalgamation of the requestClose() and "request to close" algorithms from the spec. + // FIXME: Once the "request-close" command is implemented, this will need to be split into two methods. + // For now this implementation is only used for the requestClose() method, which sets source to null. + auto source = nullptr; + // 1. If this does not have an open attribute, then return. if (!has_attribute(AttributeNames::open)) return; - // ADHOC: 2. If this's close watcher is null, then close the dialog this with returnValue, and return. See https://github.com/whatwg/html/pull/10983 + // AD-HOC: 2. If this's close watcher is null, then close the dialog this with returnValue and source, and return. See https://github.com/whatwg/html/pull/10983 if (!m_close_watcher) { - close_the_dialog(move(return_value)); + close_the_dialog(move(return_value), source); return; } // 3. Set dialog's enable close watcher for requestClose() to true. - // ADHOC: Implemented slightly differently to the spec, as the spec is unnecessarily complex. + // AD-HOC: Implemented slightly differently to the spec, as the spec is unnecessarily complex. m_close_watcher->set_enabled(true); // 4. If returnValue is not given, then set it to null. // 5. Set this's request close return value to returnValue. m_request_close_return_value = return_value; + // 6. Set subject's request close source element to source. + m_request_close_source_element = source; // 6. Request to close dialog's close watcher with false. m_close_watcher->request_close(false); // 7. Set dialog's enable close watcher for requestClose() to false. - // ADHOC: Implemented slightly differently to the spec, as the spec is unnecessarily complex. + // AD-HOC: Implemented slightly differently to the spec, as the spec is unnecessarily complex. // FIXME: This should be set based on dialog closedby state, when implemented. if (m_close_watcher) m_close_watcher->set_enabled(m_is_modal); @@ -320,25 +328,25 @@ void HTMLDialogElement::set_return_value(String return_value) } // https://html.spec.whatwg.org/multipage/interactive-elements.html#close-the-dialog -void HTMLDialogElement::close_the_dialog(Optional result) +void HTMLDialogElement::close_the_dialog(Optional result, GC::Ptr source) { // 1. If subject does not have an open attribute, then return. if (!has_attribute(AttributeNames::open)) return; - // 2. Fire an event named beforetoggle, using ToggleEvent, with the oldState attribute initialized to "open" and the newState attribute initialized to "closed" at subject. + // 2. Fire an event named beforetoggle, using ToggleEvent, with the oldState attribute initialized to "open", the newState attribute initialized to "closed", and the source attribute initialized to source at subject. ToggleEventInit event_init {}; event_init.old_state = "open"_string; event_init.new_state = "closed"_string; - dispatch_event(ToggleEvent::create(realm(), HTML::EventNames::beforetoggle, move(event_init))); + dispatch_event(ToggleEvent::create(realm(), HTML::EventNames::beforetoggle, move(event_init), source)); // 3. If subject does not have an open attribute, then return. if (!has_attribute(AttributeNames::open)) return; - // 4. Queue a dialog toggle event task given subject, "open", and "closed". - queue_a_dialog_toggle_event_task("open"_string, "closed"_string); + // 4. Queue a dialog toggle event task given subject, "open", "closed", and source. + queue_a_dialog_toggle_event_task("open"_string, "closed"_string, source); // 5. Remove subject's open attribute. remove_attribute(AttributeNames::open); @@ -355,30 +363,33 @@ void HTMLDialogElement::close_the_dialog(Optional result) // 9. Remove subject from subject's node document's open dialogs list. document().open_dialogs_list().remove_first_matching([this](auto other) { return other == this; }); - // 10. If result is not null, then set the returnValue attribute to result. + // 10. If result is not null, then set subject's return value to result. if (result.has_value()) set_return_value(result.release_value()); - // 11. Set the request close return value to null. + // 11. Set subject's request close return value to null. m_request_close_return_value = {}; - // FIXME: 12. If subject's previously focused element is not null, then: + // 12. Set subject's request close source element to null. + m_request_close_source_element = nullptr; + + // FIXME: 13. If subject's previously focused element is not null, then: // 1. Let element be subject's previously focused element. // 2. Set subject's previously focused element to null. // 3. If subject's node document's focused area of the document's DOM anchor is a shadow-including inclusive descendant of subject, // or wasModal is true, then run the focusing steps for element; the viewport should not be scrolled by doing this step. - // 13. Queue an element task on the user interaction task source given the subject element to fire an event named close at subject. + // 14. Queue an element task on the user interaction task source given the subject element to fire an event named close at subject. queue_an_element_task(HTML::Task::Source::UserInteraction, [this] { auto close_event = DOM::Event::create(realm(), HTML::EventNames::close); dispatch_event(close_event); }); - // 14. If subject's close watcher is not null, then: + // 15. If subject's close watcher is not null, then: if (m_close_watcher) { - // 14.1 Destroy subject's close watcher. + // 1. Destroy subject's close watcher. m_close_watcher->destroy(); - // 14.2 Set subject's close watcher to null. + // 2. Set subject's close watcher to null. m_close_watcher = nullptr; } } @@ -401,10 +412,10 @@ void HTMLDialogElement::set_close_watcher() 0, ""_fly_string, &realm()); auto cancel_callback = realm().heap().allocate(*cancel_callback_function, realm()); m_close_watcher->add_event_listener_without_options(HTML::EventNames::cancel, DOM::IDLEventListener::create(realm(), cancel_callback)); - // - closeAction being to close the dialog given dialog and dialog's request close return value. + // - closeAction being to close the dialog given dialog, dialog's request close return value, and dialog's request close source element. auto close_callback_function = JS::NativeFunction::create( realm(), [this](JS::VM&) { - close_the_dialog(m_request_close_return_value); + close_the_dialog(m_request_close_return_value, m_request_close_source_element); return JS::js_undefined(); }, @@ -475,19 +486,17 @@ void HTMLDialogElement::invoker_command_steps(DOM::Element& invoker, String& com return; } - // 2. If command is in the Close state and element has an open attribute: + // 2. If command is in the Close state and element has an open attribute, + // then close the dialog given element with invoker's optional value and invoker. if (command == "close" && has_attribute(AttributeNames::open)) { - // 1. Let value be invoker's value. - // FIXME: This assumes invoker is a button. - auto value = invoker.get_attribute(AttributeNames::value); - - // 2. Close the dialog element with value. - close_the_dialog(value); + // FIXME: This assumes invoker is a button. + auto optional_value = invoker.get_attribute(AttributeNames::value); + close_the_dialog(optional_value, invoker); } - // 3. If command is the Show Modal state and element does not have an open attribute, then show a modal dialog given element. + // 3. If command is the Show Modal state and element does not have an open attribute, then show a modal dialog given element and invoker. if (command == "show-modal" && !has_attribute(AttributeNames::open)) { - MUST(show_a_modal_dialog(*this)); + MUST(show_a_modal_dialog(*this, invoker)); } } diff --git a/Libraries/LibWeb/HTML/HTMLDialogElement.h b/Libraries/LibWeb/HTML/HTMLDialogElement.h index d66fce75eb5..c3fd25efdae 100644 --- a/Libraries/LibWeb/HTML/HTMLDialogElement.h +++ b/Libraries/LibWeb/HTML/HTMLDialogElement.h @@ -26,7 +26,9 @@ public: String return_value() const; void set_return_value(String); - static WebIDL::ExceptionOr show_a_modal_dialog(HTMLDialogElement&); + static WebIDL::ExceptionOr show_a_modal_dialog(HTMLDialogElement&, GC::Ptr source); + + void close_the_dialog(Optional result, GC::Ptr source); WebIDL::ExceptionOr show(); WebIDL::ExceptionOr show_modal(); @@ -50,9 +52,7 @@ private: virtual void initialize(JS::Realm&) override; virtual void visit_edges(Cell::Visitor&) override; - void queue_a_dialog_toggle_event_task(String old_state, String new_state); - - void close_the_dialog(Optional result); + void queue_a_dialog_toggle_event_task(String old_state, String new_state, GC::Ptr source); void run_dialog_focusing_steps(); @@ -63,6 +63,7 @@ private: String m_return_value; bool m_is_modal { false }; Optional m_request_close_return_value; + GC::Ptr m_request_close_source_element; GC::Ptr m_close_watcher; // https://html.spec.whatwg.org/multipage/interactive-elements.html#dialog-toggle-task-tracker diff --git a/Libraries/LibWeb/HTML/HTMLElement.cpp b/Libraries/LibWeb/HTML/HTMLElement.cpp index f4082475a82..adc04fa015b 100644 --- a/Libraries/LibWeb/HTML/HTMLElement.cpp +++ b/Libraries/LibWeb/HTML/HTMLElement.cpp @@ -693,10 +693,10 @@ void HTMLElement::attribute_changed(FlyString const& name, Optional cons // 3. If element's popover visibility state is in the showing state // and oldValue and value are in different states, - // then run the hide popover algorithm given element, true, true, false, and true. + // then run the hide popover algorithm given element, true, true, false, true, and null. if (m_popover_visibility_state == PopoverVisibilityState::Showing && popover_value_to_state(old_value) != popover_value_to_state(value)) - MUST(hide_popover(FocusPreviousElement::Yes, FireEvents::Yes, ThrowExceptions::No, IgnoreDomState::Yes)); + MUST(hide_popover(FocusPreviousElement::Yes, FireEvents::Yes, ThrowExceptions::No, IgnoreDomState::Yes, nullptr)); }(); } @@ -1211,12 +1211,12 @@ WebIDL::ExceptionOr HTMLElement::show_popover(ThrowExceptions throw_except m_popover_showing_or_hiding = false; }; - // 9. If the result of firing an event named beforetoggle, using ToggleEvent, with the cancelable attribute initialized to true, the oldState attribute initialized to "closed", and the newState attribute initialized to "open" at element is false, then run cleanupShowingFlag and return. + // 9. If the result of firing an event named beforetoggle, using ToggleEvent, with the cancelable attribute initialized to true, the oldState attribute initialized to "closed", the newState attribute initialized to "open" at element, and the source attribute initialized to invoker is false, then run cleanupShowingFlag and return. ToggleEventInit event_init {}; event_init.old_state = "closed"_string; event_init.new_state = "open"_string; event_init.cancelable = true; - if (!dispatch_event(ToggleEvent::create(realm(), HTML::EventNames::beforetoggle, move(event_init)))) { + if (!dispatch_event(ToggleEvent::create(realm(), HTML::EventNames::beforetoggle, move(event_init), invoker))) { cleanup_showing_flag(); return {}; } @@ -1354,10 +1354,10 @@ WebIDL::ExceptionOr HTMLElement::show_popover(ThrowExceptions throw_except m_popover_close_watcher = CloseWatcher::establish(*document.window()); // - cancelAction being to return true. // We simply don't add an event listener for the cancel action. - // - closeAction being to hide a popover given element, true, true, and false. + // - closeAction being to hide a popover given element, true, true, false, and null. auto close_callback_function = JS::NativeFunction::create( realm(), [this](JS::VM&) { - MUST(hide_popover(FocusPreviousElement::Yes, FireEvents::Yes, ThrowExceptions::No, IgnoreDomState::No)); + MUST(hide_popover(FocusPreviousElement::Yes, FireEvents::Yes, ThrowExceptions::No, IgnoreDomState::No, nullptr)); return JS::js_undefined(); }, @@ -1378,8 +1378,8 @@ WebIDL::ExceptionOr HTMLElement::show_popover(ThrowExceptions throw_except // FIXME: 24. Set element's implicit anchor element to invoker. // FIXME: 25. Run the popover focusing steps given element. // FIXME: 26. If shouldRestoreFocus is true and element's popover attribute is not in the no popover state, then set element's previously focused element to originallyFocusedElement. - // 27. Queue a popover toggle event task given element, "closed", and "open". - queue_a_popover_toggle_event_task("closed"_string, "open"_string); + // 27. Queue a popover toggle event task given element, "closed", "open", and invoker. + queue_a_popover_toggle_event_task("closed"_string, "open"_string, invoker); // 28. Run cleanupShowingFlag. cleanup_showing_flag(); @@ -1390,13 +1390,13 @@ WebIDL::ExceptionOr HTMLElement::show_popover(ThrowExceptions throw_except // https://whatpr.org/html/9457/popover.html#dom-hidepopover WebIDL::ExceptionOr HTMLElement::hide_popover_for_bindings() { - // The hidePopover() method steps are to run the hide popover algorithm given this, true, true, true, and false. - return hide_popover(FocusPreviousElement::Yes, FireEvents::Yes, ThrowExceptions::Yes, IgnoreDomState::No); + // The hidePopover() method steps are to run the hide popover algorithm given this, true, true, true, false, and null. + return hide_popover(FocusPreviousElement::Yes, FireEvents::Yes, ThrowExceptions::Yes, IgnoreDomState::No, nullptr); } // https://html.spec.whatwg.org/multipage/popover.html#hide-popover-algorithm // https://whatpr.org/html/9457/popover.html#hide-popover-algorithm -WebIDL::ExceptionOr HTMLElement::hide_popover(FocusPreviousElement focus_previous_element, FireEvents fire_events, ThrowExceptions throw_exceptions, IgnoreDomState ignore_dom_state) +WebIDL::ExceptionOr HTMLElement::hide_popover(FocusPreviousElement focus_previous_element, FireEvents fire_events, ThrowExceptions throw_exceptions, IgnoreDomState ignore_dom_state, GC::Ptr source) { // 1. If the result of running check popover validity given element, true, throwExceptions, null and ignoreDomState is false, then return. if (!TRY(check_popover_validity(ExpectedToBeShowing::Yes, throw_exceptions, nullptr, ignore_dom_state))) @@ -1446,11 +1446,11 @@ WebIDL::ExceptionOr HTMLElement::hide_popover(FocusPreviousElement focus_p // 9. If fireEvents is true: if (fire_events == FireEvents::Yes) { - // 9.1. Fire an event named beforetoggle, using ToggleEvent, with the oldState attribute initialized to "open" and the newState attribute initialized to "closed" at element. + // 9.1. Fire an event named beforetoggle, using ToggleEvent, with the oldState attribute initialized to "open", the newState attribute initialized to "closed", and the source attribute set to source at element. ToggleEventInit event_init {}; event_init.old_state = "open"_string; event_init.new_state = "closed"_string; - dispatch_event(ToggleEvent::create(realm(), HTML::EventNames::beforetoggle, move(event_init))); + dispatch_event(ToggleEvent::create(realm(), HTML::EventNames::beforetoggle, move(event_init), source)); // 9.2. If autoPopoverListContainsElement is true and document's showing auto popover list's last item is not element, then run hide all popovers until given element, focusPreviousElement, and false. if (auto_popover_list_contains_element && (showing_popovers.is_empty() || showing_popovers.last() != this)) @@ -1502,9 +1502,9 @@ WebIDL::ExceptionOr HTMLElement::hide_popover(FocusPreviousElement focus_p // 13. Set element's popover visibility state to hidden. m_popover_visibility_state = PopoverVisibilityState::Hidden; - // 14. If fireEvents is true, then queue a popover toggle event task given element, "open", and "closed". + // 14. If fireEvents is true, then queue a popover toggle event task given element, "open", "closed", and source. if (fire_events == FireEvents::Yes) - queue_a_popover_toggle_event_task("open"_string, "closed"_string); + queue_a_popover_toggle_event_task("open"_string, "closed"_string, source); // FIXME: 15. Let previouslyFocusedElement be element's previously focused element. @@ -1538,9 +1538,9 @@ WebIDL::ExceptionOr HTMLElement::toggle_popover(TogglePopoverOptionsOrForc invoker = options.source; }); - // 5. If this's popover visibility state is showing, and force is null or false, then run the hide popover algorithm given this, true, true, true, and false. + // 5. If this's popover visibility state is showing, and force is null or false, then run the hide popover algorithm given this, true, true, true, false, and null. if (popover_visibility_state() == PopoverVisibilityState::Showing && (!force.has_value() || !force.value())) - TRY(hide_popover(FocusPreviousElement::Yes, FireEvents::Yes, ThrowExceptions::Yes, IgnoreDomState::No)); + TRY(hide_popover(FocusPreviousElement::Yes, FireEvents::Yes, ThrowExceptions::Yes, IgnoreDomState::No, nullptr)); // 6. Otherwise, if force is not present or true, then run show popover given this true, and invoker. else if (!force.has_value() || force.value()) TRY(show_popover(ThrowExceptions::Yes, invoker)); @@ -1644,8 +1644,8 @@ void HTMLElement::hide_popover_stack_until(Vector> const& p // 1. Assert: popoverList is not empty. VERIFY(!popover_list.is_empty()); - // 2. Run the hide popover algorithm given the last item in popoverList, focusPreviousElement, fireEvents, and false. - MUST(popover_list.last()->hide_popover(focus_previous_element, fire_events, ThrowExceptions::No, IgnoreDomState::No)); + // 2. Run the hide popover algorithm given the last item in popoverList, focusPreviousElement, fireEvents, false, and null. + MUST(popover_list.last()->hide_popover(focus_previous_element, fire_events, ThrowExceptions::No, IgnoreDomState::No, nullptr)); } // 5. Assert: repeatingHide is false or popoverList's last item is endpoint. @@ -1670,8 +1670,8 @@ void HTMLElement::close_entire_popover_list(Vector> const& // FIXME: If an event handler opens a new popover then this could be an infinite loop. // 1. While popoverList is not empty: while (!popover_list.is_empty()) { - // 1. Run the hide popover algorithm given popoverList's last item, focusPreviousElement, fireEvents, and false. - MUST(popover_list.last()->hide_popover(focus_previous_element, fire_events, ThrowExceptions::No, IgnoreDomState::No)); + // 1. Run the hide popover algorithm given popoverList's last item, focusPreviousElement, fireEvents, false, and null. + MUST(popover_list.last()->hide_popover(focus_previous_element, fire_events, ThrowExceptions::No, IgnoreDomState::No, nullptr)); } } @@ -1829,7 +1829,7 @@ GC::Ptr HTMLElement::nearest_inclusive_target_popover_for_invoker() } // https://html.spec.whatwg.org/multipage/popover.html#queue-a-popover-toggle-event-task -void HTMLElement::queue_a_popover_toggle_event_task(String old_state, String new_state) +void HTMLElement::queue_a_popover_toggle_event_task(String old_state, String new_state, GC::Ptr source) { // 1. If element's popover toggle task tracker is not null, then: if (m_popover_toggle_task_tracker.has_value()) { @@ -1846,14 +1846,14 @@ void HTMLElement::queue_a_popover_toggle_event_task(String old_state, String new } // 2. Queue an element task given the DOM manipulation task source and element to run the following steps: - auto task_id = queue_an_element_task(HTML::Task::Source::DOMManipulation, [this, old_state, new_state = move(new_state)]() mutable { + auto task_id = queue_an_element_task(HTML::Task::Source::DOMManipulation, [this, old_state, new_state = move(new_state), source]() mutable { // 1. Fire an event named toggle at element, using ToggleEvent, with the oldState attribute initialized to - // oldState and the newState attribute initialized to newState. + // oldState, the newState attribute initialized to newState, and the source attribute initialized to source. ToggleEventInit event_init {}; event_init.old_state = move(old_state); event_init.new_state = move(new_state); - dispatch_event(ToggleEvent::create(realm(), HTML::EventNames::toggle, move(event_init))); + dispatch_event(ToggleEvent::create(realm(), HTML::EventNames::toggle, move(event_init), source)); // 2. Set element's popover toggle task tracker to null. m_popover_toggle_task_tracker = {}; @@ -2002,9 +2002,9 @@ void HTMLElement::removed_from(Node* old_parent, Node& old_root) Element::removed_from(old_parent, old_root); // https://whatpr.org/html/9457/infrastructure.html#dom-trees:concept-node-remove-ext - // If removedNode's popover attribute is not in the no popover state, then run the hide popover algorithm given removedNode, false, false, false, and true. + // If removedNode's popover attribute is not in the no popover state, then run the hide popover algorithm given removedNode, false, false, false, true, and null. if (popover().has_value()) - MUST(hide_popover(FocusPreviousElement::No, FireEvents::No, ThrowExceptions::No, IgnoreDomState::Yes)); + MUST(hide_popover(FocusPreviousElement::No, FireEvents::No, ThrowExceptions::No, IgnoreDomState::Yes, nullptr)); if (old_parent) { auto* parent_html_element = as_if(old_parent); diff --git a/Libraries/LibWeb/HTML/HTMLElement.h b/Libraries/LibWeb/HTML/HTMLElement.h index 609ba0f3300..bb26b36afd8 100644 --- a/Libraries/LibWeb/HTML/HTMLElement.h +++ b/Libraries/LibWeb/HTML/HTMLElement.h @@ -147,7 +147,7 @@ public: WebIDL::ExceptionOr check_popover_validity(ExpectedToBeShowing expected_to_be_showing, ThrowExceptions throw_exceptions, GC::Ptr, IgnoreDomState ignore_dom_state); WebIDL::ExceptionOr show_popover(ThrowExceptions throw_exceptions, GC::Ptr invoker); - WebIDL::ExceptionOr hide_popover(FocusPreviousElement focus_previous_element, FireEvents fire_events, ThrowExceptions throw_exceptions, IgnoreDomState ignore_dom_state); + WebIDL::ExceptionOr hide_popover(FocusPreviousElement focus_previous_element, FireEvents fire_events, ThrowExceptions throw_exceptions, IgnoreDomState ignore_dom_state, GC::Ptr source); static void hide_all_popovers_until(Variant, GC::Ptr> endpoint, FocusPreviousElement focus_previous_element, FireEvents fire_events); static GC::Ptr topmost_popover_ancestor(GC::Ptr new_popover_or_top_layer_element, Vector> const& popover_list, GC::Ptr invoker, IsPopover is_popover); @@ -199,7 +199,7 @@ private: GC::Ptr m_labels; - void queue_a_popover_toggle_event_task(String old_state, String new_state); + void queue_a_popover_toggle_event_task(String old_state, String new_state, GC::Ptr source); static Optional popover_value_to_state(Optional value); void hide_popover_stack_until(Vector> const& popover_list, FocusPreviousElement focus_previous_element, FireEvents fire_events); diff --git a/Libraries/LibWeb/HTML/HTMLFormElement.cpp b/Libraries/LibWeb/HTML/HTMLFormElement.cpp index 10f5aff1f20..e640c2e73b7 100644 --- a/Libraries/LibWeb/HTML/HTMLFormElement.cpp +++ b/Libraries/LibWeb/HTML/HTMLFormElement.cpp @@ -220,8 +220,8 @@ WebIDL::ExceptionOr HTMLFormElement::submit_form(GC::Ref subm if (!result.has_value()) result = submitter->get_attribute_value(AttributeNames::value); - // 6. Close the dialog subject with result. - subject->close(move(result)); + // 6. Close the dialog subject with result and null. + subject->close_the_dialog(move(result), nullptr); // 7. Return. return {}; diff --git a/Libraries/LibWeb/HTML/PopoverInvokerElement.cpp b/Libraries/LibWeb/HTML/PopoverInvokerElement.cpp index 8d7fd3a1f5f..d4a5f23cf91 100644 --- a/Libraries/LibWeb/HTML/PopoverInvokerElement.cpp +++ b/Libraries/LibWeb/HTML/PopoverInvokerElement.cpp @@ -61,9 +61,9 @@ void PopoverInvokerElement::popover_target_activation_behaviour(GC::Refpopover_visibility_state() == HTMLElement::PopoverVisibilityState::Hidden) return; - // 6. If popover's popover visibility state is showing, then run the hide popover algorithm given popover, true, true, false, and false. + // 6. If popover's popover visibility state is showing, then run the hide popover algorithm given popover, true, true, false, false, and node. if (popover->popover_visibility_state() == HTMLElement::PopoverVisibilityState::Showing) { - MUST(popover->hide_popover(FocusPreviousElement::Yes, FireEvents::Yes, ThrowExceptions::No, IgnoreDomState::No)); + MUST(popover->hide_popover(FocusPreviousElement::Yes, FireEvents::Yes, ThrowExceptions::No, IgnoreDomState::No, as(*node))); } // 7. Otherwise, if popover's popover visibility state is hidden and the result of running check popover validity given popover, false, false, null, and false is true, then run show popover given popover, false, and node. diff --git a/Libraries/LibWeb/HTML/ToggleEvent.cpp b/Libraries/LibWeb/HTML/ToggleEvent.cpp index 890bc849568..98d67c604c4 100644 --- a/Libraries/LibWeb/HTML/ToggleEvent.cpp +++ b/Libraries/LibWeb/HTML/ToggleEvent.cpp @@ -12,9 +12,9 @@ namespace Web::HTML { GC_DEFINE_ALLOCATOR(ToggleEvent); -GC::Ref ToggleEvent::create(JS::Realm& realm, FlyString const& event_name, ToggleEventInit event_init) +GC::Ref ToggleEvent::create(JS::Realm& realm, FlyString const& event_name, ToggleEventInit event_init, GC::Ptr source) { - return realm.create(realm, event_name, move(event_init)); + return realm.create(realm, event_name, move(event_init), source); } WebIDL::ExceptionOr> ToggleEvent::construct_impl(JS::Realm& realm, FlyString const& event_name, ToggleEventInit event_init) @@ -22,10 +22,11 @@ WebIDL::ExceptionOr> ToggleEvent::construct_impl(JS::Realm& return create(realm, event_name, move(event_init)); } -ToggleEvent::ToggleEvent(JS::Realm& realm, FlyString const& event_name, ToggleEventInit event_init) +ToggleEvent::ToggleEvent(JS::Realm& realm, FlyString const& event_name, ToggleEventInit event_init, GC::Ptr source) : DOM::Event(realm, event_name, event_init) , m_old_state(move(event_init.old_state)) , m_new_state(move(event_init.new_state)) + , m_source(source) { } @@ -35,4 +36,10 @@ void ToggleEvent::initialize(JS::Realm& realm) Base::initialize(realm); } +void ToggleEvent::visit_edges(Cell::Visitor& visitor) +{ + Base::visit_edges(visitor); + visitor.visit(m_source); +} + } diff --git a/Libraries/LibWeb/HTML/ToggleEvent.h b/Libraries/LibWeb/HTML/ToggleEvent.h index cadabcdb28e..b7240f588af 100644 --- a/Libraries/LibWeb/HTML/ToggleEvent.h +++ b/Libraries/LibWeb/HTML/ToggleEvent.h @@ -8,7 +8,9 @@ #include #include +#include #include +#include namespace Web::HTML { @@ -22,7 +24,7 @@ class ToggleEvent : public DOM::Event { GC_DECLARE_ALLOCATOR(ToggleEvent); public: - [[nodiscard]] static GC::Ref create(JS::Realm&, FlyString const& event_name, ToggleEventInit = {}); + [[nodiscard]] static GC::Ref create(JS::Realm&, FlyString const& event_name, ToggleEventInit = {}, GC::Ptr source = {}); static WebIDL::ExceptionOr> construct_impl(JS::Realm&, FlyString const& event_name, ToggleEventInit); // https://html.spec.whatwg.org/multipage/interaction.html#dom-toggleevent-oldstate @@ -31,13 +33,23 @@ public: // https://html.spec.whatwg.org/multipage/interaction.html#dom-toggleevent-newstate String const& new_state() const { return m_new_state; } + // https://html.spec.whatwg.org/multipage/interaction.html#dom-toggleevent-source + GC::Ptr source() const + { + // The source getter steps are to return the result of retargeting source against this's currentTarget. + return as(retarget(m_source, current_target())); + } + + virtual void visit_edges(Cell::Visitor&) override; + private: - ToggleEvent(JS::Realm&, FlyString const& event_name, ToggleEventInit event_init); + ToggleEvent(JS::Realm&, FlyString const& event_name, ToggleEventInit event_init, GC::Ptr source); virtual void initialize(JS::Realm&) override; String m_old_state; String m_new_state; + GC::Ptr m_source; }; } diff --git a/Libraries/LibWeb/HTML/ToggleEvent.idl b/Libraries/LibWeb/HTML/ToggleEvent.idl index 3c1fa86b989..9887d6f2bf5 100644 --- a/Libraries/LibWeb/HTML/ToggleEvent.idl +++ b/Libraries/LibWeb/HTML/ToggleEvent.idl @@ -6,6 +6,7 @@ interface ToggleEvent : Event { constructor(DOMString type, optional ToggleEventInit eventInitDict = {}); readonly attribute DOMString oldState; readonly attribute DOMString newState; + readonly attribute Element? source; }; dictionary ToggleEventInit : EventInit { diff --git a/Tests/LibWeb/Text/expected/wpt-import/html/semantics/popovers/popover-toggle-source.tentative.txt b/Tests/LibWeb/Text/expected/wpt-import/html/semantics/popovers/popover-toggle-source.tentative.txt new file mode 100644 index 00000000000..3feb95800b5 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/html/semantics/popovers/popover-toggle-source.tentative.txt @@ -0,0 +1,13 @@ +Harness status: OK + +Found 7 tests + +4 Pass +3 Fail +Pass ToggleEvent.source on popover elements: showPopover() without source. +Fail ToggleEvent.source on popover elements: showPopover() with source. +Pass ToggleEvent.source on popover elements: Calling click() on a popovertarget button. +Fail ToggleEvent.source on popover elements: Calling click() on a command button. +Pass ToggleEvent.source on popover elements: showPopover() then popovertarget button. +Fail ToggleEvent.source on popover elements: showPopover(invoker) then popovertarget button. +Pass ToggleEvent.source on popover elements: popovertarget button then hidePopover(). \ No newline at end of file diff --git a/Tests/LibWeb/Text/input/wpt-import/html/semantics/popovers/popover-toggle-source.tentative.html b/Tests/LibWeb/Text/input/wpt-import/html/semantics/popovers/popover-toggle-source.tentative.html new file mode 100644 index 00000000000..75c976c9e80 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/html/semantics/popovers/popover-toggle-source.tentative.html @@ -0,0 +1,90 @@ + + + + + + + + + +
+ popover + + +
+ + diff --git a/Tests/LibWeb/Text/input/wpt-import/html/semantics/popovers/resources/toggle-event-source-test.js b/Tests/LibWeb/Text/input/wpt-import/html/semantics/popovers/resources/toggle-event-source-test.js new file mode 100644 index 00000000000..93ecd270fac --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/html/semantics/popovers/resources/toggle-event-source-test.js @@ -0,0 +1,76 @@ +function createToggleEventSourceTest({ + description, + target, + openFunc, + closeFunc, + openSource, + closeSource, + skipBeforetoggle}) { + promise_test(async () => { + let beforetoggleEvent = null; + let beforetoggleDuplicate = false; + let toggleEvent = null; + let toggleDuplicate = false; + target.addEventListener('beforetoggle', event => { + if (beforetoggleEvent) { + beforetoggleDuplicate = true; + } + beforetoggleEvent = event; + }); + target.addEventListener('toggle', event => { + if (toggleEvent) { + toggleDuplicate = true; + } + toggleEvent = event; + }); + + await openFunc(); + await new Promise(requestAnimationFrame); + await new Promise(requestAnimationFrame); + if (!skipBeforetoggle) { + assert_true(!!beforetoggleEvent, + 'An opening beforetoggle event should have been fired.'); + assert_false(beforetoggleDuplicate, + 'Only one opening beforetoggle event should have been fired.'); + assert_equals(beforetoggleEvent.newState, 'open', + 'beforetoggle newState should be open.'); + assert_equals(beforetoggleEvent.source, openSource, + 'Opening beforetoggle.source.'); + } + assert_true(!!toggleEvent, + 'An opening toggle event should have been fired.'); + assert_false(toggleDuplicate, + 'Only one opening toggle event should have been fired.'); + assert_equals(toggleEvent.newState, 'open', + 'toggle newstate should be open.'); + assert_equals(toggleEvent.source, openSource, + 'Opening toggle.source.'); + beforetoggleEvent = null; + beforetoggleDuplicate = false; + toggleEvent = null; + toggleDuplicate = false; + + await closeFunc(); + await new Promise(requestAnimationFrame); + await new Promise(requestAnimationFrame); + + if (!skipBeforetoggle) { + assert_true(!!beforetoggleEvent, + 'A closing beforetoggle event should have been fired.'); + assert_false(beforetoggleDuplicate, + 'Only one closing beforetoggle event should have been fired.'); + assert_equals(beforetoggleEvent.newState, 'closed', + 'beforetoggle newState should be closed.'); + assert_equals(beforetoggleEvent.source, closeSource, + 'Closing beforetoggle.source.'); + } + assert_true(!!toggleEvent, + 'A closing toggle event should have been fired.'); + assert_false(toggleDuplicate, + 'Only one closing toggle event should have been fired.'); + assert_equals(toggleEvent.newState, 'closed', + 'toggle newstate should be closed.'); + assert_equals(toggleEvent.source, closeSource, + 'Closing toggle.source.'); + }, description); +}