mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-07-30 12:49:19 +00:00
LibWeb: Implement popover beforetoggle and toggle events
This commit is contained in:
parent
872a6a11a4
commit
fcf6cc27f2
Notes:
github-actions[bot]
2024-12-12 22:11:32 +00:00
Author: https://github.com/lukewarlow
Commit: fcf6cc27f2
Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/346
Reviewed-by: https://github.com/ADKaster ✅
7 changed files with 371 additions and 11 deletions
|
@ -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))
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -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.
|
|
@ -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
|
|
@ -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>
|
|
@ -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>
|
Loading…
Add table
Add a link
Reference in a new issue