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}