LibWeb: Implement popover beforetoggle and toggle events

This commit is contained in:
Luke Warlow 2024-12-06 23:10:03 +00:00 committed by Andrew Kaster
commit fcf6cc27f2
Notes: github-actions[bot] 2024-12-12 22:11:32 +00:00
7 changed files with 371 additions and 11 deletions

View file

@ -28,6 +28,7 @@
#include <LibWeb/HTML/HTMLElement.h>
#include <LibWeb/HTML/HTMLLabelElement.h>
#include <LibWeb/HTML/HTMLParagraphElement.h>
#include <LibWeb/HTML/ToggleEvent.h>
#include <LibWeb/HTML/Window.h>
#include <LibWeb/Infra/CharacterTypes.h>
#include <LibWeb/Infra/Strings.h>
@ -1016,9 +1017,21 @@ WebIDL::ExceptionOr<void> HTMLElement::show_popover(ThrowExceptions throw_except
m_popover_showing_or_hiding = false;
};
// FIXME: 8. 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.
// 8. 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.
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)))) {
cleanup_showing_flag->function()();
return {};
}
// FIXME: 9. If the result of running check popover validity given element, false, throwExceptions, and document is false, then run cleanupShowingFlag and return.
// 9. If the result of running check popover validity given element, false, throwExceptions, and document is false, then run cleanupShowingFlag and return.
if (!TRY(check_popover_validity(ExpectedToBeShowing::No, throw_exceptions, nullptr))) {
cleanup_showing_flag->function()();
return {};
}
// 10. Let shouldRestoreFocus be false.
bool should_restore_focus = false;
@ -1056,7 +1069,9 @@ WebIDL::ExceptionOr<void> HTMLElement::show_popover(ThrowExceptions throw_except
// FIXME: then set element's previously focused element to originallyFocusedElement.
}
// FIXME: 20. Queue a popover toggle event task given element, "closed", and "open".
// 20. Queue a popover toggle event task given element, "closed", and "open".
queue_a_popover_toggle_event_task("closed"_string, "open"_string);
// 21. Run cleanupShowingFlag.
cleanup_showing_flag();
@ -1112,9 +1127,19 @@ WebIDL::ExceptionOr<void> HTMLElement::hide_popover(FocusPreviousElement, FireEv
// 10. If fireEvents is true:
if (fire_events == FireEvents::Yes) {
// FIXME: 10.1. Fire an event named beforetoggle, using ToggleEvent, with the oldState attribute initialized to "open" and the newState attribute initialized to "closed" at element.
// 10.1. Fire an event named beforetoggle, using ToggleEvent, with the oldState attribute initialized to "open" and the newState attribute initialized to "closed" 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)));
// FIXME: 10.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.
// FIXME: 10.3. If the result of running check popover validity given element, true, throwExceptions, and null is false, then run cleanupSteps and return.
// 10.3. If the result of running check popover validity given element, true, throwExceptions, and null is false, then run cleanupSteps and return.
if (!TRY(check_popover_validity(ExpectedToBeShowing::Yes, throw_exceptions, nullptr))) {
cleanup_steps->function()();
return {};
}
// 10.4. Request an element to be removed from the top layer given element.
document.request_an_element_to_be_remove_from_the_top_layer(*this);
} else {
@ -1125,7 +1150,9 @@ WebIDL::ExceptionOr<void> HTMLElement::hide_popover(FocusPreviousElement, FireEv
// 12. Set element's popover visibility state to hidden.
m_popover_visibility_state = PopoverVisibilityState::Hidden;
// FIXME: 13. If fireEvents is true, then queue a popover toggle event task given element, "open", and "closed".
// 13. If fireEvents is true, then queue a popover toggle event task given element, "open", and "closed".
if (fire_events == FireEvents::Yes)
queue_a_popover_toggle_event_task("open"_string, "closed"_string);
// FIXME: 14. Let previouslyFocusedElement be element's previously focused element.
@ -1175,6 +1202,44 @@ WebIDL::ExceptionOr<bool> HTMLElement::toggle_popover(TogglePopoverOptionsOrForc
return popover_visibility_state() == PopoverVisibilityState::Showing;
}
// 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)
{
// 1. If element's popover toggle task tracker is not null, then:
if (m_popover_toggle_task_tracker.has_value()) {
// 1. Set oldState to element's popover toggle task tracker's old state.
old_state = move(m_popover_toggle_task_tracker->old_state);
// 2. Remove element's popover toggle task tracker's task from its task queue.
HTML::main_thread_event_loop().task_queue().remove_tasks_matching([&](auto const& task) {
return task.id() == m_popover_toggle_task_tracker->task_id;
});
// 3. Set element's popover toggle task tracker to null.
m_popover_toggle_task_tracker->task_id = {};
}
// 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 {
// 1. Fire an event named toggle at element, using ToggleEvent, with the oldState attribute initialized to
// oldState and the newState attribute initialized to newState.
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)));
// 2. Set element's popover toggle task tracker to null.
m_popover_toggle_task_tracker = {};
});
// 3. Set element's popover toggle task tracker to a struct with task set to the just-queued task and old state set to oldState.
m_popover_toggle_task_tracker = ToggleTaskTracker {
.task_id = task_id,
.old_state = move(old_state),
};
}
void HTMLElement::did_receive_focus()
{
if (!first_is_one_of(m_content_editable_state, ContentEditableState::True, ContentEditableState::PlaintextOnly))

View file

@ -6,10 +6,12 @@
#pragma once
#include <AK/Optional.h>
#include <LibWeb/DOM/Element.h>
#include <LibWeb/HTML/EventNames.h>
#include <LibWeb/HTML/GlobalEventHandlers.h>
#include <LibWeb/HTML/HTMLOrSVGElement.h>
#include <LibWeb/HTML/ToggleTaskTracker.h>
#include <LibWeb/HTML/TokenizedFeatures.h>
namespace Web::HTML {
@ -127,8 +129,6 @@ public:
WebIDL::ExceptionOr<void> show_popover(ThrowExceptions throw_exceptions, GC::Ptr<HTMLElement> invoker);
WebIDL::ExceptionOr<void> hide_popover(FocusPreviousElement focus_previous_element, FireEvents fire_events, ThrowExceptions throw_exceptions);
WebIDL::ExceptionOr<bool> check_popover_validity(ExpectedToBeShowing expected_to_be_showing, ThrowExceptions throw_exceptions, GC::Ptr<DOM::Document>);
protected:
HTMLElement(DOM::Document&, DOM::QualifiedName);
@ -155,6 +155,10 @@ private:
GC::Ptr<DOM::NodeList> m_labels;
WebIDL::ExceptionOr<bool> check_popover_validity(ExpectedToBeShowing expected_to_be_showing, ThrowExceptions throw_exceptions, GC::Ptr<DOM::Document>);
void queue_a_popover_toggle_event_task(String old_state, String new_state);
// https://html.spec.whatwg.org/multipage/custom-elements.html#attached-internals
GC::Ptr<ElementInternals> m_attached_internals;
@ -174,6 +178,9 @@ private:
// https://html.spec.whatwg.org/multipage/popover.html#popover-showing-or-hiding
bool m_popover_showing_or_hiding { false };
// https://html.spec.whatwg.org/multipage/popover.html#the-popover-attribute:toggle-task-tracker
Optional<ToggleTaskTracker> m_popover_toggle_task_tracker;
};
}

