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};
6
7use bitflags::bitflags;
8use embedder_traits::FocusSequenceNumber;
9use js::context::JSContext;
10use script_bindings::codegen::GenericBindings::HTMLIFrameElementBinding::HTMLIFrameElementMethods;
11use script_bindings::codegen::GenericBindings::ShadowRootBinding::ShadowRootMethods;
12use script_bindings::codegen::GenericBindings::WindowBinding::WindowMethods;
13use script_bindings::inheritance::Castable;
14use script_bindings::root::{Dom, DomRoot};
15use script_bindings::script_runtime::CanGc;
16use servo_base::id::BrowsingContextId;
17use servo_constellation_traits::{
18    RemoteFocusOperation, ScriptToConstellationMessage, SequentialFocusDirection,
19};
20
21use crate::dom::bindings::cell::DomRefCell;
22use crate::dom::focusevent::FocusEventType;
23use crate::dom::types::{Element, EventTarget, FocusEvent, HTMLElement, HTMLIFrameElement, Window};
24use crate::dom::{Document, Event, EventBubbles, EventCancelable, Node, NodeTraits};
25use crate::realms::enter_realm;
26
27/// The kind of focusable area a [`FocusableArea`] is. A [`FocusableArea`] may be click focusable,
28/// sequentially focusable, or both.
29#[derive(Clone, Copy, Debug, Default, JSTraceable, MallocSizeOf, PartialEq)]
30pub(crate) struct FocusableAreaKind(u8);
31
32bitflags! {
33    impl FocusableAreaKind: u8 {
34        /// <https://html.spec.whatwg.org/multipage/#click-focusable>
35        ///
36        /// > A focusable area is said to be click focusable if the user agent determines that it is
37        /// > click focusable. User agents should consider focusable areas with non-null tabindex values
38        /// > to be click focusable.
39        const Click = 1 << 0;
40        /// <https://html.spec.whatwg.org/multipage/#sequentially-focusable>.
41        ///
42        /// > A focusable area is said to be sequentially focusable if it is included in its
43        /// > Document's sequential focus navigation order and the user agent determines that it is
44        /// > sequentially focusable.
45        const Sequential = 1 << 1;
46    }
47}
48
49/// <https://html.spec.whatwg.org/multipage/#focusable-area>
50#[derive(Clone, Default, JSTraceable, MallocSizeOf, PartialEq)]
51pub(crate) enum FocusableArea {
52    Node {
53        node: DomRoot<Node>,
54        kind: FocusableAreaKind,
55    },
56    /// The viewport of an `<iframe>` element in its containing `Document`. `<iframe>`s
57    /// are focusable areas, but have special behavior when focusing.
58    IFrameViewport {
59        iframe_element: DomRoot<HTMLIFrameElement>,
60        kind: FocusableAreaKind,
61    },
62    #[default]
63    Viewport,
64}
65
66impl std::fmt::Debug for FocusableArea {
67    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68        match self {
69            Self::Node { node, kind } => f
70                .debug_struct("Node")
71                .field("node", node)
72                .field("kind", kind)
73                .finish(),
74            Self::IFrameViewport {
75                iframe_element,
76                kind,
77            } => f
78                .debug_struct("IFrameViewport")
79                .field("pipeline", &iframe_element.pipeline_id())
80                .field("kind", kind)
81                .finish(),
82            Self::Viewport => write!(f, "Viewport"),
83        }
84    }
85}
86
87impl FocusableArea {
88    pub(crate) fn kind(&self) -> FocusableAreaKind {
89        match self {
90            Self::Node { kind, .. } | Self::IFrameViewport { kind, .. } => *kind,
91            Self::Viewport => FocusableAreaKind::Click | FocusableAreaKind::Sequential,
92        }
93    }
94
95    /// If this focusable area is a node, return it as an [`Element`] if it is possible, otherwise
96    /// return `None`. This is the [`Element`] to use for applying `:focus` state and for firing
97    /// `blur` and `focus` events if any.
98    ///
99    /// Note: This is currently in a transitional state while the code moves more toward the
100    /// specification.
101    pub(crate) fn element(&self) -> Option<&Element> {
102        match self {
103            Self::Node { node, .. } => node.downcast(),
104            Self::IFrameViewport { iframe_element, .. } => Some(iframe_element.upcast()),
105            Self::Viewport => None,
106        }
107    }
108
109    /// <https://html.spec.whatwg.org/multipage/#dom-anchor>
110    pub(crate) fn dom_anchor(&self, document: &Document) -> DomRoot<Node> {
111        match self {
112            Self::Node { node, .. } => node.clone(),
113            Self::IFrameViewport { iframe_element, .. } => {
114                DomRoot::from_ref(iframe_element.upcast())
115            },
116            Self::Viewport => DomRoot::from_ref(document.upcast()),
117        }
118    }
119
120    pub(crate) fn focus_chain(&self) -> Vec<FocusableArea> {
121        match self {
122            FocusableArea::Node { .. } | FocusableArea::IFrameViewport { .. } => {
123                vec![self.clone(), FocusableArea::Viewport]
124            },
125            FocusableArea::Viewport => vec![self.clone()],
126        }
127    }
128}
129
130/// The [`DocumentFocusHandler`] is a structure responsible for handling and storing data related to
131/// focus for the `Document`. It exists to decrease the size of the `Document`.
132/// structure.
133#[derive(JSTraceable, MallocSizeOf)]
134#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
135pub(crate) struct DocumentFocusHandler {
136    /// The [`Window`] element for this [`DocumentFocusHandler`].
137    window: Dom<Window>,
138    /// The focused area of the [`Document`].
139    ///
140    /// <https://html.spec.whatwg.org/multipage/#focused-area-of-the-document>
141    focused_area: DomRefCell<FocusableArea>,
142    /// The last sequence number sent to the constellation.
143    #[no_trace]
144    focus_sequence: Cell<FocusSequenceNumber>,
145    /// Indicates whether the container is included in the top-level browsing
146    /// context's focus chain (not considering system focus). Permanently `true`
147    /// for a top-level document.
148    has_focus: Cell<bool>,
149}
150
151impl DocumentFocusHandler {
152    pub(crate) fn new(window: &Window, has_focus: bool) -> Self {
153        Self {
154            window: Dom::from_ref(window),
155            focused_area: Default::default(),
156            focus_sequence: Cell::new(FocusSequenceNumber::default()),
157            has_focus: Cell::new(has_focus),
158        }
159    }
160
161    pub(crate) fn has_focus(&self) -> bool {
162        self.has_focus.get()
163    }
164
165    pub(crate) fn set_has_focus(&self, has_focus: bool) {
166        self.has_focus.set(has_focus);
167    }
168
169    /// Return the element that currently has focus. If `None` is returned the viewport itself has focus.
170    pub(crate) fn focused_area<'a>(&'a self) -> Ref<'a, FocusableArea> {
171        let focused_area = self.focused_area.borrow();
172        Ref::map(focused_area, |focused_area| focused_area)
173    }
174
175    /// Set the element that currently has focus and update the focus state for both the previously
176    /// set element (if any) and the new one, as well as the new one. This will not do anything if
177    /// the new element is the same as the previous one. Note that this *will not* fire any focus
178    /// events. If that is necessary the [`DocumentFocusHandler::focus`] should be used.
179    pub(crate) fn set_focused_area(&self, new_focusable_area: FocusableArea) {
180        if new_focusable_area == *self.focused_area.borrow() {
181            return;
182        }
183
184        // From <https://html.spec.whatwg.org/multipage/#selector-focus>
185        // > For the purposes of the CSS :focus pseudo-class, an element has the focus when:
186        // >  - it is not itself a navigable container; and
187        // >  - any of the following are true:
188        // >    - it is one of the elements listed in the current focus chain of the top-level
189        // >      traversable; or
190        // >    - its shadow root shadowRoot is not null and shadowRoot is the root of at least one
191        // >      element that has the focus.
192        //
193        // We are trying to accomplish the last requirement here, by walking up the tree and
194        // marking each shadow host as focused.
195        fn recursively_set_focus_status(element: &Element, new_state: bool) {
196            element.set_focus_state(new_state);
197
198            let Some(shadow_root) = element.containing_shadow_root() else {
199                return;
200            };
201            recursively_set_focus_status(&shadow_root.Host(), new_state);
202        }
203
204        if let Some(previously_focused_element) = self.focused_area.borrow().element() {
205            recursively_set_focus_status(previously_focused_element, false);
206        }
207        if let Some(newly_focused_element) = new_focusable_area.element() {
208            recursively_set_focus_status(newly_focused_element, true);
209        }
210
211        *self.focused_area.borrow_mut() = new_focusable_area;
212    }
213
214    /// Get the last sequence number sent to the constellation.
215    ///
216    /// Received focus-related messages with sequence numbers less than the one
217    /// returned by this method must be discarded.
218    pub fn focus_sequence(&self) -> FocusSequenceNumber {
219        self.focus_sequence.get()
220    }
221
222    /// Generate the next sequence number for focus-related messages.
223    fn increment_fetch_focus_sequence(&self) -> FocusSequenceNumber {
224        self.focus_sequence.set(FocusSequenceNumber(
225            self.focus_sequence
226                .get()
227                .0
228                .checked_add(1)
229                .expect("too many focus messages have been sent"),
230        ));
231        self.focus_sequence.get()
232    }
233
234    /// <https://html.spec.whatwg.org/multipage/#current-focus-chain-of-a-top-level-traversable>
235    pub(crate) fn current_focus_chain(&self) -> Vec<FocusableArea> {
236        // > The current focus chain of a top-level traversable is the focus chain of the
237        // > currently focused area of traversable, if traversable is non-null, or an empty list
238        // > otherwise.
239
240        // We cannot easily get the full focus chain of the top-level traversable, so we just
241        // get the bits that intersect with this `Document`. The rest will be handled
242        // internally in [`Self::focus_update_steps`].
243        if !self.has_focus() {
244            return vec![];
245        }
246        self.focused_area().focus_chain()
247    }
248
249    /// Reassign the focus context to the element that last requested focus during this
250    /// transaction, or the document if no elements requested it.
251    pub(crate) fn focus(&self, cx: &mut JSContext, new_focus_target: FocusableArea) {
252        let old_focus_chain = self.current_focus_chain();
253        let new_focus_chain = new_focus_target.focus_chain();
254        self.focus_update_steps(cx, new_focus_chain, old_focus_chain, &new_focus_target);
255
256        // Advertise the change in the focus chain.
257        // <https://html.spec.whatwg.org/multipage/#focus-chain>
258        // <https://html.spec.whatwg.org/multipage/#focusing-steps>
259        //
260        // TODO: Integrate this into the "focus update steps."
261        //
262        // If the top-level BC doesn't have system focus, this won't
263        // have an immediate effect, but it will when we gain system
264        // focus again. Therefore we still have to send `ScriptMsg::
265        // Focus`.
266        //
267        // When a container with a non-null nested browsing context is
268        // focused, its active document becomes the focused area of the
269        // top-level browsing context instead. Therefore we need to let
270        // the constellation know if such a container is focused.
271        //
272        // > The focusing steps for an object `new focus target` [...]
273        // >
274        // >  3. If `new focus target` is a browsing context container
275        // >     with non-null nested browsing context, then set
276        // >     `new focus target` to the nested browsing context's
277        // >     active document.
278        let child_browsing_context_id = match new_focus_target {
279            FocusableArea::IFrameViewport { iframe_element, .. } => {
280                iframe_element.browsing_context_id()
281            },
282            _ => None,
283        };
284        let sequence = self.increment_fetch_focus_sequence();
285
286        debug!(
287            "Advertising the focus request to the constellation \
288                        with sequence number {sequence:?} and child \
289                        {child_browsing_context_id:?}",
290        );
291        self.window.send_to_constellation(
292            ScriptToConstellationMessage::FocusAncestorBrowsingContextsForFocusingSteps(
293                child_browsing_context_id,
294                sequence,
295            ),
296        );
297    }
298
299    /// <https://html.spec.whatwg.org/multipage/#focus-update-steps>
300    pub(crate) fn focus_update_steps(
301        &self,
302        cx: &mut JSContext,
303        mut new_focus_chain: Vec<FocusableArea>,
304        mut old_focus_chain: Vec<FocusableArea>,
305        new_focus_target: &FocusableArea,
306    ) {
307        let new_focus_chain_was_empty = new_focus_chain.is_empty();
308
309        // Step 1: If the last entry in old chain and the last entry in new chain are the same,
310        // pop the last entry from old chain and the last entry from new chain and redo this
311        // step.
312        //
313        // We avoid recursion here.
314        while let (Some(last_new), Some(last_old)) =
315            (new_focus_chain.last(), old_focus_chain.last())
316        {
317            if last_new == last_old {
318                new_focus_chain.pop();
319                old_focus_chain.pop();
320            } else {
321                break;
322            }
323        }
324
325        // If the two focus chains are both empty, focus hasn't changed. This isn't in the
326        // specification, but we must do it because we set the focused area to the viewport
327        // before blurring. If no focus changes, that would mean the currently focused element
328        // loses focus.
329        if old_focus_chain.is_empty() && new_focus_chain.is_empty() {
330            return;
331        }
332        // Although the "focusing steps" in the HTML specification say to wait until after firing
333        // the "blur" event to change the currently focused area of the Document, browsers tend
334        // to set it to the viewport before firing the "blur" event.
335        //
336        // See https://github.com/whatwg/html/issues/1569
337        self.set_focused_area(FocusableArea::Viewport);
338
339        // Step 2: For each entry entry in old chain, in order, run these substeps:
340        // Note: `old_focus_chain` might be empty!
341        let last_old_focus_chain_entry = old_focus_chain.len().saturating_sub(1);
342        for (index, entry) in old_focus_chain.iter().enumerate() {
343            // Step 2.1: If entry is an input element, and the change event applies to the element,
344            // and the element does not have a defined activation behavior, and the user has
345            // changed the element's value or its list of selected files while the control was
346            // focused without committing that change (such that it is different to what it was
347            // when the control was first focused), then:
348            // Step 2.1.1: Set entry's user validity to true.
349            // Step 2.1.2: Fire an event named change at the element, with the bubbles attribute initialized to true.
350            // TODO: Implement this.
351
352            // Step 2.2:
353            // - If entry is an element, let blur event target be entry.
354            // - If entry is a Document object, let blur event target be that Document object's
355            //    relevant global object.
356            // - Otherwise, let blur event target be null.
357            //
358            // Note: We always send focus and blur events for `<iframe>` elements, but other
359            // browsers only seem to do that conditionally. This needs a bit more research.
360            let blur_event_target = match entry {
361                FocusableArea::Node { node, .. } => Some(node.upcast::<EventTarget>()),
362                FocusableArea::IFrameViewport { iframe_element, .. } => {
363                    Some(iframe_element.upcast())
364                },
365                FocusableArea::Viewport => Some(self.window.upcast::<EventTarget>()),
366            };
367
368            // Step 2.3: If entry is the last entry in old chain, and entry is an Element, and
369            // the last entry in new chain is also an Element, then let related blur target be
370            // the last entry in new chain. Otherwise, let related blur target be null.
371            //
372            // Note: This can only happen when the focused `Document` doesn't change and we are
373            // moving focus from one element to another. These elements are the last in the chain
374            // because of the popping we do at the start of these steps.
375            let related_blur_target = match new_focus_chain.last() {
376                Some(FocusableArea::Node { node, .. })
377                    if index == last_old_focus_chain_entry &&
378                        matches!(entry, FocusableArea::Node { .. }) =>
379                {
380                    Some(node.upcast())
381                },
382                _ => None,
383            };
384
385            // Step 2.4: If blur event target is not null, fire a focus event named blur at
386            // blur event target, with related blur target as the related target.
387            if let Some(blur_event_target) = blur_event_target {
388                self.fire_focus_event(
389                    cx,
390                    FocusEventType::Blur,
391                    blur_event_target,
392                    related_blur_target,
393                );
394            }
395        }
396
397        // Step 3: Apply any relevant platform-specific conventions for focusing new focus
398        // target. (For example, some platforms select the contents of a text control when that
399        // control is focused.)
400        if &*self.focused_area() != new_focus_target {
401            if let Some(html_element) = new_focus_target
402                .element()
403                .and_then(|element| element.downcast::<HTMLElement>())
404            {
405                html_element.handle_focus_state_for_contenteditable(cx);
406            }
407        }
408
409        self.set_has_focus(!new_focus_chain_was_empty);
410
411        // Step 4: For each entry entry in new chain, in reverse order, run these substeps:
412        // Note: `new_focus_chain` might be empty!
413        let last_new_focus_chain_entry = new_focus_chain.len().saturating_sub(1); // Might be empty, so calculated here.
414        for (index, entry) in new_focus_chain.iter().enumerate().rev() {
415            // Step 4.1: If entry is a focusable area, and the focused area of the document is
416            // not entry:
417            //
418            // Here we deviate from the specification a bit, as all focus chain elements are
419            // focusable areas currently. We just assume that it means the first entry of the
420            // chain, which is the new focus target
421            if index == 0 {
422                // Step 4.1.1: Set document's relevant global object's navigation API's focus
423                // changed during ongoing navigation to true.
424                // TODO: Implement this.
425
426                // Step 4.1.2: Designate entry as the focused area of the document.
427                self.set_focused_area(entry.clone());
428            }
429
430            // Step 4.2:
431            // - If entry is an element, let focus event target be entry.
432            // - If entry is a Document object, let focus event target be that Document
433            //   object's relevant global object.
434            // - Otherwise, let focus event target be null.
435            //
436            // Note: We always send focus and blur events for `<iframe>` elements, but other
437            // browsers only seem to do that conditionally. This needs a bit more research.
438            let focus_event_target = match entry {
439                FocusableArea::Node { node, .. } => Some(node.upcast::<EventTarget>()),
440                FocusableArea::IFrameViewport { iframe_element, .. } => {
441                    Some(iframe_element.upcast())
442                },
443                FocusableArea::Viewport => Some(self.window.upcast::<EventTarget>()),
444            };
445
446            // Step 4.3: If entry is the last entry in new chain, and entry is an Element, and
447            // the last entry in old chain is also an Element, then let related focus target be
448            // the last entry in old chain. Otherwise, let related focus target be null.
449            //
450            // Note: This can only happen when the focused `Document` doesn't change and we are
451            // moving focus from one element to another. These elements are the last in the chain
452            // because of the popping we do at the start of these steps.
453            let related_focus_target = match old_focus_chain.last() {
454                Some(FocusableArea::Node { node, .. })
455                    if index == last_new_focus_chain_entry &&
456                        matches!(entry, FocusableArea::Node { .. }) =>
457                {
458                    Some(node.upcast())
459                },
460                _ => None,
461            };
462
463            // Step 4.4: If focus event target is not null, fire a focus event named focus at
464            // focus event target, with related focus target as the related target.
465            if let Some(focus_event_target) = focus_event_target {
466                self.fire_focus_event(
467                    cx,
468                    FocusEventType::Focus,
469                    focus_event_target,
470                    related_focus_target,
471                );
472            }
473        }
474    }
475
476    /// <https://html.spec.whatwg.org/multipage/#fire-a-focus-event>
477    pub(crate) fn fire_focus_event(
478        &self,
479        cx: &mut JSContext,
480        focus_event_type: FocusEventType,
481        event_target: &EventTarget,
482        related_target: Option<&EventTarget>,
483    ) {
484        let event_name = match focus_event_type {
485            FocusEventType::Focus => "focus".into(),
486            FocusEventType::Blur => "blur".into(),
487        };
488
489        let event = FocusEvent::new(
490            &self.window,
491            event_name,
492            EventBubbles::DoesNotBubble,
493            EventCancelable::NotCancelable,
494            Some(&self.window),
495            0i32,
496            related_target,
497            CanGc::from_cx(cx),
498        );
499        let event = event.upcast::<Event>();
500        event.set_trusted(true);
501        event.fire(event_target, CanGc::from_cx(cx));
502    }
503
504    /// <https://html.spec.whatwg.org/multipage/#focus-fixup-rule>
505    /// > For each doc of docs, if the focused area of doc is not a focusable area, then run the
506    /// > focusing steps for doc's viewport, and set doc's relevant global object's navigation API's
507    /// > focus changed during ongoing navigation to false.
508    ///
509    /// TODO: Handle the "focus changed during ongoing navigation" flag.
510    pub(crate) fn perform_focus_fixup_rule(&self, cx: &mut JSContext) {
511        if self
512            .focused_area
513            .borrow()
514            .element()
515            .is_none_or(|focused| focused.is_focusable_area())
516        {
517            return;
518        }
519        self.focus(cx, FocusableArea::Viewport);
520    }
521
522    pub(crate) fn sequentially_focus_child_iframe_local_or_remote(
523        &self,
524        cx: &mut JSContext,
525        iframe_element: &HTMLIFrameElement,
526        direction: SequentialFocusDirection,
527    ) {
528        if let Some(content_document) = iframe_element.GetContentDocument() {
529            // The <iframe> is in the same `ScriptThread` and we have direct access to it. We can
530            // move the focus directly.
531            content_document
532                .focus_handler()
533                .sequential_focus_from_another_document(cx, None, direction);
534        } else if let Some(browsing_context_id) = iframe_element.browsing_context_id() {
535            self.window.send_to_constellation(
536                ScriptToConstellationMessage::FocusRemoteBrowsingContext(
537                    browsing_context_id,
538                    RemoteFocusOperation::Sequential(direction, None),
539                ),
540            );
541        } else {
542            iframe_element
543                .upcast::<Node>()
544                .run_the_focusing_steps(cx, None);
545        }
546    }
547
548    pub(crate) fn sequentially_focus_parent_local_or_remote(
549        &self,
550        cx: &mut JSContext,
551        direction: SequentialFocusDirection,
552    ) {
553        let window_proxy = self.window.window_proxy();
554        if let Some(iframe) = window_proxy.frame_element() {
555            // The parent browsing context is in the same `ScriptThread` and we have direct access
556            // to it. We can move the focus directly.
557            let browsing_context_id = iframe
558                .downcast::<HTMLIFrameElement>()
559                .and_then(|iframe_element| iframe_element.browsing_context_id());
560            iframe
561                .owner_document()
562                .focus_handler()
563                .sequential_focus_from_another_document(cx, browsing_context_id, direction);
564        } else if let Some(browsing_context_id) = window_proxy
565            .parent()
566            .map(|parent| parent.browsing_context_id())
567        {
568            self.window.send_to_constellation(
569                ScriptToConstellationMessage::FocusRemoteBrowsingContext(
570                    browsing_context_id,
571                    RemoteFocusOperation::Sequential(
572                        direction,
573                        Some(window_proxy.browsing_context_id()),
574                    ),
575                ),
576            );
577        }
578    }
579
580    pub(crate) fn sequential_focus_from_another_document(
581        &self,
582        cx: &mut JSContext,
583        browsing_context_id: Option<BrowsingContextId>,
584        direction: SequentialFocusDirection,
585    ) {
586        let _realm = enter_realm(&*self.window);
587        let starting_point = browsing_context_id.and_then(|browsing_context_id| {
588            self.window
589                .Document()
590                .iframes()
591                .get(browsing_context_id)
592                .map(|iframe| DomRoot::from_ref(iframe.element.upcast::<Node>()))
593        });
594        self.window
595            .Document()
596            .event_handler()
597            .sequential_focus_navigation_loop(
598                cx,
599                starting_point,
600                direction,
601                true, /* allow focusing viewport */
602            );
603    }
604}