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