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::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) -> FocusableArea {
108 self.inclusive_ancestors(ShadowIncluding::Yes)
109 .find_map(|node| {
110 node.get_the_focusable_area().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) -> Option<FocusableArea> {
125 let kind = self
126 .downcast::<Element>()
127 .map(Element::focusable_area_kind)
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()
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(&self) -> Option<FocusableArea> {
154 // > To get the focusable area for a focus target that is either an element that is not a
155 // > focusable area, or is a navigable, given an optional string focus trigger (default
156 // > "other"), run the first matching set of steps from the following list:
157 //
158 // > ↪ If focus target is an area element with one or more shapes that are focusable areas
159 // > Return the shape corresponding to the first img element in tree order that uses the image
160 // > map to which the area element belongs.
161 // TODO: Implement this.
162
163 // > ↪ If focus target is an element with one or more scrollable regions that are focusable areas
164 // > Return the element's first scrollable region, according to a pre-order, depth-first
165 // > traversal of the flat tree. [CSSSCOPING]
166 // TODO: Implement this.
167
168 // > ↪ If focus target is the document element of its Document
169 // > Return the Document's viewport.
170 if self == self.owner_document().upcast::<Node>() {
171 return Some(FocusableArea::Viewport);
172 }
173
174 // > ↪ If focus target is a navigable
175 // > Return the navigable's active document.
176 // TODO: Implement this.
177
178 // > ↪ If focus target is a navigable container with a non-null content navigable
179 // > Return the navigable container's content navigable's active document.
180 // TODO: Implement this.
181
182 // > ↪ If focus target is a shadow host whose shadow root's delegates focus is true
183 if self
184 .downcast::<Element>()
185 .and_then(Element::shadow_root)
186 .is_some_and(|shadow_root| shadow_root.DelegatesFocus())
187 {
188 // > Step 1. Let focusedElement be the currently focused area of a top-level
189 // > traversable's DOM anchor.
190 //
191 // Note: This is a bit of a misnomer, because it might be a Node and not an Element.
192 let document = self.owner_document();
193 let focused_area = document.focus_handler().focused_area();
194 let focused_element = focused_area.dom_anchor(&document);
195
196 // > Step 2. If focus target is a shadow-including inclusive ancestor of
197 // > focusedElement, then return focusedElement.
198 if self
199 .upcast::<Node>()
200 .is_shadow_including_inclusive_ancestor_of(&focused_element)
201 {
202 return Some(focused_area.clone());
203 }
204
205 // > Step 3. Return the focus delegate for focus target given focus trigger.
206 return self.focus_delegate();
207 }
208
209 None
210 }
211
212 /// <https://html.spec.whatwg.org/multipage/#focus-delegate>
213 ///
214 /// In addition to returning the focus delegate for this [`Node`], this method also returns
215 /// the [`FocusableAreaKind`] for efficiency reasons.
216 fn focus_delegate(&self) -> Option<FocusableArea> {
217 // > 1. If focusTarget is a shadow host and its shadow root's delegates focus is false, then
218 // > return null.
219 let shadow_root = self.downcast::<Element>().and_then(Element::shadow_root);
220 if shadow_root
221 .as_ref()
222 .is_some_and(|shadow_root| !shadow_root.DelegatesFocus())
223 {
224 return None;
225 }
226
227 // > 2. Let whereToLook be focusTarget.
228 let mut where_to_look = self.upcast::<Node>();
229
230 // > 3. If whereToLook is a shadow host, then set whereToLook to whereToLook's shadow root.
231 if let Some(shadow_root) = shadow_root.as_ref() {
232 where_to_look = shadow_root.upcast();
233 }
234
235 // > 4. Let autofocusDelegate be the autofocus delegate for whereToLook given focusTrigger.
236 // TODO: Implement this.
237
238 // > 5. If autofocusDelegate is not null, then return autofocusDelegate.
239 // TODO: Implement this.
240
241 // > 6. For each descendant of whereToLook's descendants, in tree order:
242 let is_dialog_element = self.is::<HTMLDialogElement>();
243 for descendant in where_to_look.traverse_preorder(ShadowIncluding::No).skip(1) {
244 // > 6.1. Let focusableArea be null.
245 // Handled via early return.
246
247 // > 6.2. If focusTarget is a dialog element and descendant is sequentially focusable, then
248 // > set focusableArea to descendant.
249 let kind = descendant
250 .downcast::<Element>()
251 .map(Element::focusable_area_kind)
252 .unwrap_or_default();
253 if is_dialog_element && kind.contains(FocusableAreaKind::Sequential) {
254 return Some(FocusableArea::Node {
255 node: descendant,
256 kind,
257 });
258 }
259
260 // > 6.3. Otherwise, if focusTarget is not a dialog and descendant is a focusable area, set
261 // > focusableArea to descendant.
262 if !kind.is_empty() {
263 return Some(FocusableArea::Node {
264 node: descendant,
265 kind,
266 });
267 }
268
269 // > 6.4. Otherwise, set focusableArea to the result of getting the focusable area for
270 // descendant given focusTrigger.
271 if let Some(focusable_area) =
272 descendant.get_the_focusable_area_if_not_a_focusable_area()
273 {
274 // > 6.5. If focusableArea is not null, then return focusableArea.
275 return Some(focusable_area);
276 }
277 }
278
279 // > 7. Return null.
280 None
281 }
282
283 /// <https://html.spec.whatwg.org/multipage/#focusing-steps>
284 ///
285 /// This is an initial implementation of the "focusing steps" from the HTML specification. Note
286 /// that this is currently in a state of transition from Servo's old internal focus APIs to ones
287 /// that match the specification. That is why the arguments to this method do not match the
288 /// specification yet.
289 ///
290 /// Return `true` if anything was focused or `false` otherwise.
291 pub(crate) fn run_the_focusing_steps(
292 &self,
293 cx: &mut JSContext,
294 fallback_target: Option<FocusableArea>,
295 ) -> bool {
296 // > 1. If new focus target is not a focusable area, then set new focus target to the result
297 // > of getting the focusable area for new focus target, given focus trigger if it was
298 // > passed.
299 // > 2. If new focus target is null, then:
300 // > 2.1 If no fallback target was specified, then return.
301 // > 2.2 Otherwise, set new focus target to the fallback target.
302 let Some(focusable_area) = self.get_the_focusable_area().or(fallback_target) else {
303 return false;
304 };
305
306 // > 3. If new focus target is a navigable container with non-null content navigable, then
307 // > set new focus target to the content navigable's active document.
308 // > 4. If new focus target is a focusable area and its DOM anchor is inert, then return.
309 // > 5. If new focus target is the currently focused area of a top-level traversable, then
310 // > return.
311 // > 6. Let old chain be the current focus chain of the top-level traversable in which new
312 // > focus target finds itself.
313 // > 6.1. Let new chain be the focus chain of new focus target.
314 // > 6.2. Run the focus update steps with old chain, new chain, and new focus target
315 // > respectively.
316 //
317 // TODO: Handle all of these steps by converting the focus transaction code to follow
318 // the HTML focus specification.
319 let document = self.owner_document();
320 document.focus_handler().focus(cx, focusable_area);
321 true
322 }
323
324 /// If this node is a focus navigation scope owner, return the corresponding
325 /// [`FocusNavigationScopeOwner`] that describes it or `None` if this node is not
326 /// a focus navigation scope owner.
327 ///
328 /// <https://html.spec.whatwg.org/multipage/#focus-navigation-scope-owner>
329 pub(crate) fn as_focus_navigation_scope_owner(&self) -> Option<FocusNavigationScopeOwner> {
330 if let Some(element) = self.downcast::<Element>() {
331 if let Some(shadow_root) = element.shadow_root() {
332 return Some(FocusNavigationScopeOwner::ShadowHost {
333 shadow_host: DomRoot::from_ref(element),
334 shadow_root,
335 });
336 }
337
338 if let Some(html_slot_element) = self.downcast::<HTMLSlotElement>() {
339 // Only consider this `<slot>` element a focus navigation scope owner if
340 // it has assigned slottables and isn't displaying fallback content.
341 if html_slot_element.has_assigned_nodes() {
342 return Some(FocusNavigationScopeOwner::Slot(DomRoot::from_ref(
343 html_slot_element,
344 )));
345 }
346 }
347 }
348
349 Some(FocusNavigationScopeOwner::Document(DomRoot::from_ref(
350 self.downcast::<Document>()?,
351 )))
352 }
353
354 /// Find the focus navigation scope owner for this node. If this node is itself
355 /// a focus navigation scope owner, this will return its containing focus navigation
356 /// scope owner.
357 ///
358 /// This will return `None` if this node is the `Document` element.
359 ///
360 /// <https://html.spec.whatwg.org/multipage/#focus-navigation-scope-owner>
361 pub(crate) fn containing_focus_navigation_scope_owner(
362 &self,
363 ) -> Option<FocusNavigationScopeOwner> {
364 for ancestor in self.inclusive_ancestors(ShadowIncluding::No) {
365 // When a slot has an attached shadow DOM it takes precedence so this comes before
366 // the check for slot elements with assigned slots.
367 if let Some(shadow_root) = ancestor.downcast::<ShadowRoot>() {
368 return Some(FocusNavigationScopeOwner::ShadowHost {
369 shadow_host: shadow_root.Host(),
370 shadow_root: DomRoot::from_ref(shadow_root),
371 });
372 }
373
374 if let Some(html_slot_element) = ancestor.assigned_slot() {
375 return Some(FocusNavigationScopeOwner::Slot(html_slot_element));
376 }
377 }
378
379 if self.is::<Document>() {
380 return None;
381 }
382 Some(FocusNavigationScopeOwner::Document(self.owner_document()))
383 }
384}