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 script_bindings::codegen::GenericBindings::ShadowRootBinding::ShadowRootMethods;
6use script_bindings::inheritance::Castable;
7use script_bindings::root::DomRoot;
8use script_bindings::script_runtime::CanGc;
9
10use crate::dom::document::focus::{
11    FocusInitiator, FocusOperation, FocusableArea, FocusableAreaKind,
12};
13use crate::dom::types::{Element, HTMLDialogElement};
14use crate::dom::{Node, NodeTraits, ShadowIncluding};
15
16impl Node {
17    /// Returns the appropriate [`FocusableArea`] when this [`Node`] is clicked on according to
18    /// <https://www.w3.org/TR/pointerevents4/#handle-native-mouse-down>.
19    ///
20    /// Note that this is doing more than the specification which says to only take into account
21    /// the node from the hit test. This isn't exactly how browsers work though, as they seem
22    /// to look for the first inclusive ancestor node that has a focusable area associated with it.
23    /// Note also that this may return [`FocusableArea::Viewport`].
24    pub(crate) fn find_click_focusable_area(&self) -> FocusableArea {
25        self.inclusive_ancestors(ShadowIncluding::Yes)
26            .find_map(|node| {
27                node.get_the_focusable_area().filter(|focusable_area| {
28                    focusable_area.kind().contains(FocusableAreaKind::Click)
29                })
30            })
31            .unwrap_or(FocusableArea::Viewport)
32    }
33
34    /// <https://html.spec.whatwg.org/multipage/#get-the-focusable-area>
35    ///
36    /// There seems to be hole in the specification here. It describes how to get the focusable
37    /// area for a focus target that isn't a focuable area, but is ambiguous about how to do
38    /// this for a focus target that actually *is* a focusable area. The obvious thing is to
39    /// just return the focus target, but it's still odd that this isn't mentioned in the
40    /// specification.
41    pub(crate) fn get_the_focusable_area(&self) -> Option<FocusableArea> {
42        let kind = self
43            .downcast::<Element>()
44            .map(Element::focusable_area_kind)
45            .unwrap_or_default();
46        if !kind.is_empty() {
47            return Some(FocusableArea::Node {
48                node: DomRoot::from_ref(self),
49                kind,
50            });
51        }
52        self.get_the_focusable_area_if_not_a_focusable_area()
53    }
54
55    /// <https://html.spec.whatwg.org/multipage/#get-the-focusable-area>
56    ///
57    /// In addition to returning the DOM anchor of the focusable area for this [`Node`], this
58    /// method also returns the [`FocusableAreaKind`] for efficiency reasons. Note that `None`
59    /// is returned if this [`Node`] does not have a focusable area or if its focusable area
60    /// is the `Document`'s viewport.
61    ///
62    /// TODO: It might be better to distinguish these two cases in the future.
63    fn get_the_focusable_area_if_not_a_focusable_area(&self) -> Option<FocusableArea> {
64        // > To get the focusable area for a focus target that is either an element that is not a
65        // > focusable area, or is a navigable, given an optional string focus trigger (default
66        // > "other"), run the first matching set of steps from the following list:
67        //
68        // > ↪ If focus target is an area element with one or more shapes that are focusable areas
69        // >     Return the shape corresponding to the first img element in tree order that uses the image
70        // >     map to which the area element belongs.
71        // TODO: Implement this.
72
73        // > ↪ If focus target is an element with one or more scrollable regions that are focusable areas
74        // >     Return the element's first scrollable region, according to a pre-order, depth-first
75        // >     traversal of the flat tree. [CSSSCOPING]
76        // TODO: Implement this.
77
78        // > ↪ If focus target is the document element of its Document
79        // >     Return the Document's viewport.
80        if self == self.owner_document().upcast::<Node>() {
81            return Some(FocusableArea::Viewport);
82        }
83
84        // > ↪ If focus target is a navigable
85        // >     Return the navigable's active document.
86        // TODO: Implement this.
87
88        // > ↪ If focus target is a navigable container with a non-null content navigable
89        // >     Return the navigable container's content navigable's active document.
90        // TODO: Implement this.
91
92        // > ↪ If focus target is a shadow host whose shadow root's delegates focus is true
93        // >   Step 1. Let focusedElement be the currently focused area of a top-level
94        // >           traversable's DOM anchor.
95        if self
96            .downcast::<Element>()
97            .and_then(Element::shadow_root)
98            .is_some_and(|shadow_root| shadow_root.DelegatesFocus())
99        {
100            if let Some(focused_element) = self.owner_document().focus_handler().focused_element() {
101                // >   Step 2. If focus target is a shadow-including inclusive ancestor of
102                // >           focusedElement, then return focusedElement.
103                if self
104                    .upcast::<Node>()
105                    .is_shadow_including_inclusive_ancestor_of(focused_element.upcast())
106                {
107                    let kind = focused_element.focusable_area_kind();
108                    return Some(FocusableArea::Node {
109                        node: DomRoot::upcast(focused_element),
110                        kind,
111                    });
112                }
113            }
114            // >   Step 3. Return the focus delegate for focus target given focus trigger.
115            return self.focus_delegate();
116        }
117
118        None
119    }
120
121    /// <https://html.spec.whatwg.org/multipage/#focus-delegate>
122    ///
123    /// In addition to returning the focus delegate for this [`Node`], this method also returns
124    /// the [`FocusableAreaKind`] for efficiency reasons.
125    fn focus_delegate(&self) -> Option<FocusableArea> {
126        // > 1. If focusTarget is a shadow host and its shadow root's delegates focus is false, then
127        // >    return null.
128        let shadow_root = self.downcast::<Element>().and_then(Element::shadow_root);
129        if shadow_root
130            .as_ref()
131            .is_some_and(|shadow_root| !shadow_root.DelegatesFocus())
132        {
133            return None;
134        }
135
136        // > 2. Let whereToLook be focusTarget.
137        let mut where_to_look = self.upcast::<Node>();
138
139        // > 3. If whereToLook is a shadow host, then set whereToLook to whereToLook's shadow root.
140        if let Some(shadow_root) = shadow_root.as_ref() {
141            where_to_look = shadow_root.upcast();
142        }
143
144        // > 4. Let autofocusDelegate be the autofocus delegate for whereToLook given focusTrigger.
145        // TODO: Implement this.
146
147        // > 5. If autofocusDelegate is not null, then return autofocusDelegate.
148        // TODO: Implement this.
149
150        // > 6. For each descendant of whereToLook's descendants, in tree order:
151        let is_dialog_element = self.is::<HTMLDialogElement>();
152        for descendant in where_to_look.traverse_preorder(ShadowIncluding::No).skip(1) {
153            // > 6.1. Let focusableArea be null.
154            // Handled via early return.
155
156            // > 6.2. If focusTarget is a dialog element and descendant is sequentially focusable, then
157            // >      set focusableArea to descendant.
158            let kind = descendant
159                .downcast::<Element>()
160                .map(Element::focusable_area_kind)
161                .unwrap_or_default();
162            if is_dialog_element && kind.contains(FocusableAreaKind::Sequential) {
163                return Some(FocusableArea::Node {
164                    node: descendant,
165                    kind,
166                });
167            }
168
169            // > 6.3. Otherwise, if focusTarget is not a dialog and descendant is a focusable area, set
170            // >      focusableArea to descendant.
171            if !kind.is_empty() {
172                return Some(FocusableArea::Node {
173                    node: descendant,
174                    kind,
175                });
176            }
177
178            // > 6.4. Otherwise, set focusableArea to the result of getting the focusable area for
179            //        descendant given focusTrigger.
180            if let Some(focusable_area) =
181                descendant.get_the_focusable_area_if_not_a_focusable_area()
182            {
183                // > 6.5. If focusableArea is not null, then return focusableArea.
184                return Some(focusable_area);
185            }
186        }
187
188        // > 7. Return null.
189        None
190    }
191
192    /// <https://html.spec.whatwg.org/multipage/#focusing-steps>
193    ///
194    /// This is an initial implementation of the "focusing steps" from the HTML specification. Note
195    /// that this is currently in a state of transition from Servo's old internal focus APIs to ones
196    /// that match the specification. That is why the arguments to this method do not match the
197    /// specification yet.
198    ///
199    /// Return `true` if anything was focused or `false` otherwise.
200    pub(crate) fn run_the_focusing_steps(
201        &self,
202        fallback_target: Option<FocusableArea>,
203        can_gc: CanGc,
204    ) -> bool {
205        // > 1. If new focus target is not a focusable area, then set new focus target to the result
206        // >    of getting the focusable area for new focus target, given focus trigger if it was
207        // >    passed.
208        // > 2. If new focus target is null, then:
209        // > 2.1 If no fallback target was specified, then return.
210        // > 2.2 Otherwise, set new focus target to the fallback target.
211        let Some(focusable_area) = self.get_the_focusable_area().or(fallback_target) else {
212            return false;
213        };
214
215        // > 3. If new focus target is a navigable container with non-null content navigable, then
216        // >    set new focus target to the content navigable's active document.
217        // > 4. If new focus target is a focusable area and its DOM anchor is inert, then return.
218        // > 5. If new focus target is the currently focused area of a top-level traversable, then
219        // >    return.
220        // > 6. Let old chain be the current focus chain of the top-level traversable in which new
221        // >    focus target finds itself.
222        // > 6.1. Let new chain be the focus chain of new focus target.
223        // > 6.2. Run the focus update steps with old chain, new chain, and new focus target
224        // >      respectively.
225        //
226        // TODO: Handle all of these steps by converting the focus transaction code to follow
227        // the HTML focus specification.
228        let document = self.owner_document();
229        document.focus_handler().focus(
230            FocusOperation::Focus(focusable_area),
231            FocusInitiator::Local,
232            can_gc,
233        );
234        true
235    }
236}