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;
6
7use bitflags::bitflags;
8use embedder_traits::FocusSequenceNumber;
9use script_bindings::codegen::GenericBindings::ShadowRootBinding::ShadowRootMethods;
10use script_bindings::inheritance::Castable;
11use script_bindings::root::{Dom, DomRoot};
12use script_bindings::script_runtime::CanGc;
13use servo_constellation_traits::ScriptToConstellationMessage;
14
15use crate::dom::bindings::root::MutNullableDom;
16use crate::dom::execcommand::contenteditable::ContentEditableRange;
17use crate::dom::focusevent::FocusEventType;
18use crate::dom::types::{Element, EventTarget, FocusEvent, HTMLElement, HTMLIFrameElement, Window};
19use crate::dom::{Event, EventBubbles, EventCancelable, Node, NodeTraits};
20
21pub(crate) enum FocusOperation {
22    Focus(FocusableArea),
23    Unfocus,
24}
25
26/// The kind of focusable area a [`FocusableArea`] is. A [`FocusableArea`] may be click focusable,
27/// sequentially focusable, or both.
28#[derive(Clone, Copy, Debug, Default, MallocSizeOf)]
29pub(crate) struct FocusableAreaKind(u8);
30
31bitflags! {
32    impl FocusableAreaKind: u8 {
33        /// <https://html.spec.whatwg.org/multipage/#click-focusable>
34        ///
35        /// > A focusable area is said to be click focusable if the user agent determines that it is
36        /// > click focusable. User agents should consider focusable areas with non-null tabindex values
37        /// > to be click focusable.
38        const Click = 1 << 0;
39        /// <https://html.spec.whatwg.org/multipage/#sequentially-focusable>.
40        ///
41        /// > A focusable area is said to be sequentially focusable if it is included in its
42        /// > Document's sequential focus navigation order and the user agent determines that it is
43        /// > sequentially focusable.
44        const Sequential = 1 << 1;
45    }
46}
47
48pub(crate) enum FocusableArea {
49    Node {
50        node: DomRoot<Node>,
51        kind: FocusableAreaKind,
52    },
53    Viewport,
54}
55
56impl FocusableArea {
57    pub(crate) fn kind(&self) -> FocusableAreaKind {
58        match self {
59            FocusableArea::Node { kind, .. } => *kind,
60            FocusableArea::Viewport => FocusableAreaKind::Click | FocusableAreaKind::Sequential,
61        }
62    }
63}
64
65/// Specifies the initiator of a focus operation.
66#[derive(Clone, Copy, PartialEq)]
67pub(crate) enum FocusInitiator {
68    /// The operation is initiated by a focus change in this [`Document`]. This
69    /// means the change might trigger focus changes in parent [`Document`]s.
70    Local,
71    /// The operation is initiated somewhere else, and we are updating our
72    /// internal state accordingly.
73    Remote,
74}
75
76/// The [`DocumentFocusHandler`] is a structure responsible for handling and storing data related to
77/// focus for the `Document`. It exists to decrease the size of the `Document`.
78/// structure.
79#[derive(JSTraceable, MallocSizeOf)]
80#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
81pub(crate) struct DocumentFocusHandler {
82    /// The [`Window`] element for this [`DocumentFocusHandler`].
83    window: Dom<Window>,
84    /// The element that currently has focus in the `Document`.
85    focused_element: MutNullableDom<Element>,
86    /// The last sequence number sent to the constellation.
87    #[no_trace]
88    focus_sequence: Cell<FocusSequenceNumber>,
89    /// Indicates whether the container is included in the top-level browsing
90    /// context's focus chain (not considering system focus). Permanently `true`
91    /// for a top-level document.
92    has_focus: Cell<bool>,
93}
94
95impl DocumentFocusHandler {
96    pub(crate) fn new(window: &Window, has_focus: bool) -> Self {
97        Self {
98            window: Dom::from_ref(window),
99            focused_element: Default::default(),
100            focus_sequence: Cell::new(FocusSequenceNumber::default()),
101            has_focus: Cell::new(has_focus),
102        }
103    }
104
105    pub(crate) fn has_focus(&self) -> bool {
106        self.has_focus.get()
107    }
108
109    /// Return the element that currently has focus. If `None` is returned the viewport itself has focus.
110    pub(crate) fn focused_element(&self) -> Option<DomRoot<Element>> {
111        self.focused_element.get()
112    }
113
114    /// Set the element that currently has focus and update the focus state for both the previously
115    /// set element (if any) and the new one, as well as the new one. This will not do anything if
116    /// the new element is the same as the previous one. Note that this *will not* fire any focus
117    /// events. If that is necessary the [`DocumentFocusHandler::focus`] should be used.
118    pub(crate) fn set_focused_element(&self, new_element: Option<&Element>) {
119        let previously_focused_element = self.focused_element.get();
120        if new_element == previously_focused_element.as_deref() {
121            return;
122        }
123
124        // From <https://html.spec.whatwg.org/multipage/#selector-focus>
125        // > For the purposes of the CSS :focus pseudo-class, an element has the focus when:
126        // >  - it is not itself a navigable container; and
127        // >  - any of the following are true:
128        // >    - it is one of the elements listed in the current focus chain of the top-level
129        // >      traversable; or
130        // >    - its shadow root shadowRoot is not null and shadowRoot is the root of at least one
131        // >      element that has the focus.
132        //
133        // We are trying to accomplish the last requirement here, by walking up the tree and
134        // marking each shadow host as focused.
135        fn recursively_set_focus_status(element: &Element, new_state: bool) {
136            element.set_focus_state(new_state);
137
138            let Some(shadow_root) = element.containing_shadow_root() else {
139                return;
140            };
141            recursively_set_focus_status(&shadow_root.Host(), new_state);
142        }
143
144        if let Some(previously_focused_element) = previously_focused_element {
145            recursively_set_focus_status(&previously_focused_element, false);
146        }
147        if let Some(newly_focused_element) = new_element {
148            recursively_set_focus_status(newly_focused_element, true);
149        }
150
151        self.focused_element.set(new_element);
152    }
153
154    /// Get the last sequence number sent to the constellation.
155    ///
156    /// Received focus-related messages with sequence numbers less than the one
157    /// returned by this method must be discarded.
158    pub fn focus_sequence(&self) -> FocusSequenceNumber {
159        self.focus_sequence.get()
160    }
161
162    /// Generate the next sequence number for focus-related messages.
163    fn increment_fetch_focus_sequence(&self) -> FocusSequenceNumber {
164        self.focus_sequence.set(FocusSequenceNumber(
165            self.focus_sequence
166                .get()
167                .0
168                .checked_add(1)
169                .expect("too many focus messages have been sent"),
170        ));
171        self.focus_sequence.get()
172    }
173
174    /// Update the local focus state accordingly after being notified that the
175    /// document's container is removed from the top-level browsing context's
176    /// focus chain (not considering system focus).
177    pub(crate) fn handle_container_unfocus(&self, can_gc: CanGc) {
178        if self.window.parent_info().is_none() {
179            warn!("Top-level document cannot be unfocused");
180            return;
181        }
182        self.focus(FocusOperation::Unfocus, FocusInitiator::Remote, can_gc);
183    }
184
185    /// Reassign the focus context to the element that last requested focus during this
186    /// transaction, or the document if no elements requested it.
187    pub(crate) fn focus(
188        &self,
189        focus_operation: FocusOperation,
190        focus_initiator: FocusInitiator,
191        can_gc: CanGc,
192    ) {
193        let (mut new_focused, new_focus_state) = match focus_operation {
194            FocusOperation::Focus(focusable_area) => (
195                match focusable_area {
196                    FocusableArea::Node { node, .. } => DomRoot::downcast::<Element>(node),
197                    FocusableArea::Viewport => None,
198                },
199                true,
200            ),
201            FocusOperation::Unfocus => (
202                self.focused_element.get().as_deref().map(DomRoot::from_ref),
203                false,
204            ),
205        };
206
207        if !new_focus_state {
208            // In many browsers, a document forgets its focused area when the
209            // document is removed from the top-level BC's focus chain
210            if new_focused.take().is_some() {
211                trace!(
212                    "Forgetting the document's focused area because the \
213                    document's container was removed from the top-level BC's \
214                    focus chain"
215                );
216            }
217        }
218
219        let old_focused = self.focused_element.get();
220        let old_focus_state = self.has_focus.get();
221
222        debug!(
223            "Committing focus transaction: {:?} → {:?}",
224            (&old_focused, old_focus_state),
225            (&new_focused, new_focus_state),
226        );
227
228        // `*_focused_filtered` indicates the local element (if any) included in
229        // the top-level BC's focus chain.
230        let old_focused_filtered = old_focused.as_ref().filter(|_| old_focus_state);
231        let new_focused_filtered = new_focused.as_ref().filter(|_| new_focus_state);
232
233        let trace_focus_chain = |name, element, doc| {
234            trace!(
235                "{} local focus chain: {}",
236                name,
237                match (element, doc) {
238                    (Some(e), _) => format!("[{:?}, document]", e),
239                    (None, true) => "[document]".to_owned(),
240                    (None, false) => "[]".to_owned(),
241                }
242            );
243        };
244
245        trace_focus_chain("Old", old_focused_filtered, old_focus_state);
246        trace_focus_chain("New", new_focused_filtered, new_focus_state);
247
248        if old_focused_filtered != new_focused_filtered {
249            // Although the "focusing steps" in the HTML specification say to wait until after firing
250            // the "blur" event to change the currently focused area of the Document, browsers tend
251            // to set it to the viewport before firing the "blur" event.
252            //
253            // See https://github.com/whatwg/html/issues/1569
254            self.set_focused_element(None);
255
256            if let Some(element) = &old_focused_filtered {
257                if element.upcast::<Node>().is_connected() {
258                    self.fire_focus_event(
259                        FocusEventType::Blur,
260                        element.upcast(),
261                        new_focused_filtered.map(|element| element.upcast()),
262                        can_gc,
263                    );
264                }
265            }
266        }
267
268        if old_focus_state != new_focus_state && !new_focus_state {
269            self.fire_focus_event(FocusEventType::Blur, self.window.upcast(), None, can_gc);
270        }
271
272        self.set_focused_element(new_focused.as_deref());
273        self.has_focus.set(new_focus_state);
274
275        if old_focus_state != new_focus_state && new_focus_state {
276            self.fire_focus_event(FocusEventType::Focus, self.window.upcast(), None, can_gc);
277        }
278
279        if old_focused_filtered != new_focused_filtered {
280            if let Some(element) = &new_focused_filtered {
281                if let Some(html_element) = element.downcast::<HTMLElement>() {
282                    html_element.handle_focus_state_for_contenteditable(can_gc);
283                }
284
285                self.fire_focus_event(
286                    FocusEventType::Focus,
287                    element.upcast(),
288                    old_focused_filtered.map(|element| element.upcast()),
289                    can_gc,
290                );
291            }
292        }
293
294        if focus_initiator == FocusInitiator::Remote {
295            return;
296        }
297
298        // We are the initiator of the focus operation, so we must broadcast
299        // the change we intend to make.
300        match (old_focus_state, new_focus_state) {
301            (_, true) => {
302                // Advertise the change in the focus chain.
303                // <https://html.spec.whatwg.org/multipage/#focus-chain>
304                // <https://html.spec.whatwg.org/multipage/#focusing-steps>
305                //
306                // If the top-level BC doesn't have system focus, this won't
307                // have an immediate effect, but it will when we gain system
308                // focus again. Therefore we still have to send `ScriptMsg::
309                // Focus`.
310                //
311                // When a container with a non-null nested browsing context is
312                // focused, its active document becomes the focused area of the
313                // top-level browsing context instead. Therefore we need to let
314                // the constellation know if such a container is focused.
315                //
316                // > The focusing steps for an object `new focus target` [...]
317                // >
318                // >  3. If `new focus target` is a browsing context container
319                // >     with non-null nested browsing context, then set
320                // >     `new focus target` to the nested browsing context's
321                // >     active document.
322                let child_browsing_context_id = new_focused
323                    .as_ref()
324                    .and_then(|elem| elem.downcast::<HTMLIFrameElement>())
325                    .and_then(|iframe| iframe.browsing_context_id());
326
327                let sequence = self.increment_fetch_focus_sequence();
328
329                debug!(
330                    "Advertising the focus request to the constellation \
331                        with sequence number {} and child BC ID {}",
332                    sequence,
333                    child_browsing_context_id
334                        .as_ref()
335                        .map(|id| id as &dyn std::fmt::Display)
336                        .unwrap_or(&"(none)"),
337                );
338
339                self.window
340                    .send_to_constellation(ScriptToConstellationMessage::Focus(
341                        child_browsing_context_id,
342                        sequence,
343                    ));
344            },
345            (false, false) => {
346                // Our `Document` doesn't have focus, and we intend to keep it
347                // this way.
348            },
349            (true, false) => {
350                unreachable!(
351                    "Can't lose the document's focus without specifying \
352                    another one to focus"
353                );
354            },
355        }
356    }
357
358    /// <https://html.spec.whatwg.org/multipage/#fire-a-focus-event>
359    fn fire_focus_event(
360        &self,
361        focus_event_type: FocusEventType,
362        event_target: &EventTarget,
363        related_target: Option<&EventTarget>,
364        can_gc: CanGc,
365    ) {
366        let (event_name, does_bubble) = match focus_event_type {
367            FocusEventType::Focus => ("focus".into(), EventBubbles::DoesNotBubble),
368            FocusEventType::Blur => ("blur".into(), EventBubbles::DoesNotBubble),
369        };
370        let event = FocusEvent::new(
371            &self.window,
372            event_name,
373            does_bubble,
374            EventCancelable::NotCancelable,
375            Some(&self.window),
376            0i32,
377            related_target,
378            can_gc,
379        );
380        let event = event.upcast::<Event>();
381        event.set_trusted(true);
382        event.fire(event_target, can_gc);
383    }
384
385    /// <https://html.spec.whatwg.org/multipage/#focus-fixup-rule>
386    /// > For each doc of docs, if the focused area of doc is not a focusable area, then run the
387    /// > focusing steps for doc's viewport, and set doc's relevant global object's navigation API's
388    /// > focus changed during ongoing navigation to false.
389    ///
390    /// TODO: Handle the "focus changed during ongoing navigation" flag.
391    pub(crate) fn perform_focus_fixup_rule(&self, can_gc: CanGc) {
392        if self
393            .focused_element
394            .get()
395            .as_deref()
396            .is_none_or(|focused| focused.is_focusable_area())
397        {
398            return;
399        }
400        self.focus(
401            FocusOperation::Focus(FocusableArea::Viewport),
402            FocusInitiator::Local,
403            can_gc,
404        );
405    }
406}