View file

@ -0,0 +1,6 @@
Harness status: OK
Found 1 tests
1 Pass
Pass Ensure the `beforetoggle` event can be used to populate content before the popover renders

View file

@ -2,8 +2,7 @@ Harness status: OK
Found 3 tests
2 Pass
1 Fail
3 Pass
Pass togglePopover should toggle the popover and return true or false as specified.
Fail togglePopover's return value should reflect what the end state is, not just the force parameter.
Pass togglePopover's return value should reflect what the end state is, not just the force parameter.
Pass togglePopover should throw an exception when there is no popover attribute.

View file

@ -0,0 +1,42 @@
Harness status: OK
Found 37 tests
37 Pass
Pass the event is an instance of ToggleEvent
Pass the event inherts from Event
Pass Missing type argument
Pass type argument is string
Pass type argument is null
Pass event type set to undefined
Pass oldState has default value of empty string
Pass oldState is readonly
Pass newState has default value of empty string
Pass newState is readonly
Pass ToggleEventInit argument is null
Pass ToggleEventInit argument is undefined
Pass ToggleEventInit argument is empty dictionary
Pass oldState set to 'sample'
Pass oldState set to undefined
Pass oldState set to null
Pass oldState set to false
Pass oldState set to true
Pass oldState set to a number
Pass oldState set to []
Pass oldState set to [1, 2, 3]
Pass oldState set to an object
Pass oldState set to an object with a valueOf function
Pass ToggleEventInit properties set value
Pass ToggleEventInit properties set value 2
Pass ToggleEventInit properties set value 3
Pass ToggleEventInit properties set value 4
Pass newState set to 'sample'
Pass newState set to undefined
Pass newState set to null
Pass newState set to false
Pass newState set to true
Pass newState set to a number
Pass newState set to []
Pass newState set to [1, 2, 3]
Pass newState set to an object
Pass newState set to an object with a valueOf function

