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