Skip to main content

script/dom/document/
focus.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::cell::{Cell, Ref};
6use std::cmp::Ordering;
7
8use bitflags::bitflags;
9use embedder_traits::FocusSequenceNumber;
10use js::context::JSContext;
11use keyboard_types::Modifiers;
12use script_bindings::cell::DomRefCell;
13use script_bindings::codegen::GenericBindings::HTMLIFrameElementBinding::HTMLIFrameElementMethods;
14use script_bindings::codegen::GenericBindings::ShadowRootBinding::ShadowRootMethods;
15use script_bindings::codegen::GenericBindings::WindowBinding::WindowMethods;
16use script_bindings::inheritance::Castable;
17use script_bindings::root::{Dom, DomRoot};
18use script_bindings::script_runtime::CanGc;
19use servo_base::id::BrowsingContextId;
20use servo_constellation_traits::{
21    RemoteFocusOperation, ScriptToConstellationMessage, SequentialFocusDirection,
22};
23
24use crate::dom::bindings::root::MutNullableDom;
25use crate::dom::focusevent::FocusEventType;
26use crate::dom::node::focus::FocusNavigationScopeOwner;
27use crate::dom::types::{
28    Element, EventTarget, FocusEvent, HTMLElement, HTMLIFrameElement, KeyboardEvent, Window,
29};
30use crate::dom::{Document, Event, EventBubbles, EventCancelable, Node, NodeTraits};
31use crate::realms::enter_realm;
32
33/// The kind of focusable area a [`FocusableArea`] is. A [`FocusableArea`] may be click focusable,
34/// sequentially focusable, or both.
35#[derive(Clone, Copy, Debug, Default, JSTraceable, MallocSizeOf, PartialEq)]
36pub(crate) struct FocusableAreaKind(u8);
37
38bitflags! {
39    impl FocusableAreaKind: u8 {
40        /// <https://html.spec.whatwg.org/multipage/#click-focusable>
41        ///
42        /// > A focusable area is said to be click focusable if the user agent determines that it is
43        /// > click focusable. User agents should consider focusable areas with non-null tabindex values
44        /// > to be click focusable.
45        const Click = 1 << 0;
46        /// <https://html.spec.whatwg.org/multipage/#sequentially-focusable>.
47        ///
48        /// > A focusable area is said to be sequentially focusable if it is included in its
49        /// > Document's sequential focus navigation order and the user agent determines that it is
50        /// > sequentially focusable.
51        const Sequential = 1 << 1;
52    }
53}
54
55/// <https://html.spec.whatwg.org/multipage/#focusable-area>
56#[derive(Clone, Default, JSTraceable, MallocSizeOf, PartialEq)]
57pub(crate) enum FocusableArea {
58    Node {
59        node: DomRoot<Node>,
60        kind: FocusableAreaKind,
61    },
62    /// The viewport of an `<iframe>` element in its containing `Document`. `<iframe>`s
63    /// are focusable areas, but have special behavior when focusing.
64    IFrameViewport {
65        iframe_element: DomRoot<HTMLIFrameElement>,
66        kind: FocusableAreaKind,
67    },
68    #[default]
69    Viewport,
70}
71
72impl std::fmt::Debug for FocusableArea {
73    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74        match self {
75            Self::Node { node, kind } => f
76                .debug_struct("Node")
77                .field("node", node)
78                .field("kind", kind)
79                .finish(),
80            Self::IFrameViewport {
81                iframe_element,
82                kind,
83            } => f
84                .debug_struct("IFrameViewport")
85                .field("pipeline", &iframe_element.pipeline_id())
86                .field("kind", kind)
87                .finish(),
88            Self::Viewport => write!(f, "Viewport"),
89        }
90    }
91}
92
93impl FocusableArea {
94    pub(crate) fn kind(&self) -> FocusableAreaKind {
95        match self {
96            Self::Node { kind, .. } | Self::IFrameViewport { kind, .. } => *kind,
97            Self::Viewport => FocusableAreaKind::Click | FocusableAreaKind::Sequential,
98        }
99    }
100
101    /// If this focusable area is a node, return it as an [`Element`] if it is possible, otherwise
102    /// return `None`. This is the [`Element`] to use for applying `:focus` state and for firing
103    /// `blur` and `focus` events if any.
104    ///
105    /// Note: This is currently in a transitional state while the code moves more toward the
106    /// specification.
107    pub(crate) fn element(&self) -> Option<&Element> {
108        match self {
109            Self::Node { node, .. } => node.downcast(),
110            Self::IFrameViewport { iframe_element, .. } => Some(iframe_element.upcast()),
111            Self::Viewport => None,
112        }
113    }
114
115    /// <https://html.spec.whatwg.org/multipage/#dom-anchor>
116    pub(crate) fn dom_anchor(&self, document: &Document) -> DomRoot<Node> {
117        match self {
118            Self::Node { node, .. } => node.clone(),
119            Self::IFrameViewport { iframe_element, .. } => {
120                DomRoot::from_ref(iframe_element.upcast())
121            },
122            Self::Viewport => DomRoot::from_ref(document.upcast()),
123        }
124    }
125
126    pub(crate) fn focus_chain(&self) -> Vec<FocusableArea> {
127        match self {
128            FocusableArea::Node { .. } | FocusableArea::IFrameViewport { .. } => {
129                vec![self.clone(), FocusableArea::Viewport]
130            },
131            FocusableArea::Viewport => vec![self.clone()],
132        }
133    }
134}
135
136/// The [`DocumentFocusHandler`] is a structure responsible for handling and storing data related to
137/// focus for the `Document`. It exists to decrease the size of the `Document`.
138/// structure.
139#[derive(JSTraceable, MallocSizeOf)]
140#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
141pub(crate) struct DocumentFocusHandler {
142    /// The [`Window`] element for this [`DocumentFocusHandler`].
143    window: Dom<Window>,
144    /// The focused area of the [`Document`].
145    ///
146    /// <https://html.spec.whatwg.org/multipage/#focused-area-of-the-document>
147    focused_area: DomRefCell<FocusableArea>,
148    /// The last sequence number sent to the constellation.
149    #[no_trace]
150    focus_sequence: Cell<FocusSequenceNumber>,
151    /// Indicates whether the container is included in the top-level browsing
152    /// context's focus chain (not considering system focus). Permanently `true`
153    /// for a top-level document.
154    has_focus: Cell<bool>,
155    /// <https://html.spec.whatwg.org/multipage/#sequential-focus-navigation-starting-point>
156    sequential_focus_navigation_starting_point: MutNullableDom<Node>,
157}
158
159impl DocumentFocusHandler {
160    pub(crate) fn new(window: &Window, has_focus: bool) -> Self {
161        Self {
162            window: Dom::from_ref(window),
163            focused_area: Default::default(),
164            focus_sequence: Cell::new(FocusSequenceNumber::default()),
165            has_focus: Cell::new(has_focus),
166            sequential_focus_navigation_starting_point: Default::default(),
167        }
168    }
169
170    pub(crate) fn has_focus(&self) -> bool {
171        self.has_focus.get()
172    }
173
174    pub(crate) fn set_has_focus(&self, has_focus: bool) {
175        self.has_focus.set(has_focus);
176    }
177
178    /// Return the element that currently has focus. If `None` is returned the viewport itself has focus.
179    pub(crate) fn focused_area(&self) -> Ref<'_, FocusableArea> {
180        self.focused_area.borrow()
181    }
182
183    /// Set the element that currently has focus and update the focus state for both the previously
184    /// set element (if any) and the new one, as well as the new one. This will not do anything if
185    /// the new element is the same as the previous one. Note that this *will not* fire any focus
186    /// events. If that is necessary the [`DocumentFocusHandler::focus`] should be used.
187    pub(crate) fn set_focused_area(&self, new_focusable_area: FocusableArea) {
188        if new_focusable_area == *self.focused_area.borrow() {
189            return;
190        }
191
192        // From <https://html.spec.whatwg.org/multipage/#selector-focus>
193        // > For the purposes of the CSS :focus pseudo-class, an element has the focus when:
194        // >  - it is not itself a navigable container; and
195        // >  - any of the following are true:
196        // >    - it is one of the elements listed in the current focus chain of the top-level
197        // >      traversable; or
198        // >    - its shadow root shadowRoot is not null and shadowRoot is the root of at least one
199        // >      element that has the focus.
200        //
201        // We are trying to accomplish the last requirement here, by walking up the tree and
202        // marking each shadow host as focused.
203        fn recursively_set_focus_status(element: &Element, new_state: bool) {
204            element.set_focus_state(new_state);
205
206            let Some(shadow_root) = element.containing_shadow_root() else {
207                return;
208            };
209            recursively_set_focus_status(&shadow_root.Host(), new_state);
210        }
211
212        if let Some(previously_focused_element) = self.focused_area.borrow().element() {
213            recursively_set_focus_status(previously_focused_element, false);
214        }
215        if let Some(newly_focused_element) = new_focusable_area.element() {
216            recursively_set_focus_status(newly_focused_element, true);
217        }
218
219        *self.focused_area.borrow_mut() = new_focusable_area;
220    }
221
222    /// Get the last sequence number sent to the constellation.
223    ///
224    /// Received focus-related messages with sequence numbers less than the one
225    /// returned by this method must be discarded.
226    pub fn focus_sequence(&self) -> FocusSequenceNumber {
227        self.focus_sequence.get()
228    }
229
230    /// Generate the next sequence number for focus-related messages.
231    fn increment_fetch_focus_sequence(&self) -> FocusSequenceNumber {
232        self.focus_sequence.set(FocusSequenceNumber(
233            self.focus_sequence
234                .get()
235                .0
236                .checked_add(1)
237                .expect("too many focus messages have been sent"),
238        ));
239        self.focus_sequence.get()
240    }
241
242    /// <https://html.spec.whatwg.org/multipage/#current-focus-chain-of-a-top-level-traversable>
243    pub(crate) fn current_focus_chain(&self) -> Vec<FocusableArea> {
244        // > The current focus chain of a top-level traversable is the focus chain of the
245        // > currently focused area of traversable, if traversable is non-null, or an empty list
246        // > otherwise.
247
248        // We cannot easily get the full focus chain of the top-level traversable, so we just
249        // get the bits that intersect with this `Document`. The rest will be handled
250        // internally in [`Self::focus_update_steps`].
251        if !self.has_focus() {
252            return vec![];
253        }
254        self.focused_area().focus_chain()
255    }
256
257    /// Reassign the focus context to the element that last requested focus during this
258    /// transaction, or the document if no elements requested it.
259    pub(crate) fn focus(&self, cx: &mut JSContext, new_focus_target: FocusableArea) {
260        let old_focus_chain = self.current_focus_chain();
261        let new_focus_chain = new_focus_target.focus_chain();
262        self.focus_update_steps(cx, new_focus_chain, old_focus_chain, &new_focus_target);
263
264        // Advertise the change in the focus chain.
265        // <https://html.spec.whatwg.org/multipage/#focus-chain>
266        // <https://html.spec.whatwg.org/multipage/#focusing-steps>
267        //
268        // TODO: Integrate this into the "focus update steps."
269        //
270        // If the top-level BC doesn't have system focus, this won't
271        // have an immediate effect, but it will when we gain system
272        // focus again. Therefore we still have to send `ScriptMsg::
273        // Focus`.
274        //
275        // When a container with a non-null nested browsing context is
276        // focused, its active document becomes the focused area of the
277        // top-level browsing context instead. Therefore we need to let
278        // the constellation know if such a container is focused.
279        //
280        // > The focusing steps for an object `new focus target` [...]
281        // >
282        // >  3. If `new focus target` is a browsing context container
283        // >     with non-null nested browsing context, then set
284        // >     `new focus target` to the nested browsing context's
285        // >     active document.
286        let child_browsing_context_id = match new_focus_target {
287            FocusableArea::IFrameViewport { iframe_element, .. } => {
288                iframe_element.browsing_context_id()
289            },
290            _ => None,
291        };
292        let sequence = self.increment_fetch_focus_sequence();
293
294        debug!(
295            "Advertising the focus request to the constellation \
296                        with sequence number {sequence:?} and child \
297                        {child_browsing_context_id:?}",
298        );
299        self.window.send_to_constellation(
300            ScriptToConstellationMessage::FocusAncestorBrowsingContextsForFocusingSteps(
301                child_browsing_context_id,
302                sequence,
303            ),
304        );
305    }
306
307    /// <https://html.spec.whatwg.org/multipage/#focus-update-steps>
308    pub(crate) fn focus_update_steps(
309        &self,
310        cx: &mut JSContext,
311        mut new_focus_chain: Vec<FocusableArea>,
312        mut old_focus_chain: Vec<FocusableArea>,
313        new_focus_target: &FocusableArea,
314    ) {
315        let new_focus_chain_was_empty = new_focus_chain.is_empty();
316
317        // Step 1: If the last entry in old chain and the last entry in new chain are the same,
318        // pop the last entry from old chain and the last entry from new chain and redo this
319        // step.
320        //
321        // We avoid recursion here.
322        while let (Some(last_new), Some(last_old)) =
323            (new_focus_chain.last(), old_focus_chain.last())
324        {
325            if last_new == last_old {
326                new_focus_chain.pop();
327                old_focus_chain.pop();
328            } else {
329                break;
330            }
331        }
332
333        // If the two focus chains are both empty, focus hasn't changed. This isn't in the
334        // specification, but we must do it because we set the focused area to the viewport
335        // before blurring. If no focus changes, that would mean the currently focused element
336        // loses focus.
337        if old_focus_chain.is_empty() && new_focus_chain.is_empty() {
338            return;
339        }
340        // Although the "focusing steps" in the HTML specification say to wait until after firing
341        // the "blur" event to change the currently focused area of the Document, browsers tend
342        // to set it to the viewport before firing the "blur" event.
343        //
344        // See https://github.com/whatwg/html/issues/1569
345        self.set_focused_area(FocusableArea::Viewport);
346
347        // Step 2: For each entry entry in old chain, in order, run these substeps:
348        // Note: `old_focus_chain` might be empty!
349        let last_old_focus_chain_entry = old_focus_chain.len().saturating_sub(1);
350        for (index, entry) in old_focus_chain.iter().enumerate() {
351            // Step 2.1: If entry is an input element, and the change event applies to the element,
352            // and the element does not have a defined activation behavior, and the user has
353            // changed the element's value or its list of selected files while the control was
354            // focused without committing that change (such that it is different to what it was
355            // when the control was first focused), then:
356            // Step 2.1.1: Set entry's user validity to true.
357            // Step 2.1.2: Fire an event named change at the element, with the bubbles attribute initialized to true.
358            // TODO: Implement this.
359
360            // Step 2.2:
361            // - If entry is an element, let blur event target be entry.
362            // - If entry is a Document object, let blur event target be that Document object's
363            //    relevant global object.
364            // - Otherwise, let blur event target be null.
365            //
366            // Note: We always send focus and blur events for `<iframe>` elements, but other
367            // browsers only seem to do that conditionally. This needs a bit more research.
368            let blur_event_target = match entry {
369                FocusableArea::Node { node, .. } => Some(node.upcast::<EventTarget>()),
370                FocusableArea::IFrameViewport { iframe_element, .. } => {
371                    Some(iframe_element.upcast())
372                },
373                FocusableArea::Viewport => Some(self.window.upcast::<EventTarget>()),
374            };
375
376            // Step 2.3: If entry is the last entry in old chain, and entry is an Element, and
377            // the last entry in new chain is also an Element, then let related blur target be
378            // the last entry in new chain. Otherwise, let related blur target be null.
379            //
380            // Note: This can only happen when the focused `Document` doesn't change and we are
381            // moving focus from one element to another. These elements are the last in the chain
382            // because of the popping we do at the start of these steps.
383            let related_blur_target = match new_focus_chain.last() {
384                Some(FocusableArea::Node { node, .. })
385                    if index == last_old_focus_chain_entry &&
386                        matches!(entry, FocusableArea::Node { .. }) =>
387                {
388                    Some(node.upcast())
389                },
390                _ => None,
391            };
392
393            // Step 2.4: If blur event target is not null, fire a focus event named blur at
394            // blur event target, with related blur target as the related target.
395            if let Some(blur_event_target) = blur_event_target {
396                self.fire_focus_event(
397                    cx,
398                    FocusEventType::Blur,
399                    blur_event_target,
400                    related_blur_target,
401                );
402            }
403        }
404
405        // Step 3: Apply any relevant platform-specific conventions for focusing new focus
406        // target. (For example, some platforms select the contents of a text control when that
407        // control is focused.)
408        if &*self.focused_area() != new_focus_target &&
409            let Some(html_element) = new_focus_target
410                .element()
411                .and_then(|element| element.downcast::<HTMLElement>())
412        {
413            html_element.handle_focus_state_for_contenteditable(cx);
414        }
415
416        self.set_has_focus(!new_focus_chain_was_empty);
417
418        // Step 4: For each entry entry in new chain, in reverse order, run these substeps:
419        // Note: `new_focus_chain` might be empty!
420        let last_new_focus_chain_entry = new_focus_chain.len().saturating_sub(1); // Might be empty, so calculated here.
421        for (index, entry) in new_focus_chain.iter().enumerate().rev() {
422            // Step 4.1: If entry is a focusable area, and the focused area of the document is
423            // not entry:
424            //
425            // Here we deviate from the specification a bit, as all focus chain elements are
426            // focusable areas currently. We just assume that it means the first entry of the
427            // chain, which is the new focus target
428            if index == 0 {
429                // Step 4.1.1: Set document's relevant global object's navigation API's focus
430                // changed during ongoing navigation to true.
431                // TODO: Implement this.
432
433                // Step 4.1.2: Designate entry as the focused area of the document.
434                self.set_focused_area(entry.clone());
435            }
436
437            // Step 4.2:
438            // - If entry is an element, let focus event target be entry.
439            // - If entry is a Document object, let focus event target be that Document
440            //   object's relevant global object.
441            // - Otherwise, let focus event target be null.
442            //
443            // Note: We always send focus and blur events for `<iframe>` elements, but other
444            // browsers only seem to do that conditionally. This needs a bit more research.
445            let focus_event_target = match entry {
446                FocusableArea::Node { node, .. } => Some(node.upcast::<EventTarget>()),
447                FocusableArea::IFrameViewport { iframe_element, .. } => {
448                    Some(iframe_element.upcast())
449                },
450                FocusableArea::Viewport => Some(self.window.upcast::<EventTarget>()),
451            };
452
453            // Step 4.3: If entry is the last entry in new chain, and entry is an Element, and
454            // the last entry in old chain is also an Element, then let related focus target be
455            // the last entry in old chain. Otherwise, let related focus target be null.
456            //
457            // Note: This can only happen when the focused `Document` doesn't change and we are
458            // moving focus from one element to another. These elements are the last in the chain
459            // because of the popping we do at the start of these steps.
460            let related_focus_target = match old_focus_chain.last() {
461                Some(FocusableArea::Node { node, .. })
462                    if index == last_new_focus_chain_entry &&
463                        matches!(entry, FocusableArea::Node { .. }) =>
464                {
465                    Some(node.upcast())
466                },
467                _ => None,
468            };
469
470            // Step 4.4: If focus event target is not null, fire a focus event named focus at
471            // focus event target, with related focus target as the related target.
472            if let Some(focus_event_target) = focus_event_target {
473                self.fire_focus_event(
474                    cx,
475                    FocusEventType::Focus,
476                    focus_event_target,
477                    related_focus_target,
478                );
479            }
480        }
481    }
482
483    /// <https://html.spec.whatwg.org/multipage/#fire-a-focus-event>
484    pub(crate) fn fire_focus_event(
485        &self,
486        cx: &mut JSContext,
487        focus_event_type: FocusEventType,
488        event_target: &EventTarget,
489        related_target: Option<&EventTarget>,
490    ) {
491        let event_name = match focus_event_type {
492            FocusEventType::Focus => "focus".into(),
493            FocusEventType::Blur => "blur".into(),
494        };
495
496        let event = FocusEvent::new(
497            &self.window,
498            event_name,
499            EventBubbles::DoesNotBubble,
500            EventCancelable::NotCancelable,
501            Some(&self.window),
502            0i32,
503            related_target,
504            CanGc::from_cx(cx),
505        );
506        let event = event.upcast::<Event>();
507        event.set_trusted(true);
508        event.fire(event_target, CanGc::from_cx(cx));
509    }
510
511    /// <https://html.spec.whatwg.org/multipage/#focus-fixup-rule>
512    /// > For each doc of docs, if the focused area of doc is not a focusable area, then run the
513    /// > focusing steps for doc's viewport, and set doc's relevant global object's navigation API's
514    /// > focus changed during ongoing navigation to false.
515    ///
516    /// TODO: Handle the "focus changed during ongoing navigation" flag.
517    pub(crate) fn perform_focus_fixup_rule(&self, cx: &mut JSContext) {
518        if self
519            .focused_area
520            .borrow()
521            .element()
522            .is_none_or(|focused| focused.is_focusable_area())
523        {
524            return;
525        }
526        self.focus(cx, FocusableArea::Viewport);
527    }
528
529    pub(crate) fn set_sequential_focus_navigation_starting_point(&self, node: &Node) {
530        self.sequential_focus_navigation_starting_point
531            .set(Some(node));
532    }
533
534    fn sequential_focus_navigation_starting_point(&self) -> Option<DomRoot<Node>> {
535        self.sequential_focus_navigation_starting_point
536            .get()
537            .filter(|node| node.is_connected())
538    }
539
540    pub(crate) fn sequential_focus_navigation_via_keyboard_event(
541        &self,
542        cx: &mut JSContext,
543        event: &KeyboardEvent,
544    ) {
545        let direction = if event.modifiers().contains(Modifiers::SHIFT) {
546            SequentialFocusDirection::Backward
547        } else {
548            SequentialFocusDirection::Forward
549        };
550
551        self.sequential_focus_navigation(cx, direction);
552    }
553
554    /// <https://html.spec.whatwg.org/multipage/#sequential-focus-navigation:currently-focused-area-of-a-top-level-traversable>
555    fn sequential_focus_navigation(&self, cx: &mut JSContext, direction: SequentialFocusDirection) {
556        // > When the user requests that focus move from the currently focused area of a top-level
557        // > traversable to the next or previous focusable area (e.g., as the default action of
558        // > pressing the tab key), or when the user requests that focus sequentially move to a
559        // > top-level traversable in the first place (e.g., from the browser's location bar), the
560        // > user agent must use the following algorithm:
561
562        // > 1. Let starting point be the currently focused area of a top-level traversable, if the
563        // > user requested to move focus sequentially from there, or else the top-level traversable
564        // > itself, if the user instead requested to move focus from outside the top-level
565        // > traversable.
566        //
567        // Note: Here `None` represents the current traversible.
568        let mut starting_point = self
569            .focused_area()
570            .element()
571            .map(|element| DomRoot::from_ref(element.upcast::<Node>()));
572
573        // > 2. If there is a sequential focus navigation starting point defined and it is inside
574        // > starting point, then let starting point be the sequential focus navigation starting point
575        // > instead.
576        if let Some(sequential_focus_navigation_starting_point) =
577            self.sequential_focus_navigation_starting_point() &&
578            starting_point.as_ref().is_none_or(|starting_point| {
579                starting_point.is_ancestor_of(&sequential_focus_navigation_starting_point)
580            })
581        {
582            starting_point = Some(sequential_focus_navigation_starting_point);
583        }
584
585        // > 3. Let direction be "forward" if the user requested the next control, and "backward" if
586        // > the user requested the previous control.
587        //
588        // Note: This is handled by the `direction` argument to this method.
589        self.sequential_focus_navigation_loop(
590            cx,
591            starting_point,
592            direction,
593            false, /* allow_focusing_viewport */
594        );
595    }
596
597    /// The inner loop ("Loop") of:
598    /// <https://html.spec.whatwg.org/multipage/#sequential-focus-navigation:currently-focused-area-of-a-top-level-traversable>
599    fn sequential_focus_navigation_loop(
600        &self,
601        cx: &mut JSContext,
602        starting_point: Option<DomRoot<Node>>,
603        direction: SequentialFocusDirection,
604        allow_focusing_viewport: bool,
605    ) {
606        // > 4. Loop: Let selection mechanism be "sequential" if starting point is a navigable or if
607        // > starting point is in its Document's sequential focus navigation order.
608        // > Otherwise, starting point is not in its Document's sequential focus navigation order;
609        // > let selection mechanism be "DOM".
610        let starting_point_is_navigable = starting_point
611            .as_ref()
612            .is_none_or(|starting_point| starting_point.is::<HTMLIFrameElement>());
613        let selection_mechanism = starting_point
614            .as_ref()
615            .and_then(|node| node.downcast::<Element>())
616            .filter(|element| element.is_sequentially_focusable())
617            .map(|element| {
618                SequentialFocusNavigationMechanism::Sequential(
619                    element.explicitly_set_tab_index().unwrap_or_default(),
620                )
621            })
622            .unwrap_or_else(|| {
623                if starting_point_is_navigable {
624                    SequentialFocusNavigationMechanism::FirstOrLast
625                } else {
626                    SequentialFocusNavigationMechanism::Dom
627                }
628            });
629
630        // > 5. Let candidate be the result of running the sequential navigation search algorithm
631        // > with starting point, direction, and selection mechanism.
632        let candidate = SequentialFocusNavigationSearch::new(
633            starting_point
634                .as_ref()
635                .and_then(|node| node.containing_focus_navigation_scope_owner())
636                .unwrap_or_else(|| FocusNavigationScopeOwner::Document(self.window.Document())),
637            direction,
638            selection_mechanism,
639            starting_point,
640        )
641        .search();
642
643        // > 6. If candidate is not null, then run the focusing steps for candidate and return.
644        if let Some(candidate) = candidate {
645            let document = self.window.Document();
646            let event_handler = document.event_handler();
647            event_handler.focus_and_scroll_to_element_for_key_event(cx, &candidate);
648            // We can't simply run the focusing steps, because:
649            //  1. The focusing steps do not scroll to the element.
650            //  2. When focus shifts to a child navigable (iframe) we have special behavior to reach
651            //     across document boundaries to focus the first focusable element in the iframe.
652            match candidate.downcast::<HTMLIFrameElement>() {
653                Some(iframe_element) => self.sequentially_focus_child_iframe_local_or_remote(
654                    cx,
655                    iframe_element,
656                    direction,
657                ),
658                None => event_handler.focus_and_scroll_to_element_for_key_event(cx, &candidate),
659            }
660            return;
661        }
662
663        // > 7. Otherwise, unset the sequential focus navigation starting point.
664        self.sequential_focus_navigation_starting_point.clear();
665
666        // This is not in the specification, but there's a difference between moving focus into
667        // a child `<iframe>` and within a Document. If no suitable focusable area can be found
668        // when moving into an `<iframe>`, we want to focus the `<iframe>`'s viewport itself.
669        if allow_focusing_viewport {
670            self.focus(cx, FocusableArea::Viewport);
671            return;
672        }
673
674        // > 8. If starting point is a top-level traversable, or a focusable area in the top-level
675        // > traversable, the user agent should transfer focus to its own controls appropriately (if
676        // > any), honouring direction, and then return.
677        // TODO: Implement this.
678        if self.window.is_top_level() {
679            return;
680        }
681
682        // > 9. Otherwise, starting point is a focusable area in a child navigable. Set starting
683        // > point to that child navigable's parent and return to the step labeled loop.
684        self.sequentially_focus_parent_local_or_remote(cx, direction);
685    }
686
687    fn sequentially_focus_child_iframe_local_or_remote(
688        &self,
689        cx: &mut JSContext,
690        iframe_element: &HTMLIFrameElement,
691        direction: SequentialFocusDirection,
692    ) {
693        if let Some(content_document) = iframe_element.GetContentDocument() {
694            // The <iframe> is in the same `ScriptThread` and we have direct access to it. We can
695            // move the focus directly.
696            content_document
697                .focus_handler()
698                .sequential_focus_from_another_document(cx, None, direction);
699        } else if let Some(browsing_context_id) = iframe_element.browsing_context_id() {
700            self.window.send_to_constellation(
701                ScriptToConstellationMessage::FocusRemoteBrowsingContext(
702                    browsing_context_id,
703                    RemoteFocusOperation::Sequential(direction, None),
704                ),
705            );
706        } else {
707            iframe_element
708                .upcast::<Node>()
709                .run_the_focusing_steps(cx, None);
710        }
711    }
712
713    fn sequentially_focus_parent_local_or_remote(
714        &self,
715        cx: &mut JSContext,
716        direction: SequentialFocusDirection,
717    ) {
718        let window_proxy = self.window.window_proxy();
719        if let Some(iframe) = window_proxy.frame_element() {
720            // The parent browsing context is in the same `ScriptThread` and we have direct access
721            // to it. We can move the focus directly.
722            let browsing_context_id = iframe
723                .downcast::<HTMLIFrameElement>()
724                .and_then(|iframe_element| iframe_element.browsing_context_id());
725            iframe
726                .owner_document()
727                .focus_handler()
728                .sequential_focus_from_another_document(cx, browsing_context_id, direction);
729        } else if let Some(browsing_context_id) = window_proxy
730            .parent()
731            .map(|parent| parent.browsing_context_id())
732        {
733            self.window.send_to_constellation(
734                ScriptToConstellationMessage::FocusRemoteBrowsingContext(
735                    browsing_context_id,
736                    RemoteFocusOperation::Sequential(
737                        direction,
738                        Some(window_proxy.browsing_context_id()),
739                    ),
740                ),
741            );
742        }
743    }
744
745    pub(crate) fn sequential_focus_from_another_document(
746        &self,
747        cx: &mut JSContext,
748        browsing_context_id: Option<BrowsingContextId>,
749        direction: SequentialFocusDirection,
750    ) {
751        let _realm = enter_realm(&*self.window);
752        let starting_point = browsing_context_id.and_then(|browsing_context_id| {
753            self.window
754                .Document()
755                .iframes()
756                .get(browsing_context_id)
757                .map(|iframe| DomRoot::from_ref(iframe.element.upcast::<Node>()))
758        });
759        self.sequential_focus_navigation_loop(
760            cx,
761            starting_point,
762            direction,
763            true, /* allow focusing viewport */
764        );
765    }
766}
767
768/// <https://html.spec.whatwg.org/multipage/#selection-mechanism>
769///
770/// This also incorporates the case where the starting point is a navigable
771/// as that is a distinct set of behaviors from the two kinds of mechanisms
772/// listed in the specification.
773#[derive(Clone, Copy, Debug)]
774pub(crate) enum SequentialFocusNavigationMechanism {
775    Dom,
776    Sequential(i32 /* focused_element_tab_index */),
777    /// This case isn't mentioned explicitly in the specification, but it's implied. It works
778    /// like `Sequential`, but without a starting point. This kind of search will return the
779    /// first or last (depending on direction) sequentially focusable element in sequential
780    /// focus order. This is used in two situations:
781    ///
782    ///  - When the starting point is a navigable
783    ///  - When descending into a nested focus scope
784    FirstOrLast,
785}
786
787#[derive(PartialEq)]
788enum Continue {
789    Yes,
790    No,
791}
792
793#[derive(PartialEq)]
794pub(crate) enum SequentialFocusNavigationSearchContext {
795    /// The focus scope that initiated this search. Containing contexts and nested contexts can
796    /// both be searched.
797    Original,
798    /// The search has descended into a nested search scope. Containing contexts should never
799    /// be searched.
800    Nested,
801    /// The search has ascended to search a containing search scope. The starting point's focus
802    /// scope should never be searched to avoid cycles.
803    Containing,
804}
805
806/// This structure is used to do a traversal search of the DOM in order to find an
807/// appropriate target when doing sequential focus navigation, such as when handling
808/// tab key presses.
809///
810/// The specification talks about the [flattened tabindex-ordered focus navigation scope],
811/// which represents all of the [tabindex-ordered focus navigation scope]s of a particular
812/// page, flattened into a single list of all the sequentially focusable areas of the
813/// page. Then, the specification describes how to search this list during sequential focus
814/// navigation.
815///
816/// The choice that Servo and other browsers make is to trade updating this flattened list
817/// during every DOM mutation (frequent) with a DOM traversal of, potentially, the entire
818/// document during sequential focus navigation (infrequent).
819///
820/// The search done via [`SequentialFocusNavigationSearch`] matches the semantics of the
821/// flattened tabindex-ordered focus navigation scope without having to maintain the
822/// flattened list. It uses a series of nested traversals (one per focus scope) that
823/// only considers each focusable area of a page at most once.
824///
825/// The search performs a linear DOM traversal starting at the containing focus scope of
826/// the search start point. When encountering a nested focus scope, if that scope could
827/// contain the final target for the search, the search recurses into the nested scope. If
828/// the search reaches the end of a focus scope without finding a candidate, the search
829/// continues in the focus scope's containing scope (though never re-ascending back into a
830/// scope it recursed from).
831///
832/// [flattened tabindex-ordered focus navigation scope]: https://html.spec.whatwg.org/multipage/#flattened-tabindex-ordered-focus-navigation-scope
833/// [tabindex-ordered focus navigation scope]: https://html.spec.whatwg.org/multipage/#tabindex-ordered-focus-navigation-scope
834pub(crate) struct SequentialFocusNavigationSearch {
835    focus_navigation_scope_owner: FocusNavigationScopeOwner,
836    direction: SequentialFocusDirection,
837    mechanism: SequentialFocusNavigationMechanism,
838    starting_point: Option<DomRoot<Node>>,
839    current_winner: Option<(DomRoot<Element>, i32)>,
840    passed_starting_point: bool,
841    search_context: SequentialFocusNavigationSearchContext,
842}
843
844impl SequentialFocusNavigationSearch {
845    pub(crate) fn new(
846        focus_navigation_scope_owner: FocusNavigationScopeOwner,
847        direction: SequentialFocusDirection,
848        mechanism: SequentialFocusNavigationMechanism,
849        starting_point: Option<DomRoot<Node>>,
850    ) -> Self {
851        // If there's no starting point, the starting point is actually the root element, which
852        // we always have passed.
853        let passed_starting_point = starting_point.is_none();
854        Self {
855            focus_navigation_scope_owner,
856            direction,
857            mechanism,
858            starting_point,
859            current_winner: Default::default(),
860            passed_starting_point,
861            search_context: SequentialFocusNavigationSearchContext::Original,
862        }
863    }
864
865    pub(crate) fn search(mut self) -> Option<DomRoot<Element>> {
866        for node in self.focus_navigation_scope_owner.iterator() {
867            if self.process_node(&node) == Continue::No {
868                break;
869            }
870        }
871
872        if let Some(winner) = self.current_winner.take() {
873            return Some(winner.0);
874        }
875
876        // If searching a nested focus navigation scope, never try to search the containing
877        // scope, as that will lead to an endless cycle.
878        if self.search_context != SequentialFocusNavigationSearchContext::Nested {
879            return self.maybe_search_in_containing_focus_navigation_scope();
880        }
881
882        None
883    }
884
885    fn maybe_search_in_containing_focus_navigation_scope(&self) -> Option<DomRoot<Element>> {
886        let containing_node = self.focus_navigation_scope_owner.node();
887        let containing_focus_navigation_scope_owner =
888            containing_node.containing_focus_navigation_scope_owner()?;
889
890        let tab_index = containing_node
891            .downcast::<Element>()?
892            .explicitly_set_tab_index()
893            .unwrap_or_default();
894        let mechanism = match &self.mechanism {
895            // If the traversal was sequential, but the containing focus navigation scope owner was
896            // explicitly marked as not sequentially focusable, the search in the containing scope
897            // needs to work like a DOM traversal i.e. take the first sequentially focusable target
898            // after this one in the parent traversal.
899            SequentialFocusNavigationMechanism::Sequential(..) if tab_index == -1 => {
900                SequentialFocusNavigationMechanism::Dom
901            },
902            SequentialFocusNavigationMechanism::Sequential(..) => {
903                SequentialFocusNavigationMechanism::Sequential(tab_index)
904            },
905            mechanism => *mechanism,
906        };
907
908        if self.direction == SequentialFocusDirection::Backward &&
909            let Some(containing_element) = containing_node.downcast::<Element>() &&
910            containing_element.is_sequentially_focusable()
911        {
912            return Some(DomRoot::from_ref(containing_element));
913        }
914
915        Self {
916            focus_navigation_scope_owner: containing_focus_navigation_scope_owner,
917            direction: self.direction,
918            mechanism,
919            starting_point: Some(DomRoot::from_ref(containing_node)),
920            current_winner: Default::default(),
921            passed_starting_point: false,
922            search_context: SequentialFocusNavigationSearchContext::Containing,
923        }
924        .search()
925    }
926
927    fn process_node(&mut self, node: &Node) -> Continue {
928        if Some(node) == self.starting_point.as_deref() {
929            self.passed_starting_point = true;
930        } else if self.process_node_as_sequentially_focusable_node(node) == Continue::No {
931            return Continue::No;
932        }
933
934        self.process_node_as_focus_scope_owner(node)
935    }
936
937    /// If this node is sequentially focusable, consider whether or not to accept it
938    /// as the new winner.
939    fn process_node_as_sequentially_focusable_node(&mut self, node: &Node) -> Continue {
940        let Some(element) = node.downcast::<Element>() else {
941            return Continue::Yes;
942        };
943        if !element.is_sequentially_focusable() {
944            return Continue::Yes;
945        }
946
947        let tab_index = element.explicitly_set_tab_index().unwrap_or_default();
948        let (is_new_winner, should_continue) = self.process_candidate_with_tab_index(tab_index);
949        if is_new_winner {
950            self.current_winner = Some((DomRoot::from_ref(element), tab_index));
951        }
952        should_continue
953    }
954
955    /// If this node itself forms a nested sequential focus scope, decide whether or
956    /// not to descend and consider its contained focusable areas as candidates.
957    fn process_node_as_focus_scope_owner(&mut self, node: &Node) -> Continue {
958        // Never try to recurse into the same focus scope that we are in. This path
959        // might be reached if we are in the root focus scope where the document is
960        // one of the nodes processed.
961        if self.focus_navigation_scope_owner.node() == node {
962            return Continue::Yes;
963        }
964
965        // If the search has ascended into a containing scope, never try to search back down
966        // into the scope that originated this part of the search. Otherwise the search would
967        // cycle endlessly.
968        if Some(node) == self.starting_point.as_deref() &&
969            self.search_context == SequentialFocusNavigationSearchContext::Containing
970        {
971            return Continue::Yes;
972        }
973
974        let Some(focus_navigation_scope_owner) = node.as_focus_navigation_scope_owner() else {
975            return Continue::Yes;
976        };
977
978        // The candidate inherits the tab index of the node that establishes its containing
979        // sequential focus navigation scope.
980        let tab_index = focus_navigation_scope_owner
981            .node()
982            .downcast::<Element>()
983            .and_then(Element::explicitly_set_tab_index)
984            .unwrap_or_default();
985        let (is_new_winner, should_continue) = self.process_candidate_with_tab_index(tab_index);
986        if !is_new_winner {
987            return should_continue;
988        }
989
990        let mechanism = match self.mechanism {
991            // If we were searching without regard to sequential focus order, keep doing that.
992            SequentialFocusNavigationMechanism::Dom => SequentialFocusNavigationMechanism::Dom,
993            // If we were searching taking into account sequential focus order, keep doing that, but
994            // take the first candidate in sequential focus order without regard to the outer scope's
995            // starting point or tab index.
996            _ => SequentialFocusNavigationMechanism::FirstOrLast,
997        };
998
999        let element = Self {
1000            focus_navigation_scope_owner,
1001            direction: self.direction,
1002            mechanism,
1003            starting_point: None,
1004            current_winner: Default::default(),
1005            passed_starting_point: self.passed_starting_point,
1006            search_context: SequentialFocusNavigationSearchContext::Nested,
1007        }
1008        .search();
1009
1010        let Some(element) = element else {
1011            return Continue::Yes;
1012        };
1013
1014        self.current_winner = Some((element, tab_index));
1015        should_continue
1016    }
1017
1018    /// Process the node or focus scope owner with the provided tab index according to this
1019    /// search's search mechanism. Returns a boolean that is true if this candidate is the new
1020    /// winner and a [`Continue`] which says whether to keep searching or stop.
1021    fn process_candidate_with_tab_index(&mut self, candidate_tab_index: i32) -> (bool, Continue) {
1022        match self.mechanism {
1023            SequentialFocusNavigationMechanism::Dom => self.process_element_for_dom_traversal(),
1024            SequentialFocusNavigationMechanism::Sequential(focused_element_tab_index) => self
1025                .process_element_for_sequential_traversal(
1026                    candidate_tab_index,
1027                    focused_element_tab_index,
1028                ),
1029            SequentialFocusNavigationMechanism::FirstOrLast => (
1030                self.process_element_for_first_or_last_traversal(candidate_tab_index),
1031                Continue::Yes,
1032            ),
1033        }
1034    }
1035
1036    /// Process the node or focus scope owner, given the state of [`Self::passed_starting_point`]
1037    /// with for searches with the [`SequentialFocusNavigationMechanism::Dom`] search mechanism.
1038    /// Returns a boolean that is true if this candidate is the new winner and a [`Continue`] which
1039    /// says whether to keep searching or stop.
1040    fn process_element_for_dom_traversal(&self) -> (bool, Continue) {
1041        match self.direction {
1042            // direction is "forward"
1043            // > Let candidate be the first suitable sequentially focusable area after starting point,
1044            // > in starting point's Document's sequential focus navigation order, if any; or else
1045            // > null
1046            SequentialFocusDirection::Forward if self.passed_starting_point => (true, Continue::No),
1047            // If searching forward, do not consider anything until passing the starting point.
1048            SequentialFocusDirection::Forward => (false, Continue::Yes),
1049            // direction is "backward"
1050            // > Let candidate be the last suitable sequentially focusable area before starting
1051            // > point, in starting point's Document's sequential focus navigation order, if any; or
1052            // > else null
1053            SequentialFocusDirection::Backward if !self.passed_starting_point => {
1054                (true, Continue::Yes)
1055            },
1056            // There is no possible winner after the starting point when searching backward.
1057            SequentialFocusDirection::Backward => (false, Continue::No),
1058        }
1059    }
1060
1061    /// Process the node or focus scope owner with the provided tab index for searches with the
1062    /// [`SequentialFocusNavigationMechanism::FirstOrLast`] search mechanism. Returns a boolean
1063    /// that is true if this candidate is the new winner.
1064    fn process_element_for_first_or_last_traversal(
1065        &self,
1066        candidate_element_tab_index: i32,
1067    ) -> bool {
1068        let Some((_, winning_tab_index)) = self.current_winner else {
1069            return true;
1070        };
1071
1072        let candidate_and_current_winner_ordering =
1073            compare_tab_indices(candidate_element_tab_index, winning_tab_index);
1074        match self.direction {
1075            // direction is "forward"
1076            // > Let candidate be the first suitable sequentially focusable area in starting point's
1077            // > active document, if any; or else null
1078            //
1079            // There's an ambiguity in the specification here. In this case it says to choose
1080            // the "first suitable sequentially focusable area." It's possible to interpret this
1081            // as the first in DOM order, but browsers seem to agree to follow tab index
1082            // order instead.
1083            //
1084            // Pick the lowest, prioritizing the earlier node when equal.
1085            SequentialFocusDirection::Forward
1086                if candidate_and_current_winner_ordering == Ordering::Less =>
1087            {
1088                true
1089            },
1090            // direction is "backward"
1091            // > Let candidate be the last suitable sequentially focusable area in starting point's
1092            // > active document, if any; or else null
1093            //
1094            // There's an ambiguity in the specification here. In this case it says to choose
1095            // the "last suitable sequentially focusable area" It's possible to interpret this
1096            // as the first in DOM order, but browsers seem to agree to following tab index
1097            // order instead.
1098            //
1099            // Pick the highest, prioritizing the later node when equal.
1100            SequentialFocusDirection::Backward
1101                if candidate_and_current_winner_ordering != Ordering::Less =>
1102            {
1103                true
1104            },
1105            _ => false,
1106        }
1107    }
1108
1109    /// Process the node or focus scope owner with the provided tab index for searches with the
1110    /// [`SequentialFocusNavigationMechanism::Sequential`] search mechanism. Returns a boolean
1111    /// that is true if this candidate is the new winner and a [`Continue`] which says whether to
1112    /// keep searching or stop.
1113    fn process_element_for_sequential_traversal(
1114        &self,
1115        candidate_element_tab_index: i32,
1116        focused_element_tab_index: i32,
1117    ) -> (bool, Continue) {
1118        let candidate_and_focused_ordering =
1119            compare_tab_indices(candidate_element_tab_index, focused_element_tab_index);
1120        match self.direction {
1121            SequentialFocusDirection::Forward => {
1122                // If moving forward the first element with equal tab index after the current
1123                // element is the winner.
1124                if self.passed_starting_point && candidate_and_focused_ordering == Ordering::Equal {
1125                    return (true, Continue::No);
1126                }
1127                // If the candidate element does not have a greater tab index, then discard it.
1128                if candidate_and_focused_ordering != Ordering::Greater {
1129                    return (false, Continue::Yes);
1130                }
1131                let Some((_, winning_tab_index)) = self.current_winner else {
1132                    // If this candidate has a tab index which is one greater than the current
1133                    // tab index, then we know it is the winner, because we give precedence to
1134                    // elements earlier in the DOM.
1135                    if candidate_element_tab_index == focused_element_tab_index + 1 {
1136                        return (true, Continue::No);
1137                    }
1138
1139                    return (true, Continue::Yes);
1140                };
1141
1142                // If the candidate element has a lesser tab index than the current winner,
1143                // then it becomes the winner.
1144                let should_select =
1145                    compare_tab_indices(candidate_element_tab_index, winning_tab_index) ==
1146                        Ordering::Less;
1147
1148                (should_select, Continue::Yes)
1149            },
1150            SequentialFocusDirection::Backward => {
1151                // If moving backward the last element with an equal tab index that precedes
1152                // the focused element in the DOM is the winner.
1153                if !self.passed_starting_point && candidate_and_focused_ordering == Ordering::Equal
1154                {
1155                    return (true, Continue::Yes);
1156                }
1157                // If the candidate does not have a lesser tab index, then discard it.
1158                if candidate_and_focused_ordering != Ordering::Less {
1159                    return (false, Continue::Yes);
1160                }
1161                let Some((_, winning_tab_index)) = self.current_winner else {
1162                    return (true, Continue::Yes);
1163                };
1164                // If the candidate element's tab index is not less than the current winner,
1165                // then it becomes the new winner. This means that when the tab indices are
1166                // equal, we give preference to the last one in DOM order.
1167                let should_select =
1168                    compare_tab_indices(candidate_element_tab_index, winning_tab_index) !=
1169                        Ordering::Less;
1170                (should_select, Continue::Yes)
1171            },
1172        }
1173    }
1174}
1175
1176/// Compare two tab indices according to <https://html.spec.whatwg.org/multipage/#tabindex-value>.
1177///
1178/// `Ordering::Less`: The index should come before the other in sequential focus order.
1179/// `Ordering::Equal`: The two indices should be processed in DOM order, respecting focus direction
1180/// and focus scopes.
1181/// `Ordering::Greater`: The index should come after the other in sequential focus order.
1182///
1183/// Note that a tabindex of 0 should come after all others, which is essentially why we need this
1184/// function.
1185fn compare_tab_indices(a: i32, b: i32) -> Ordering {
1186    if a == b {
1187        Ordering::Equal
1188    } else if a == 0 {
1189        Ordering::Greater
1190    } else if b == 0 {
1191        Ordering::Less
1192    } else {
1193        a.cmp(&b)
1194    }
1195}