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