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