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