View file

@ -0,0 +1,33 @@
<!DOCTYPE html>
<meta charset="utf-8" />
<title>Popover beforetoggle event</title>
<link rel="author" href="mailto:masonf@chromium.org">
<link rel=help href="https://open-ui.org/components/popover.research.explainer">
<link rel=help href="https://html.spec.whatwg.org/multipage/popover.html">
<script src="../../../resources/testharness.js"></script>
<script src="../../../resources/testharnessreport.js"></script>
<div popover></div>
<script>
test(() => {
let frameCount = 0;
requestAnimationFrame(() => {++frameCount;});
const popover = document.querySelector('[popover]');
const testText = 'Show Event Occurred';
popover.addEventListener('beforetoggle',(e) => {
assert_false(e.bubbles, 'beforetoggle event does not bubble');
if (e.newState !== "open")
return;
popover.textContent = testText;
})
popover.offsetHeight;
assert_equals(popover.textContent,"");
assert_equals(frameCount,0);
popover.showPopover();
popover.offsetHeight;
assert_equals(popover.textContent,testText);
assert_equals(frameCount,0,'nothing should be rendered before the popover is updated');
popover.hidePopover(); // Cleanup
},'Ensure the `beforetoggle` event can be used to populate content before the popover renders');
</script>

View file

