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