LibWeb: Clean up the element fullscreen APIs a bit

* Don't publicly expose methods that are only used internally.

* Modernize comment style (wrap at 120 chars, use NB for our notes).

* Change the stringifier for RequestFullscreenError to return a UTF-16
  string. We were allocating a String, then transcoding it to UTF-16.

* Give helper methods clearer names, e.g.:

  fullscreen_has_error_check -> is_element_allowed_to_enter_fullscreen
This commit is contained in:
Timothy Flynn 2026-02-28 12:06:27 -05:00 committed by Gregory Bertilson
parent 07d1b222c7
commit 2282636f98
4 changed files with 137 additions and 136 deletions

View file

@ -6851,27 +6851,25 @@ void Document::append_pending_fullscreen_change(PendingFullscreenEvent::Type typ
// https://fullscreen.spec.whatwg.org/#fullscreen-an-element
void Document::fullscreen_element_within_doc(GC::Ref<Element> element)
{
// FIXME: Spec issue: Finding topmost popover ancestor algorithm takes different parameters than those described
// by the fullscreen spec. Since the new algorithm takes 4 parameters, with the new "popover list", we must
// also account for the auto popover list.
// See: https://github.com/whatwg/fullscreen/issues/245
auto const get_hide_until = [&](auto const& popover_list) {
return HTML::HTMLElement::topmost_popover_ancestor(element, popover_list, nullptr, HTML::IsPopover::No);
};
// 1. Let hideUntil be the result of running topmost popover ancestor given
// element, null, and false.
// 1. Let hideUntil be the result of running topmost popover ancestor given element, null, and false.
auto hide_until = get_hide_until(showing_hint_popover_list());
// Finding topmost popover ancestor algorithm takes different parameters than those
// described by the fullscreen spec. Since the new algorithm takes 4 parameters, with the new "popover list"
// we must also account for the auto popover list.
// More can be read about this "spec bug" in https://github.com/whatwg/fullscreen/issues/245
if (hide_until == nullptr)
hide_until = get_hide_until(showing_auto_popover_list());
// Our hide_all_popovers_until takes a variant. topmost_popover_ancestor produces a Ptr<HTMLElement>
Variant<GC::Ptr<HTML::HTMLElement>, GC::Ptr<Document>> hide_until_argument { hide_until };
// 2. If hideUntil is null, then set hideUntil to elements node document.
if (hide_until == nullptr)
hide_until_argument = element->owner_document();
hide_until_argument = GC::Ptr { element->document() };
// 3. Run hide all popovers until given hideUntil, false, and true.
HTML::HTMLElement::hide_all_popovers_until(hide_until_argument, HTML::FocusPreviousElement::No, HTML::FireEvents::Yes);
@ -6902,23 +6900,22 @@ GC::Ptr<Element> Document::fullscreen_element() const
// https://fullscreen.spec.whatwg.org/#dom-document-fullscreenelement
GC::Ptr<Element> Document::fullscreen_element_for_bindings() const
{
GC::Ptr<Element> fullscreen_element = this->fullscreen_element();
if (!fullscreen_element) {
auto fullscreen_element = this->fullscreen_element();
if (!fullscreen_element)
return nullptr;
}
// 1. If this is a shadow root and its host is not connected, then return null.
// Note: We're not a shadow root. See ShadowRoot::fullscreen_element_for_bindings() instead.
// NB: We're not a shadow root. See ShadowRoot::fullscreen_element_for_bindings().
// 2. Let candidate be the result of retargeting fullscreen element against this.
auto* candidate = retarget(fullscreen_element.ptr(), const_cast<Document*>(this));
if (!candidate) {
auto* candidate = retarget(fullscreen_element, const_cast<Document*>(this));
if (!candidate)
return nullptr;
}
// 3. If candidate and this are in the same tree, then return candidate.
if (auto* retargeted_element = as<Element>(candidate); retargeted_element && &retargeted_element->root() == &root()) {
if (auto* retargeted_element = as<Element>(candidate); retargeted_element && &retargeted_element->root() == &root())
return retargeted_element;
}
// 4. Return null.
return nullptr;
}
@ -6952,17 +6949,12 @@ void Document::fully_exit_fullscreen()
fullscreen_elements.append(element);
}
for (auto const& element : fullscreen_elements) {
for (auto const& element : fullscreen_elements)
unfullscreen_element(element);
}
// 3. Exit fullscreen document.
// Note/FIXME: Because Document::destroy() does not "Assert: this is running as part of a task queued on document's relevant agent's event loop." and doesn't seem
// to have any temporary execution context while it's running (because it's making exit_fullscreen crash), we check if there's an execution context
// if not, it means this is running via the "run_unloading_cleanup_steps" and thus we must first add a context.
HTML::TemporaryExecutionContext context(realm(), HTML::TemporaryExecutionContext::CallbacksEnabled::Yes);
(void)exit_fullscreen();
HTML::TemporaryExecutionContext context { realm(), HTML::TemporaryExecutionContext::CallbacksEnabled::Yes };
exit_fullscreen();
}
// https://fullscreen.spec.whatwg.org/#exit-fullscreen
@ -6973,7 +6965,8 @@ GC::Ref<WebIDL::Promise> Document::exit_fullscreen()
// 1. Let promise be a new promise.
auto promise = WebIDL::create_promise(realm);
// 2. If doc is not fully active or docs fullscreen element is null, then reject promise with a TypeError exception and return promise.
// 2. If doc is not fully active or docs fullscreen element is null, then reject promise with a TypeError exception
// and return promise.
if (!is_fully_active() || !fullscreen_element()) {
WebIDL::reject_promise(realm, promise, JS::TypeError::create(realm, "Document not fully active or no fullscreen element."sv));
return promise;
@ -7008,6 +7001,7 @@ GC::Ref<WebIDL::Promise> Document::exit_fullscreen()
Platform::EventLoopPlugin::the().deferred_invoke(GC::create_function(heap(), [&realm, document_to_unfullscreen, promise, resize] {
HTML::TemporaryExecutionContext context(realm, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes);
// FIXME: 9. Run the fully unlock the screen orientation steps with doc.
// 10. If resize is true, resize docs viewport to its "normal" dimensions.
// NB: Fullscreen API is affected by site-isolation and will require additional work once site-isolation is implemented.
if (resize)
@ -7022,7 +7016,8 @@ GC::Ref<WebIDL::Promise> Document::exit_fullscreen()
// 12. Let exitDocs be the result of collecting documents to unfullscreen given doc.
auto exit_docs = document_to_unfullscreen->collect_documents_to_unfullscreen();
// 13. Let descendantDocs be an ordered set consisting of docs descendant navigables' active documents whose fullscreen element is non-null, if any, in tree order.
// 13. Let descendantDocs be an ordered set consisting of docs descendant navigables' active documents whose
// fullscreen element is non-null, if any, in tree order.
auto descendant_docs = realm.heap().allocate<GC::HeapVector<GC::Ref<Document>>>();
for (auto& descendant : document_to_unfullscreen->descendant_navigables()) {
if (descendant->active_document()->fullscreen_element())
@ -7034,13 +7029,12 @@ GC::Ref<WebIDL::Promise> Document::exit_fullscreen()
// 1. Append (fullscreenchange, exitDocs fullscreen element) to exitDocs list of pending fullscreen events.
exit_doc->append_pending_fullscreen_change(PendingFullscreenEvent::Type::Change, *exit_doc->fullscreen_element());
if (resize) {
// 2. If resize is true, unfullscreen exitDoc.
// 2. If resize is true, unfullscreen exitDoc.
if (resize)
exit_doc->unfullscreen();
} else {
// 3. Otherwise, unfullscreen exitDocs fullscreen element.
// 3. Otherwise, unfullscreen exitDocs fullscreen element.
else
exit_doc->unfullscreen_element(*exit_doc->fullscreen_element());
}
}
// 15. For each descendantDoc in descendantDocs:
@ -7052,10 +7046,13 @@ GC::Ref<WebIDL::Promise> Document::exit_fullscreen()
descendant_doc->unfullscreen();
}
// NOTE: The order in which documents are unfullscreened is not observable, because run the fullscreen steps is invoked in tree order.
// Note: The order in which documents are unfullscreened is not observable, because run the fullscreen steps is
// invoked in tree order.
// 16. Resolve promise with undefined.
WebIDL::resolve_promise(realm, promise, JS::js_undefined());
}));
return promise;
}
@ -7132,7 +7129,8 @@ GC::Ref<GC::HeapVector<GC::Ref<Document>>> Document::collect_documents_to_unfull
// https://fullscreen.spec.whatwg.org/#unfullscreen-an-element
void Document::unfullscreen_element(GC::Ref<Element> element)
{
// To unfullscreen an element, unset elements fullscreen flag and iframe fullscreen flag (if any), and remove from the top layer immediately given element.
// To unfullscreen an element, unset elements fullscreen flag and iframe fullscreen flag (if any), and remove from
// the top layer immediately given element.
element->set_fullscreen_flag(false);
if (auto* iframe_element = as_if<HTML::HTMLIFrameElement>(element.ptr()))
iframe_element->set_iframe_fullscreen_flag(false);

View file

@ -1587,7 +1587,7 @@ void Element::removed_from(Node* old_parent, Node& old_root)
}
play_or_cancel_animations_after_display_property_change();
removing_steps_fullscreen();
exit_fullscreen_on_element_removal();
}
void Element::moved_from(GC::Ptr<Node> old_parent)
@ -2432,81 +2432,6 @@ WebIDL::ExceptionOr<void> Element::insert_adjacent_html(String const& position,
return {};
}
// Used to signal what message should be shown when the promise for the algorithm rejects..
enum class RequestFullscreenError : u8 {
False,
ElementReadyCheckFailed,
UnsupportedElement,
NoTransientUserActivation,
ElementNodeDocIsNotPendingDoc
};
static constexpr String to_string(RequestFullscreenError error)
{
switch (error) {
// This should never be called with this value
case RequestFullscreenError::False:
return "false"_string;
case RequestFullscreenError::ElementReadyCheckFailed:
return "Element ready check failed"_string;
case RequestFullscreenError::UnsupportedElement:
return "Not supported element"_string;
case RequestFullscreenError::NoTransientUserActivation:
return "No transient user activation available to consume"_string;
case RequestFullscreenError::ElementNodeDocIsNotPendingDoc:
return "Element's node document is not pending doc"_string;
}
VERIFY_NOT_REACHED();
}
// step 5 of requestFullscreen:
// 5. If any of conditions are false, set error to true
static RequestFullscreenError fullscreen_has_error_check(Element const& element)
{
// Thiss namespace is the HTML namespace or this is an SVG svg or MathML math element. [SVG] [MATHML]
// FIXME: This likely wants to use is<MathML::MathMLMathElement> instead.
if (!(element.namespace_uri() == Namespace::HTML || element.is_svg_svg_element() || (is<MathML::MathMLElement>(element) && element.tag_name() == MathML::TagNames::math)))
return RequestFullscreenError::UnsupportedElement;
// This is not a dialog element
if (is<HTML::HTMLDialogElement>(element))
return RequestFullscreenError::UnsupportedElement;
// The fullscreen element ready check for this returns true.
if (!element.element_ready_check())
return RequestFullscreenError::ElementReadyCheckFailed;
// FIXME: Implement 'Fullscreen is supported.' check
// Thiss relevant global object has transient activation or
// FIXME: the algorithm is triggered by a user generated orientation change.
auto* window = as<HTML::Window>(&HTML::relevant_global_object(element));
if (!window->has_transient_activation())
return RequestFullscreenError::NoTransientUserActivation;
return RequestFullscreenError::False;
}
// https://fullscreen.spec.whatwg.org/#fullscreen-element-ready-check
bool Element::element_ready_check() const
{
// A fullscreen element ready check for an element element returns true if all of the following are true, and false otherwise:
// element is connected.
if (!is_connected())
return false;
// elements node document is allowed to use the "fullscreen" feature.
if (!m_document->is_allowed_to_use_feature(PolicyControlledFeature::Fullscreen))
return false;
// element namespace is not the HTML namespace or elements popover visibility state is hidden.
if (namespace_uri() != Namespace::HTML)
return true;
auto const* html_element = as_if<HTML::HTMLElement>(this);
return html_element ? (html_element->popover_visibility_state() == HTML::HTMLElement::PopoverVisibilityState::Hidden) : false;
}
// https://fullscreen.spec.whatwg.org/#dom-element-requestfullscreen
GC::Ref<WebIDL::Promise> Element::request_fullscreen()
{
@ -2526,7 +2451,7 @@ GC::Ref<WebIDL::Promise> Element::request_fullscreen()
// 4. Let error be false.
// 5. If any of conditions are false, set error to true
auto error = fullscreen_has_error_check(*this);
auto error = is_element_allowed_to_enter_fullscreen();
// 6. If error is false, then consume user activation given pendingDocs relevant global object.
if (error == RequestFullscreenError::False) {
@ -2537,27 +2462,29 @@ GC::Ref<WebIDL::Promise> Element::request_fullscreen()
// 7. Return promise, and run the remaining steps in parallel.
Platform::EventLoopPlugin::the().deferred_invoke(GC::create_function(heap(), [&realm, error, pending_doc, requesting_element = GC::Ref { *this }, promise]() mutable {
HTML::TemporaryExecutionContext context(realm, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes);
// N.B: Fullscreen API is affected by site-isolation and will require additional work once site-isolation is implemented.
// NB: Fullscreen API is affected by site-isolation and will require additional work once site-isolation is implemented.
// 8. If error is false, then resize pendingDocs node navigables top-level traversables
// active documents viewports dimensions FIXME: optionally taking into account options["navigationUI"]:
// 8. If error is false, then resize pendingDocs node navigables top-level traversables active documents
// viewports dimensions FIXME: optionally taking into account options["navigationUI"]:
if (error == RequestFullscreenError::False)
pending_doc->page().client().page_did_request_fullscreen_window();
// 9. If any of the following conditions are false, then set error to true:
// Thiss node document is pendingDoc.
// The fullscreen element ready check for this returns true.
// * Thiss node document is pendingDoc.
// * The fullscreen element ready check for this returns true.
if (pending_doc != requesting_element->owner_document())
error = RequestFullscreenError::ElementNodeDocIsNotPendingDoc;
if (!requesting_element->element_ready_check())
if (!requesting_element->is_element_ready_for_fullscreen())
error = RequestFullscreenError::ElementReadyCheckFailed;
// 10. If error is true:
// Append (fullscreenerror, this) to pendingDocs list of pending fullscreen events.
// Reject promise with a TypeError exception and terminate these steps.
if (error != RequestFullscreenError::False) {
// 1. Append (fullscreenerror, this) to pendingDocs list of pending fullscreen events.
pending_doc->append_pending_fullscreen_change(PendingFullscreenEvent::Type::Error, requesting_element);
WebIDL::reject_promise(realm, promise, JS::TypeError::create(realm, to_string(error)));
// 2. Reject promise with a TypeError exception and terminate these steps.
WebIDL::reject_promise(realm, promise, JS::TypeError::create(realm, request_fullscreen_error_to_string(error)));
return;
}
@ -2588,7 +2515,7 @@ GC::Ref<WebIDL::Promise> Element::request_fullscreen()
// 2. If element is docs fullscreen element, continue.
if (doc.fullscreen_element() == element) {
// Spec note: No need to notify observers when nothing has changed.
// Note: No need to notify observers when nothing has changed.
continue;
}
@ -2613,12 +2540,13 @@ GC::Ref<WebIDL::Promise> Element::request_fullscreen()
}
// https://fullscreen.spec.whatwg.org/#removing-steps
void Element::removing_steps_fullscreen()
void Element::exit_fullscreen_on_element_removal()
{
// 1. Let document be removedNodes node document.
auto& document = this->document();
// 2. Let nodes be removedNodes shadow-including inclusive descendants that have their fullscreen flag set, in shadow-including tree order.
// 2. Let nodes be removedNodes shadow-including inclusive descendants that have their fullscreen flag set, in
// shadow-including tree order.
// 3. For each node in nodes:
for_each_shadow_including_inclusive_descendant([&](Node& node) {
auto* element = as_if<Element>(node);
@ -2628,13 +2556,12 @@ void Element::removing_steps_fullscreen()
if (!element->is_fullscreen_element())
return TraversalDecision::Continue;
if (document.fullscreen_element() == element) {
// 1. If node is documents fullscreen element, exit fullscreen document.
// 1. If node is documents fullscreen element, exit fullscreen document.
if (document.fullscreen_element() == element)
document.exit_fullscreen();
} else {
// 2. Otherwise, unfullscreen node.
// 2. Otherwise, unfullscreen node.
else
document.unfullscreen_element(*element);
}
// 3. If documents top layer contains node, remove from the top layer immediately given node
if (element->in_top_layer())
@ -2644,6 +2571,73 @@ void Element::removing_steps_fullscreen()
});
}
Utf16String Element::request_fullscreen_error_to_string(RequestFullscreenError error)
{
switch (error) {
case RequestFullscreenError::False:
break;
case RequestFullscreenError::ElementReadyCheckFailed:
return "Element ready check failed"_utf16;
case RequestFullscreenError::UnsupportedElement:
return "Not supported element"_utf16;
case RequestFullscreenError::NoTransientUserActivation:
return "No transient user activation available to consume"_utf16;
case RequestFullscreenError::ElementNodeDocIsNotPendingDoc:
return "Element's node document is not pending doc"_utf16;
}
VERIFY_NOT_REACHED();
}
// https://fullscreen.spec.whatwg.org/#dom-element-requestfullscreen
// 5. If any of conditions are false, set error to true
Element::RequestFullscreenError Element::is_element_allowed_to_enter_fullscreen() const
{
// * Thiss namespace is the HTML namespace or this is an SVG svg or MathML math element. [SVG] [MATHML]
// FIXME: This likely wants to use is<MathML::MathMLMathElement> instead.
if (!(namespace_uri() == Namespace::HTML || is_svg_svg_element() || (is<MathML::MathMLElement>(*this) && tag_name() == MathML::TagNames::math)))
return RequestFullscreenError::UnsupportedElement;
// * This is not a dialog element
if (is<HTML::HTMLDialogElement>(*this))
return RequestFullscreenError::UnsupportedElement;
// * The fullscreen element ready check for this returns true.
if (!is_element_ready_for_fullscreen())
return RequestFullscreenError::ElementReadyCheckFailed;
// FIXME: * Fullscreen is supported.
// * Thiss relevant global object has transient activation or the algorithm is triggered by a user generated
// orientation change.
// FIXME: Handle user generated orientation changes.
auto* window = as<HTML::Window>(&HTML::relevant_global_object(*this));
if (!window->has_transient_activation())
return RequestFullscreenError::NoTransientUserActivation;
return RequestFullscreenError::False;
}
// https://fullscreen.spec.whatwg.org/#fullscreen-element-ready-check
bool Element::is_element_ready_for_fullscreen() const
{
// A fullscreen element ready check for an element element returns true if all of the following are true, and false otherwise:
// * element is connected.
if (!is_connected())
return false;
// * elements node document is allowed to use the "fullscreen" feature.
if (!m_document->is_allowed_to_use_feature(PolicyControlledFeature::Fullscreen))
return false;
// * element namespace is not the HTML namespace or elements popover visibility state is hidden.
if (namespace_uri() != Namespace::HTML)
return true;
auto const* html_element = as_if<HTML::HTMLElement>(this);
return html_element ? (html_element->popover_visibility_state() == HTML::HTMLElement::PopoverVisibilityState::Hidden) : false;
}
GC::Ptr<WebIDL::CallbackType> Element::onfullscreenchange()
{
return event_handler_attribute(HTML::EventNames::fullscreenchange);

View file

@ -265,9 +265,7 @@ public:
WebIDL::ExceptionOr<void> insert_adjacent_html(String const& position, TrustedTypes::TrustedHTMLOrString const&);
bool element_ready_check() const;
GC::Ref<WebIDL::Promise> request_fullscreen();
void removing_steps_fullscreen();
void set_fullscreen_flag(bool is_fullscreen) { m_fullscreen_flag = is_fullscreen; }
bool is_fullscreen_element() const { return m_fullscreen_flag; }
@ -587,6 +585,19 @@ private:
void invalidate_style_after_attribute_change(FlyString const& attribute_name, Optional<String> const& old_value, Optional<String> const& new_value);
enum class RequestFullscreenError : u8 {
False,
ElementReadyCheckFailed,
UnsupportedElement,
NoTransientUserActivation,
ElementNodeDocIsNotPendingDoc
};
static Utf16String request_fullscreen_error_to_string(RequestFullscreenError);
void exit_fullscreen_on_element_removal();
RequestFullscreenError is_element_allowed_to_enter_fullscreen() const;
bool is_element_ready_for_fullscreen() const;
WebIDL::ExceptionOr<GC::Ptr<Node>> insert_adjacent(StringView where, GC::Ref<Node> node);
void enqueue_an_element_on_the_appropriate_element_queue();

View file

@ -49,11 +49,9 @@ GC::Ptr<Element> ShadowRoot::fullscreen_element_for_bindings() const
return nullptr;
// 2. Let candidate be the result of retargeting fullscreen element against this.
// Note: ShadowRoot does not have it's own top layer. But the algorithm says to get the fullscreen
// element from the top layer, so it's grabbed from this' document.
auto* candidate = retarget(const_cast<ShadowRoot*>(this)->document().fullscreen_element().ptr(), const_cast<ShadowRoot*>(this));
// NB: ShadowRoot does not have it's own top layer. But the algorithm says to get the fullscreen element from the
// top layer, so it's grabbed from this' document.
auto* candidate = retarget(document().fullscreen_element(), const_cast<ShadowRoot*>(this));
if (!candidate)
return nullptr;