@ -0,0 +1,208 @@
<!DOCTYPE html>
<meta charset="utf-8">
<link rel="author" href="mailto:masonf@chromium.org">
<link rel=help href="https://open-ui.org/components/popover.research.explainer">
<link rel=help href="https://html.spec.whatwg.org/multipage/popover.html">
<script src="../../../resources/testharness.js"></script>
<script src="../../../resources/testharnessreport.js"></script>
<script>
test(function() {
var event = new ToggleEvent("");
assert_true(event instanceof window.ToggleEvent);
}, "the event is an instance of ToggleEvent");
test(function() {
var event = new ToggleEvent("");
assert_true(event instanceof window.Event);
}, "the event inherts from Event");
test(function() {
assert_throws_js(TypeError, function() {
new ToggleEvent();
}, 'First argument (type) is required, so was expecting a TypeError.');
}, 'Missing type argument');
test(function() {
var event = new ToggleEvent("test");
assert_equals(event.type, "test");
}, "type argument is string");
test(function() {
var event = new ToggleEvent(null);
assert_equals(event.type, "null");
}, "type argument is null");
test(function() {
var event = new ToggleEvent(undefined);
assert_equals(event.type, "undefined");
}, "event type set to undefined");
test(function() {
var event = new ToggleEvent("test");
assert_equals(event.oldState, "");
}, "oldState has default value of empty string");
test(function() {
var event = new ToggleEvent("test");
assert_readonly(event, "oldState", "readonly attribute value");
}, "oldState is readonly");
test(function() {
var event = new ToggleEvent("test");
assert_equals(event.newState, "");
}, "newState has default value of empty string");
test(function() {
var event = new ToggleEvent("test");
assert_readonly(event, "newState", "readonly attribute value");
}, "newState is readonly");
test(function() {
var event = new ToggleEvent("test", null);
assert_equals(event.oldState, "");
assert_equals(event.newState, "");
}, "ToggleEventInit argument is null");
test(function() {
var event = new ToggleEvent("test", undefined);
assert_equals(event.oldState, "");
assert_equals(event.newState, "");
}, "ToggleEventInit argument is undefined");
test(function() {
var event = new ToggleEvent("test", {});
assert_equals(event.oldState, "");
assert_equals(event.newState, "");
}, "ToggleEventInit argument is empty dictionary");
test(function() {
var event = new ToggleEvent("test", {oldState: "sample"});
assert_equals(event.oldState, "sample");
}, "oldState set to 'sample'");
test(function() {
var event = new ToggleEvent("test", {oldState: undefined});
assert_equals(event.oldState, "");
}, "oldState set to undefined");
test(function() {
var event = new ToggleEvent("test", {oldState: null});
assert_equals(event.oldState, "null");
}, "oldState set to null");
test(function() {
var event = new ToggleEvent("test", {oldState: false});
assert_equals(event.oldState, "false");
}, "oldState set to false");
test(function() {
var event = new ToggleEvent("test", {oldState: true});
assert_equals(event.oldState, "true");
}, "oldState set to true");
test(function() {
var event = new ToggleEvent("test", {oldState: 0.5});
assert_equals(event.oldState, "0.5");
}, "oldState set to a number");
test(function() {
var event = new ToggleEvent("test", {oldState: []});
assert_equals(event.oldState, "");
}, "oldState set to []");
test(function() {
var event = new ToggleEvent("test", {oldState: [1, 2, 3]});
assert_equals(event.oldState, "1,2,3");
}, "oldState set to [1, 2, 3]");
test(function() {
var event = new ToggleEvent("test", {oldState: {sample: 0.5}});
assert_equals(event.oldState, "[object Object]");
}, "oldState set to an object");
test(function() {
var event = new ToggleEvent("test",
{oldState: {valueOf: function () { return 'sample'; }}});
assert_equals(event.oldState, "[object Object]");
}, "oldState set to an object with a valueOf function");
test(function() {
var eventInit = {oldState: "sample",newState: "sample2"};
var event = new ToggleEvent("test", eventInit);
assert_equals(event.oldState, "sample");
assert_equals(event.newState, "sample2");
}, "ToggleEventInit properties set value");
test(function() {
var eventInit = {oldState: "open",newState: "closed"};
var event = new ToggleEvent("beforetoggle", eventInit);
assert_equals(event.oldState, "open");
assert_equals(event.newState, "closed");
}, "ToggleEventInit properties set value 2");
test(function() {
var eventInit = {oldState: "closed",newState: "open"};
var event = new ToggleEvent("toggle", eventInit);
assert_equals(event.oldState, "closed");
assert_equals(event.newState, "open");
}, "ToggleEventInit properties set value 3");
test(function() {
var eventInit = {oldState: "open",newState: "open"};
var event = new ToggleEvent("beforetoggle", eventInit);
assert_equals(event.oldState, "open");
assert_equals(event.newState, "open");
}, "ToggleEventInit properties set value 4");
test(function() {
var event = new ToggleEvent("test", {newState: "sample"});
assert_equals(event.newState, "sample");
}, "newState set to 'sample'");
test(function() {
var event = new ToggleEvent("test", {newState: undefined});
assert_equals(event.newState, "");
}, "newState set to undefined");
test(function() {
var event = new ToggleEvent("test", {newState: null});
assert_equals(event.newState, "null");
}, "newState set to null");
test(function() {
var event = new ToggleEvent("test", {newState: false});
assert_equals(event.newState, "false");
}, "newState set to false");
test(function() {
var event = new ToggleEvent("test", {newState: true});
assert_equals(event.newState, "true");
}, "newState set to true");
test(function() {
var event = new ToggleEvent("test", {newState: 0.5});
assert_equals(event.newState, "0.5");
}, "newState set to a number");
test(function() {
var event = new ToggleEvent("test", {newState: []});
assert_equals(event.newState, "");
}, "newState set to []");
test(function() {
var event = new ToggleEvent("test", {newState: [1, 2, 3]});
assert_equals(event.newState, "1,2,3");
}, "newState set to [1, 2, 3]");
test(function() {
var event = new ToggleEvent("test", {newState: {sample: 0.5}});
assert_equals(event.newState, "[object Object]");
}, "newState set to an object");
test(function() {
var event = new ToggleEvent("test",
{newState: {valueOf: function () { return 'sample'; }}});
assert_equals(event.newState, "[object Object]");
}, "newState set to an object with a valueOf function");
</script>