2022-11-05 14:30:49 +00:00
/*
2024-10-04 13:19:50 +02:00
* Copyright ( c ) 2022 , Andreas Kling < andreas @ ladybird . org >
2022-11-05 14:30:49 +00:00
* Copyright ( c ) 2022 , Andrew Kaster < akaster @ serenityos . org >
* Copyright ( c ) 2022 , Luke Wilde < lukew @ serenityos . org >
*
* SPDX - License - Identifier : BSD - 2 - Clause
*/
# include <AK/TypeCasts.h>
# include <AK/Vector.h>
2024-11-15 04:01:23 +13:00
# include <LibGC/Root.h>
2022-11-05 14:30:49 +00:00
# include <LibWeb/DOM/Document.h>
# include <LibWeb/DOM/Element.h>
# include <LibWeb/DOM/ShadowRoot.h>
# include <LibWeb/HTML/Focus.h>
2023-12-03 08:24:16 -05:00
# include <LibWeb/HTML/HTMLInputElement.h>
2023-09-19 20:24:18 +02:00
# include <LibWeb/HTML/TraversableNavigable.h>
2022-11-05 14:30:49 +00:00
# include <LibWeb/UIEvents/FocusEvent.h>
namespace Web : : HTML {
2024-08-14 11:39:32 +02:00
// https://html.spec.whatwg.org/multipage/interaction.html#fire-a-focus-event
2024-11-15 04:01:23 +13:00
static void fire_a_focus_event ( GC : : Ptr < DOM : : EventTarget > focus_event_target , GC : : Ptr < DOM : : EventTarget > related_focus_target , FlyString const & event_name , bool bubbles )
2024-08-14 11:39:32 +02:00
{
// To fire a focus event named e at an element t with a given related target r, fire an event named e at t, using FocusEvent,
// with the relatedTarget attribute initialized to r, the view attribute initialized to t's node document's relevant global
// object, and the composed flag set.
UIEvents : : FocusEventInit focus_event_init { } ;
focus_event_init . related_target = related_focus_target ;
2025-01-21 09:12:05 -05:00
focus_event_init . view = as < HTML : : Window > ( focus_event_target - > realm ( ) . global_object ( ) ) . window ( ) ;
2024-08-14 11:39:32 +02:00
auto focus_event = UIEvents : : FocusEvent : : create ( focus_event_target - > realm ( ) , event_name , focus_event_init ) ;
// AD-HOC: support bubbling focus events, used for focusin & focusout.
// See: https://github.com/whatwg/html/issues/3514
focus_event - > set_bubbles ( bubbles ) ;
focus_event - > set_composed ( true ) ;
focus_event_target - > dispatch_event ( focus_event ) ;
}
2022-11-05 14:30:49 +00:00
// https://html.spec.whatwg.org/multipage/interaction.html#focus-update-steps
2024-11-15 04:01:23 +13:00
static void run_focus_update_steps ( Vector < GC : : Root < DOM : : Node > > old_chain , Vector < GC : : Root < DOM : : Node > > new_chain , DOM : : Node * new_focus_target )
2022-11-05 14:30:49 +00:00
{
2025-02-02 20:41:23 +01:00
// The focus update steps, given an old chain, a new chain, and a new focus target respectively, are as follows:
2025-08-22 11:04:24 +02:00
// 1. If the last entry in old chain and the last entry in new chain are the same, pop the last entry from old chain
// and the last entry from new chain and redo this step.
2022-11-05 14:30:49 +00:00
while ( ! old_chain . is_empty ( )
& & ! new_chain . is_empty ( )
2025-01-30 14:32:24 +00:00
& & old_chain . last ( ) = = new_chain . last ( ) ) {
2022-11-05 14:30:49 +00:00
( void ) old_chain . take_last ( ) ;
( void ) new_chain . take_last ( ) ;
}
// 2. For each entry entry in old chain, in order, run these substeps:
for ( auto & entry : old_chain ) {
2025-02-02 20:41:23 +01:00
// 1. If entry is an input element
2025-08-22 11:04:24 +02:00
if ( auto * input_element = as_if < HTMLInputElement > ( * entry ) ) {
2023-12-03 08:24:16 -05:00
// FIXME: Spec issue: It doesn't make sense to check if the element has a defined activation behavior, as
// that is always true. Instead, we check if it has an *input* activation behavior.
// https://github.com/whatwg/html/issues/9973
2025-08-22 11:04:24 +02:00
// and the change event applies to the element, and the element does not have a defined activation behavior,
// and the user has changed the element's value or its list of selected files while the control was focused
// without committing that change (such that it is different to what it was when the control was first
2025-02-02 20:41:23 +01:00
// focused), then:
2025-08-22 11:04:24 +02:00
if ( input_element - > change_event_applies ( ) & & ! input_element - > has_input_activation_behavior ( )
& & input_element - > has_uncommitted_changes ( ) ) {
2025-02-02 20:41:23 +01:00
// 1. Set entry's user validity to true.
2025-08-22 11:04:24 +02:00
input_element - > set_user_validity ( true ) ;
2025-02-02 20:41:23 +01:00
// 2. Fire an event named change at the element, with the bubbles attribute initialized to true.
2025-08-22 11:04:24 +02:00
input_element - > commit_pending_changes ( ) ;
2023-12-03 08:24:16 -05:00
}
}
2022-11-05 14:30:49 +00:00
2025-08-22 11:04:24 +02:00
// 2. If entry is an element, let blur event target be entry.
// If entry is a Document object, let blur event target be that Document object's relevant global object.
// Otherwise, let blur event target be null.
2024-11-15 04:01:23 +13:00
GC : : Ptr < DOM : : EventTarget > blur_event_target ;
2025-08-22 11:04:24 +02:00
if ( is < DOM : : Element > ( * entry ) )
blur_event_target = entry ;
else if ( is < DOM : : Document > ( * entry ) )
blur_event_target = as < Window > ( relevant_global_object ( * entry ) ) ;
2022-11-05 14:30:49 +00:00
2025-08-22 11:04:24 +02:00
// 3. If entry is the last entry in old chain, and entry is an Element, and the last entry in new chain is also
// an Element, then let related blur target be the last entry in new chain. Otherwise, let related blur
// target be null.
2024-11-15 04:01:23 +13:00
GC : : Ptr < DOM : : EventTarget > related_blur_target ;
2022-11-05 14:30:49 +00:00
if ( ! old_chain . is_empty ( )
& & & entry = = & old_chain . last ( )
& & is < DOM : : Element > ( * entry )
& & ! new_chain . is_empty ( )
& & is < DOM : : Element > ( * new_chain . last ( ) ) ) {
2025-08-22 11:04:24 +02:00
related_blur_target = new_chain . last ( ) ;
2022-11-05 14:30:49 +00:00
}
2025-08-22 11:04:24 +02:00
// 4. If blur event target is not null, fire a focus event named blur at blur event target, with related blur
// target as the related target.
// FIXME: NOTE: In some cases, e.g., if entry is an area element's shape, a scrollable region, or a viewport, no event
// is fired.
2022-11-05 14:30:49 +00:00
if ( blur_event_target ) {
2024-08-14 11:39:32 +02:00
fire_a_focus_event ( blur_event_target , related_blur_target , HTML : : EventNames : : blur , false ) ;
2024-02-25 06:51:28 +01:00
2024-08-14 11:39:32 +02:00
// AD-HOC: dispatch focusout
fire_a_focus_event ( blur_event_target , related_blur_target , HTML : : EventNames : : focusout , true ) ;
}
2022-11-05 14:30:49 +00:00
}
2025-08-22 11:04:24 +02:00
// FIXME: 3. Apply any relevant platform-specific conventions for focusing new focus target. (For example, some platforms
// select the contents of a text control when that control is focused.)
2022-11-05 14:30:49 +00:00
( void ) new_focus_target ;
// 4. For each entry entry in new chain, in reverse order, run these substeps:
for ( auto & entry : new_chain . in_reverse ( ) ) {
2025-08-22 11:04:24 +02:00
// 1. If entry is a focusable area, and the focused area of the document is not entry:
if ( entry - > is_focusable ( ) & & entry - > document ( ) . focused_area ( ) ! = entry . ptr ( ) ) {
// 1. Set document's relevant global object's navigation API's focus changed during ongoing navigation to
// true.
as < Window > ( relevant_global_object ( * entry ) ) . navigation ( ) - > set_focus_changed_during_ongoing_navigation ( true ) ;
2022-11-05 14:30:49 +00:00
2025-08-22 11:04:24 +02:00
// 2. Designate entry as the focused area of the document.
entry - > document ( ) . set_focused_area ( * entry ) ;
2022-11-05 14:30:49 +00:00
}
2025-08-22 11:04:24 +02:00
// 2. If entry is an element, let focus event target be entry.
// If entry is a Document object, let focus event target be that Document object's relevant global object.
// Otherwise, let focus event target be null.
GC : : Ptr < DOM : : EventTarget > focus_event_target ;
if ( is < DOM : : Document > ( * entry ) )
focus_event_target = as < Window > ( relevant_global_object ( * entry ) ) ;
else if ( is < DOM : : Element > ( * entry ) )
focus_event_target = * entry ;
// 3. If entry is the last entry in new chain, and entry is an Element, and the last entry in old chain is also
// an Element, then let related focus target be the last entry in old chain. Otherwise, let related focus
// target be null.
2024-11-15 04:01:23 +13:00
GC : : Ptr < DOM : : EventTarget > related_focus_target ;
2022-11-05 14:30:49 +00:00
if ( ! new_chain . is_empty ( )
& & & entry = = & new_chain . last ( )
& & is < DOM : : Element > ( * entry )
& & ! old_chain . is_empty ( )
& & is < DOM : : Element > ( * old_chain . last ( ) ) ) {
2025-08-22 11:04:24 +02:00
related_focus_target = old_chain . last ( ) ;
2022-11-05 14:30:49 +00:00
}
2025-08-22 11:04:24 +02:00
// 4. If focus event target is not null, fire a focus event named focus at focus event target, with related
// focus target as the related target.
// FIXME: NOTE: In some cases, e.g. if entry is an area element's shape, a scrollable region, or a viewport, no event
// is fired.
2022-11-05 14:30:49 +00:00
if ( focus_event_target ) {
2024-08-14 11:39:32 +02:00
fire_a_focus_event ( focus_event_target , related_focus_target , HTML : : EventNames : : focus , false ) ;
// AD-HOC: dispatch focusin
fire_a_focus_event ( focus_event_target , related_focus_target , HTML : : EventNames : : focusin , true ) ;
2022-11-05 14:30:49 +00:00
}
}
}
// https://html.spec.whatwg.org/multipage/interaction.html#focus-chain
2024-11-15 04:01:23 +13:00
static Vector < GC : : Root < DOM : : Node > > focus_chain ( DOM : : Node * subject )
2022-11-05 14:30:49 +00:00
{
// FIXME: Move this somewhere more spec-friendly.
if ( ! subject )
return { } ;
// 1. Let output be an empty list.
2024-11-15 04:01:23 +13:00
Vector < GC : : Root < DOM : : Node > > output ;
2022-11-05 14:30:49 +00:00
// 2. Let currentObject be subject.
auto * current_object = subject ;
// 3. While true:
while ( true ) {
// 1. Append currentObject to output.
2024-11-15 04:01:23 +13:00
output . append ( GC : : make_root ( * current_object ) ) ;
2022-11-05 14:30:49 +00:00
// FIXME: 2. If currentObject is an area element's shape, then append that area element to output.
// FIXME: Otherwise, if currentObject's DOM anchor is an element that is not currentObject itself, then append currentObject's DOM anchor to output.
// FIXME: Everything below needs work. The conditions are not entirely right.
if ( ! is < DOM : : Document > ( * current_object ) ) {
// 3. If currentObject is a focusable area, then set currentObject to currentObject's DOM anchor's node document.
current_object = & current_object - > document ( ) ;
} else if ( is < DOM : : Document > ( * current_object )
2023-09-07 01:19:24 +02:00
& & current_object - > navigable ( )
& & current_object - > navigable ( ) - > parent ( ) ) {
// Otherwise, if currentObject is a Document whose node navigable's parent is non-null, then set currentObject to currentObject's node navigable's parent.
current_object = current_object - > navigable ( ) - > container ( ) ;
2022-11-05 14:30:49 +00:00
} else {
2023-09-07 01:19:24 +02:00
// Otherwise, break.
2022-11-05 14:30:49 +00:00
break ;
}
}
// 4. Return output.
return output ;
}
// https://html.spec.whatwg.org/multipage/interaction.html#focusing-steps
// FIXME: This should accept more types.
2025-06-13 14:10:30 +02:00
void run_focusing_steps ( DOM : : Node * new_focus_target , DOM : : Node * fallback_target , FocusTrigger focus_trigger )
2022-11-05 14:30:49 +00:00
{
2025-08-22 11:04:24 +02:00
// FIXME: 1. If new focus target is not a focusable area, then set new focus target to the result of getting the focusable
// area for new focus target, given focus trigger if it was passed.
2022-11-05 14:30:49 +00:00
// 2. If new focus target is null, then:
if ( ! new_focus_target ) {
// 1. If no fallback target was specified, then return.
if ( ! fallback_target )
return ;
// 2. Otherwise, set new focus target to the fallback target.
new_focus_target = fallback_target ;
}
2024-11-03 17:22:22 +01:00
// 3. If new focus target is a navigable container with non-null content navigable, then set new focus target to the content navigable's active document.
2025-08-22 11:04:24 +02:00
if ( auto * navigable_container = as_if < NavigableContainer > ( * new_focus_target ) ) {
if ( auto content_navigable = navigable_container - > content_navigable ( ) )
2024-11-03 17:22:22 +01:00
new_focus_target = content_navigable - > active_document ( ) ;
2022-11-05 14:30:49 +00:00
}
2025-02-05 23:27:09 +00:00
// 4. If new focus target is a focusable area and its DOM anchor is inert, then return.
if ( new_focus_target - > is_inert ( ) )
return ;
2022-11-05 14:30:49 +00:00
// 5. If new focus target is the currently focused area of a top-level browsing context, then return.
if ( ! new_focus_target - > document ( ) . browsing_context ( ) )
return ;
2024-04-26 16:59:04 +02:00
auto top_level_traversable = new_focus_target - > document ( ) . browsing_context ( ) - > top_level_traversable ( ) ;
if ( new_focus_target = = top_level_traversable - > currently_focused_area ( ) . ptr ( ) )
2022-11-05 14:30:49 +00:00
return ;
// 6. Let old chain be the current focus chain of the top-level browsing context in which
// new focus target finds itself.
2024-04-26 16:59:04 +02:00
auto old_chain = focus_chain ( top_level_traversable - > currently_focused_area ( ) ) ;
2022-11-05 14:30:49 +00:00
// 7. Let new chain be the focus chain of new focus target.
auto new_chain = focus_chain ( new_focus_target ) ;
2025-06-13 14:10:30 +02:00
// AD-HOC: Remember last focus trigger for :focus-visible / focus indication.
new_focus_target - > document ( ) . set_last_focus_trigger ( focus_trigger ) ;
2022-11-05 14:30:49 +00:00
// 8. Run the focus update steps with old chain, new chain, and new focus target respectively.
run_focus_update_steps ( old_chain , new_chain , new_focus_target ) ;
}
2023-12-03 08:42:32 -05:00
// https://html.spec.whatwg.org/multipage/interaction.html#unfocusing-steps
2022-11-05 14:30:49 +00:00
void run_unfocusing_steps ( DOM : : Node * old_focus_target )
{
// NOTE: The unfocusing steps do not always result in the focus changing, even when applied to the currently focused
// area of a top-level browsing context. For example, if the currently focused area of a top-level browsing context
// is a viewport, then it will usually keep its focus regardless until another focusable area is explicitly focused
// with the focusing steps.
auto is_shadow_host = [ ] ( DOM : : Node * node ) {
2023-01-28 20:33:36 +01:00
return is < DOM : : Element > ( node ) & & static_cast < DOM : : Element * > ( node ) - > is_shadow_host ( ) ;
2022-11-05 14:30:49 +00:00
} ;
// 1. If old focus target is a shadow host whose shadow root's delegates focus is true, and old focus target's
// shadow root is a shadow-including inclusive ancestor of the currently focused area of a top-level browsing
// context's DOM anchor, then set old focus target to that currently focused area of a top-level browsing
// context.
if ( is_shadow_host ( old_focus_target ) ) {
2024-06-25 11:28:58 +02:00
auto shadow_root = static_cast < DOM : : Element * > ( old_focus_target ) - > shadow_root ( ) ;
2022-11-05 14:30:49 +00:00
if ( shadow_root - > delegates_focus ( ) ) {
2024-04-26 16:59:04 +02:00
auto top_level_traversable = old_focus_target - > document ( ) . browsing_context ( ) - > top_level_traversable ( ) ;
if ( auto currently_focused_area = top_level_traversable - > currently_focused_area ( ) ) {
2022-11-05 14:30:49 +00:00
if ( shadow_root - > is_shadow_including_ancestor_of ( * currently_focused_area ) ) {
old_focus_target = currently_focused_area ;
}
}
}
}
2025-02-05 23:27:09 +00:00
// 2. If old focus target is inert, then return.
if ( old_focus_target - > is_inert ( ) )
return ;
2022-11-05 14:30:49 +00:00
// FIXME: 3. If old focus target is an area element and one of its shapes is the currently focused area of a
// top-level browsing context, or, if old focus target is an element with one or more scrollable regions, and one
// of them is the currently focused area of a top-level browsing context, then let old focus target be that
// currently focused area of a top-level browsing context.
// NOTE: HTMLAreaElement is currently missing the shapes property
// 4. Let old chain be the current focus chain of the top-level browsing context in which old focus target finds itself.
2025-08-22 11:04:24 +02:00
auto top_level_traversable = old_focus_target - > document ( ) . browsing_context ( ) - > top_level_traversable ( ) ;
2024-04-26 16:59:04 +02:00
auto old_chain = focus_chain ( top_level_traversable - > currently_focused_area ( ) ) ;
2022-11-05 14:30:49 +00:00
// 5. If old focus target is not one of the entries in old chain, then return.
2023-12-03 08:42:32 -05:00
auto it = old_chain . find_if ( [ & ] ( auto const & node ) { return old_focus_target = = node ; } ) ;
if ( it = = old_chain . end ( ) )
return ;
2022-11-05 14:30:49 +00:00
// 6. If old focus target is not a focusable area, then return.
if ( ! old_focus_target - > is_focusable ( ) )
return ;
// 7. Let topDocument be old chain's last entry.
2025-08-22 11:04:24 +02:00
auto & top_document = as < DOM : : Document > ( * old_chain . last ( ) ) ;
2022-11-05 14:30:49 +00:00
2023-09-19 20:24:18 +02:00
// 8. If topDocument's node navigable has system focus, then run the focusing steps for topDocument's viewport.
2025-08-22 11:04:24 +02:00
if ( top_document . navigable ( ) - > traversable_navigable ( ) - > system_visibility_state ( ) = = HTML : : VisibilityState : : Visible ) {
run_focusing_steps ( & top_document ) ;
2022-11-05 14:30:49 +00:00
} else {
2025-08-22 11:04:24 +02:00
// FIXME: Otherwise, apply any relevant platform-specific conventions for removing system focus from topDocument's
// browsing context, and run the focus update steps with old chain, an empty list, and null respectively.
2022-11-05 14:30:49 +00:00
// What? It already doesn't have system focus, what possible platform-specific conventions are there?
run_focus_update_steps ( old_chain , { } , nullptr ) ;
}
2025-08-22 11:04:24 +02:00
// NOTE: The unfocusing steps do not always result in the focus changing, even when applied to the currently focused
// area of a top-level traversable. For example, if the currently focused area of a top-level traversable is a
// viewport, then it will usually keep its focus regardless until another focusable area is explicitly focused
// with the focusing steps.
2022-11-05 14:30:49 +00:00
}
}