Skip to main content

script/dom/node/
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::collections::VecDeque;
6
7use html5ever::local_name;
8use js::context::JSContext;
9use script_bindings::codegen::GenericBindings::ShadowRootBinding::ShadowRootMethods;
10use script_bindings::inheritance::Castable;
11use script_bindings::root::{Dom, DomRoot};
12
13use crate::dom::document::focus::{FocusableArea, FocusableAreaKind};
14use crate::dom::iterators::ShadowIncluding;
15use crate::dom::node::iterators::TreeIterator;
16use crate::dom::types::{
17    Element, HTMLDialogElement, HTMLIFrameElement, HTMLSlotElement, ShadowRoot,
18};
19use crate::dom::{Document, Node, NodeTraits};
20
21/// <https://html.spec.whatwg.org/multipage/#focus-navigation-scope-owner>
22///
23/// This enum represents the "focus navigation scope owner" in Servo.
24pub(crate) enum FocusNavigationScopeOwner {
25    Document(DomRoot<Document>),
26    ShadowHost {
27        shadow_host: DomRoot<Element>,
28        shadow_root: DomRoot<ShadowRoot>,
29    },
30    Slot(DomRoot<HTMLSlotElement>),
31}
32
33impl FocusNavigationScopeOwner {
34    pub(crate) fn iterator(&self) -> FocusNavigationScopeIterator {
35        let iterators = match self {
36            Self::Document(document) => VecDeque::from([document
37                .upcast::<Node>()
38                .traverse_preorder(ShadowIncluding::No)]),
39            Self::ShadowHost { shadow_root, .. } => VecDeque::from([shadow_root
40                .upcast::<Node>()
41                .traverse_preorder(ShadowIncluding::No)]),
42            Self::Slot(html_slot_element) => html_slot_element
43                .assigned_nodes()
44                .iter()
45                .map(|slottable| slottable.node().traverse_preorder(ShadowIncluding::No))
46                .collect(),
47        };
48
49        FocusNavigationScopeIterator { iterators }
50    }
51
52    /// Returns the `Node` that backs this [`FocusNavigationScopeOwner`]. This is a node that
53    /// can be found in the containing focus navigation scope, so a traversal on it will
54    /// traverse the nodes in the scope.
55    pub(crate) fn node(&self) -> &Node {
56        match self {
57            FocusNavigationScopeOwner::Document(document) => document.upcast(),
58            FocusNavigationScopeOwner::ShadowHost { shadow_host, .. } => shadow_host.upcast(),
59            FocusNavigationScopeOwner::Slot(html_slot_element) => html_slot_element.upcast(),
60        }
61    }
62}
63
64pub(crate) struct FocusNavigationScopeIterator {
65    iterators: VecDeque<TreeIterator>,
66}
67
68impl Iterator for FocusNavigationScopeIterator {
69    type Item = DomRoot<Node>;
70
71    fn next(&mut self) -> Option<Self::Item> {
72        let should_skip_element_children = |element: &Element| {
73            element.is_shadow_host() ||
74                element
75                    .downcast::<HTMLSlotElement>()
76                    .is_some_and(|html_slot_element| html_slot_element.has_assigned_nodes())
77        };
78
79        while !self.iterators.is_empty() {
80            if let Some(next) = self.iterators.front_mut().and_then(|front| {
81                let should_skip_children_on_next_iteration = front
82                    .peek()
83                    .and_then(|node| node.downcast::<Element>())
84                    .is_some_and(should_skip_element_children);
85                if should_skip_children_on_next_iteration {
86                    front.next_skipping_children()
87                } else {
88                    front.next()
89                }
90            }) {
91                return Some(next);
92            }
93
94            self.iterators.pop_front();
95        }
96        None
97    }
98}
99
100#[derive(Clone, Copy, PartialEq)]
101pub(crate) enum FocusTrigger {
102    Click,
103    Other,
104}
105
106impl Node {
107    /// Returns the appropriate [`FocusableArea`] when this [`Node`] is clicked on according to
108    /// <https://www.w3.org/TR/pointerevents4/#handle-native-mouse-down>.
109    ///
110    /// Note that this is doing more than the specification which says to only take into account
111    /// the node from the hit test. This isn't exactly how browsers work though, as they seem
112    /// to look for the first inclusive ancestor node that has a focusable area associated with it.
113    /// Note also that this may return [`FocusableArea::Viewport`].
114    pub(crate) fn find_click_focusable_area(&self, cx: &JSContext) -> FocusableArea {
115        self.inclusive_ancestors(ShadowIncluding::Yes)
116            .find_map(|node| {
117                node.get_the_focusable_area(cx, FocusTrigger::Click)
118                    .filter(|focusable_area| {
119                        focusable_area.kind().contains(FocusableAreaKind::Click)
120                    })
121            })
122            .unwrap_or(FocusableArea::Viewport)
123    }
124
125    /// <https://html.spec.whatwg.org/multipage/#get-the-focusable-area>
126    ///
127    /// There seems to be hole in the specification here. It describes how to get the focusable
128    /// area for a focus target that isn't a focuable area, but is ambiguous about how to do
129    /// this for a focus target that actually *is* a focusable area. The obvious thing is to
130    /// just return the focus target, but it's still odd that this isn't mentioned in the
131    /// specification.
132    pub(crate) fn get_the_focusable_area(
133        &self,
134        cx: &JSContext,
135        focus_trigger: FocusTrigger,
136    ) -> Option<FocusableArea> {
137        let kind = self
138            .downcast::<Element>()
139            .map(|element| Element::focusable_area_kind(element, cx.no_gc()))
140            .unwrap_or_default();
141        if !kind.is_empty() {
142            if let Some(iframe_element) = self.downcast::<HTMLIFrameElement>() {
143                return Some(FocusableArea::IFrameViewport {
144                    iframe_element: Dom::from_ref(iframe_element),
145                    kind,
146                });
147            }
148
149            return Some(FocusableArea::Node {
150                node: Dom::from_ref(self),
151                kind,
152            });
153        }
154
155        self.get_the_focusable_area_if_not_a_focusable_area(cx, focus_trigger)
156    }
157
158    /// <https://html.spec.whatwg.org/multipage/#get-the-focusable-area>
159    ///
160    /// In addition to returning the DOM anchor of the focusable area for this [`Node`], this
161    /// method also returns the [`FocusableAreaKind`] for efficiency reasons. Note that `None`
162    /// is returned if this [`Node`] does not have a focusable area or if its focusable area
163    /// is the `Document`'s viewport.
164    ///
165    /// TODO: It might be better to distinguish these two cases in the future.
166    fn get_the_focusable_area_if_not_a_focusable_area(
167        &self,
168        cx: &JSContext,
169        focus_trigger: FocusTrigger,
170    ) -> Option<FocusableArea> {
171        // > To get the focusable area for a focus target that is either an element that is not a
172        // > focusable area, or is a navigable, given an optional string focus trigger (default
173        // > "other"), run the first matching set of steps from the following list:
174        //
175        // > ↪ If focus target is an area element with one or more shapes that are focusable areas
176        // >     Return the shape corresponding to the first img element in tree order that uses the image
177        // >     map to which the area element belongs.
178        // TODO: Implement this.
179
180        // > ↪ If focus target is an element with one or more scrollable regions that are focusable areas
181        // >     Return the element's first scrollable region, according to a pre-order, depth-first
182        // >     traversal of the flat tree. [CSSSCOPING]
183        // TODO: Implement this.
184
185        // > ↪ If focus target is the document element of its Document
186        // >     Return the Document's viewport.
187        if self == self.owner_document().upcast::<Node>() {
188            return Some(FocusableArea::Viewport);
189        }
190
191        // > ↪ If focus target is a navigable
192        // >     Return the navigable's active document.
193        // TODO: Implement this.
194
195        // > ↪ If focus target is a navigable container with a non-null content navigable
196        // >     Return the navigable container's content navigable's active document.
197        // TODO: Implement this.
198
199        // > ↪ If focus target is a shadow host whose shadow root's delegates focus is true
200        if self
201            .downcast::<Element>()
202            .and_then(Element::shadow_root)
203            .is_some_and(|shadow_root| shadow_root.DelegatesFocus())
204        {
205            // >   Step 1. Let focusedElement be the currently focused area of a top-level
206            // >           traversable's DOM anchor.
207            //
208            // Note: This is a bit of a misnomer, because it might be a Node and not an Element.
209            let document = self.owner_document();
210            let focused_area = document.focus_handler().focused_area();
211            let focused_element = focused_area.dom_anchor(&document);
212
213            // >   Step 2. If focus target is a shadow-including inclusive ancestor of
214            // >           focusedElement, then return focusedElement.
215            if self
216                .upcast::<Node>()
217                .is_shadow_including_inclusive_ancestor_of(&focused_element)
218            {
219                return Some(focused_area.clone());
220            }
221
222            // >   Step 3. Return the focus delegate for focus target given focus trigger.
223            return self.focus_delegate(cx, Some(focus_trigger));
224        }
225
226        None
227    }
228
229    /// <https://html.spec.whatwg.org/multipage/#focus-delegate>
230    ///
231    /// In addition to returning the focus delegate for this [`Node`], this method also returns
232    /// the [`FocusableAreaKind`] for efficiency reasons.
233    pub(crate) fn focus_delegate(
234        &self,
235        cx: &JSContext,
236        focus_trigger: Option<FocusTrigger>,
237    ) -> Option<FocusableArea> {
238        let focus_trigger = focus_trigger.unwrap_or(FocusTrigger::Other);
239        // > 1. If focusTarget is a shadow host and its shadow root's delegates focus is false, then
240        // >    return null.
241        let shadow_root = self.downcast::<Element>().and_then(Element::shadow_root);
242        if shadow_root
243            .as_ref()
244            .is_some_and(|shadow_root| !shadow_root.DelegatesFocus())
245        {
246            return None;
247        }
248
249        // > 2. Let whereToLook be focusTarget.
250        let mut where_to_look = self.upcast::<Node>();
251
252        // > 3. If whereToLook is a shadow host, then set whereToLook to whereToLook's shadow root.
253        if let Some(shadow_root) = shadow_root.as_ref() {
254            where_to_look = shadow_root.upcast();
255        }
256
257        // > 4. Let autofocusDelegate be the autofocus delegate for whereToLook given focusTrigger.
258        rooted!(&in(cx) let autofocus_delegate = where_to_look.autofocus_delegate(cx, focus_trigger));
259
260        // > 5. If autofocusDelegate is not null, then return autofocusDelegate.
261        if autofocus_delegate.is_some() {
262            return autofocus_delegate.clone();
263        }
264
265        // > 6. For each descendant of whereToLook's descendants, in tree order:
266        let is_dialog_element = self.is::<HTMLDialogElement>();
267        for descendant in where_to_look.traverse_preorder(ShadowIncluding::No).skip(1) {
268            // > 6.1. Let focusableArea be null.
269            // Handled via early return.
270
271            // > 6.2. If focusTarget is a dialog element and descendant is sequentially focusable, then
272            // >      set focusableArea to descendant.
273            let kind = descendant
274                .downcast::<Element>()
275                .map(|node| Element::focusable_area_kind(node, cx.no_gc()))
276                .unwrap_or_default();
277            if is_dialog_element && kind.contains(FocusableAreaKind::Sequential) {
278                return Some(FocusableArea::Node {
279                    node: descendant.as_traced(),
280                    kind,
281                });
282            }
283
284            // > 6.3. Otherwise, if focusTarget is not a dialog and descendant is a focusable area, set
285            // >      focusableArea to descendant.
286            if !kind.is_empty() {
287                return Some(FocusableArea::Node {
288                    node: descendant.as_traced(),
289                    kind,
290                });
291            }
292
293            // > 6.4. Otherwise, set focusableArea to the result of getting the focusable area for
294            //        descendant given focusTrigger.
295            rooted!(&in(cx) let focusable_area = descendant.get_the_focusable_area_if_not_a_focusable_area(
296                cx,
297                focus_trigger)
298            );
299            if let Some(focusable_area) = &*focusable_area {
300                // > 6.5. If focusableArea is not null, then return focusableArea.
301                return Some(focusable_area.clone());
302            }
303        }
304
305        // > 7. Return null.
306        None
307    }
308
309    /// <https://html.spec.whatwg.org/multipage/#autofocus-delegate>
310    fn autofocus_delegate(
311        &self,
312        cx: &JSContext,
313        focus_trigger: FocusTrigger,
314    ) -> Option<FocusableArea> {
315        // > 1. For each descendant descendant of focus target, in tree order:
316        for descendant in self.traverse_preorder(ShadowIncluding::No).skip(1) {
317            // > 1.1. If descendant does not have an autofocus content attribute, then continue.
318            if !descendant
319                .downcast::<Element>()
320                .is_some_and(|element| element.has_attribute(&local_name!("autofocus")))
321            {
322                continue;
323            }
324
325            // > 1.2. Let focusable area be descendant, if descendant is a focusable area; otherwise
326            // >      let focusable area be the result of getting the focusable area for descendant
327            // >      given focus trigger.
328            rooted!(&in(cx) let focusable_area = descendant.get_the_focusable_area(cx, focus_trigger));
329            let Some(focusable_area) = &*focusable_area else {
330                // > 1.3. If focusable area is null, then continue.
331                continue;
332            };
333
334            // > 1.4. If focusable area is not click focusable and focus trigger is "click", then continue.
335            if !focusable_area.kind().contains(FocusableAreaKind::Click) &&
336                focus_trigger == FocusTrigger::Click
337            {
338                continue;
339            }
340
341            // > 1.5. Return focusable area.
342            return Some(focusable_area.clone());
343        }
344
345        // > 2. Return null.
346        None
347    }
348
349    /// <https://html.spec.whatwg.org/multipage/#focusing-steps>
350    ///
351    /// This is an initial implementation of the "focusing steps" from the HTML specification. Note
352    /// that this is currently in a state of transition from Servo's old internal focus APIs to ones
353    /// that match the specification. That is why the arguments to this method do not match the
354    /// specification yet.
355    ///
356    /// Return `true` if anything was focused or `false` otherwise.
357    #[cfg_attr(crown, expect(crown::unrooted_must_root))]
358    pub(crate) fn run_the_focusing_steps(
359        &self,
360        cx: &mut JSContext,
361        fallback_target: Option<FocusableArea>,
362        focus_trigger: FocusTrigger,
363    ) -> bool {
364        // > 1. If new focus target is not a focusable area, then set new focus target to the result
365        // >    of getting the focusable area for new focus target, given focus trigger if it was
366        // >    passed.
367        // > 2. If new focus target is null, then:
368        // > 2.1 If no fallback target was specified, then return.
369        // > 2.2 Otherwise, set new focus target to the fallback target.
370        rooted!(&in(cx) let mut focusable_area = self
371            .get_the_focusable_area(cx, focus_trigger)
372            .or(fallback_target)
373        );
374        if focusable_area.is_none() {
375            return false;
376        }
377
378        // > 3. If new focus target is a navigable container with non-null content navigable, then
379        // >    set new focus target to the content navigable's active document.
380        // > 4. If new focus target is a focusable area and its DOM anchor is inert, then return.
381        // > 5. If new focus target is the currently focused area of a top-level traversable, then
382        // >    return.
383        // > 6. Let old chain be the current focus chain of the top-level traversable in which new
384        // >    focus target finds itself.
385        // > 6.1. Let new chain be the focus chain of new focus target.
386        // > 6.2. Run the focus update steps with old chain, new chain, and new focus target
387        // >      respectively.
388        //
389        // TODO: Handle all of these steps by converting the focus transaction code to follow
390        // the HTML focus specification.
391        let document = self.owner_document();
392        document
393            .focus_handler()
394            .focus(cx, &focusable_area.take().unwrap());
395        true
396    }
397
398    /// If this node is a focus navigation scope owner, return the corresponding
399    /// [`FocusNavigationScopeOwner`] that describes it or `None` if this node is not
400    /// a focus navigation scope owner.
401    ///
402    /// <https://html.spec.whatwg.org/multipage/#focus-navigation-scope-owner>
403    pub(crate) fn as_focus_navigation_scope_owner(&self) -> Option<FocusNavigationScopeOwner> {
404        if let Some(element) = self.downcast::<Element>() {
405            if let Some(shadow_root) = element.shadow_root() {
406                return Some(FocusNavigationScopeOwner::ShadowHost {
407                    shadow_host: DomRoot::from_ref(element),
408                    shadow_root,
409                });
410            }
411
412            if let Some(html_slot_element) = self.downcast::<HTMLSlotElement>() {
413                // Only consider this `<slot>` element a focus navigation scope owner if
414                // it has assigned slottables and isn't displaying fallback content.
415                if html_slot_element.has_assigned_nodes() {
416                    return Some(FocusNavigationScopeOwner::Slot(DomRoot::from_ref(
417                        html_slot_element,
418                    )));
419                }
420            }
421        }
422
423        Some(FocusNavigationScopeOwner::Document(DomRoot::from_ref(
424            self.downcast::<Document>()?,
425        )))
426    }
427
428    /// Find the focus navigation scope owner for this node. If this node is itself
429    /// a focus navigation scope owner, this will return its containing focus navigation
430    /// scope owner.
431    ///
432    /// This will return `None` if this node is the `Document` element.
433    ///
434    /// <https://html.spec.whatwg.org/multipage/#focus-navigation-scope-owner>
435    pub(crate) fn containing_focus_navigation_scope_owner(
436        &self,
437    ) -> Option<FocusNavigationScopeOwner> {
438        for ancestor in self.inclusive_ancestors(ShadowIncluding::No) {
439            // When a slot has an attached shadow DOM it takes precedence so this comes before
440            // the check for slot elements with assigned slots.
441            if let Some(shadow_root) = ancestor.downcast::<ShadowRoot>() {
442                return Some(FocusNavigationScopeOwner::ShadowHost {
443                    shadow_host: shadow_root.Host(),
444                    shadow_root: DomRoot::from_ref(shadow_root),
445                });
446            }
447
448            if let Some(html_slot_element) = ancestor.assigned_slot() {
449                return Some(FocusNavigationScopeOwner::Slot(html_slot_element));
450            }
451        }
452
453        if self.is::<Document>() {
454            return None;
455        }
456        Some(FocusNavigationScopeOwner::Document(self.owner_document()))
457    }
458}