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