script/dom/document/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::cell::{Cell, Ref};
6use std::cmp::Ordering;
7
8use bitflags::bitflags;
9use embedder_traits::FocusSequenceNumber;
10use js::context::JSContext;
11use keyboard_types::Modifiers;
12use script_bindings::cell::DomRefCell;
13use script_bindings::codegen::GenericBindings::HTMLIFrameElementBinding::HTMLIFrameElementMethods;
14use script_bindings::codegen::GenericBindings::ShadowRootBinding::ShadowRootMethods;
15use script_bindings::codegen::GenericBindings::WindowBinding::WindowMethods;
16use script_bindings::inheritance::Castable;
17use script_bindings::root::{Dom, DomRoot};
18use servo_base::id::BrowsingContextId;
19use servo_constellation_traits::{
20 RemoteFocusOperation, ScriptToConstellationMessage, SequentialFocusDirection,
21};
22
23use crate::dom::bindings::root::MutNullableDom;
24use crate::dom::focusevent::FocusEventType;
25use crate::dom::node::focus::FocusNavigationScopeOwner;
26use crate::dom::types::{
27 Element, EventTarget, FocusEvent, HTMLElement, HTMLIFrameElement, KeyboardEvent, Window,
28};
29use crate::dom::{Document, Event, EventBubbles, EventCancelable, Node, NodeTraits};
30use crate::realms::enter_auto_realm;
31
32/// The kind of focusable area a [`FocusableArea`] is. A [`FocusableArea`] may be click focusable,
33/// sequentially focusable, or both.
34#[derive(Clone, Copy, Debug, Default, JSTraceable, MallocSizeOf, PartialEq)]
35pub(crate) struct FocusableAreaKind(u8);
36
37bitflags! {
38 impl FocusableAreaKind: u8 {
39 /// <https://html.spec.whatwg.org/multipage/#click-focusable>
40 ///
41 /// > A focusable area is said to be click focusable if the user agent determines that it is
42 /// > click focusable. User agents should consider focusable areas with non-null tabindex values
43 /// > to be click focusable.
44 const Click = 1 << 0;
45 /// <https://html.spec.whatwg.org/multipage/#sequentially-focusable>.
46 ///
47 /// > A focusable area is said to be sequentially focusable if it is included in its
48 /// > Document's sequential focus navigation order and the user agent determines that it is
49 /// > sequentially focusable.
50 const Sequential = 1 << 1;
51 }
52}
53
54/// <https://html.spec.whatwg.org/multipage/#focusable-area>
55#[derive(Clone, Default, JSTraceable, MallocSizeOf, PartialEq)]
56pub(crate) enum FocusableArea {
57 Node {
58 node: DomRoot<Node>,
59 kind: FocusableAreaKind,
60 },
61 /// The viewport of an `<iframe>` element in its containing `Document`. `<iframe>`s
62 /// are focusable areas, but have special behavior when focusing.
63 IFrameViewport {
64 iframe_element: DomRoot<HTMLIFrameElement>,
65 kind: FocusableAreaKind,
66 },
67 #[default]
68 Viewport,
69}
70
71impl std::fmt::Debug for FocusableArea {
72 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73 match self {
74 Self::Node { node, kind } => f
75 .debug_struct("Node")
76 .field("node", node)
77 .field("kind", kind)
78 .finish(),
79 Self::IFrameViewport {
80 iframe_element,
81 kind,
82 } => f
83 .debug_struct("IFrameViewport")
84 .field("pipeline", &iframe_element.pipeline_id())
85 .field("kind", kind)
86 .finish(),
87 Self::Viewport => write!(f, "Viewport"),
88 }
89 }
90}
91
92impl FocusableArea {
93 pub(crate) fn kind(&self) -> FocusableAreaKind {
94 match self {
95 Self::Node { kind, .. } | Self::IFrameViewport { kind, .. } => *kind,
96 Self::Viewport => FocusableAreaKind::Click | FocusableAreaKind::Sequential,
97 }
98 }
99
100 /// If this focusable area is a node, return it as an [`Element`] if it is possible, otherwise
101 /// return `None`. This is the [`Element`] to use for applying `:focus` state and for firing
102 /// `blur` and `focus` events if any.
103 ///
104 /// Note: This is currently in a transitional state while the code moves more toward the
105 /// specification.
106 pub(crate) fn element(&self) -> Option<&Element> {
107 match self {
108 Self::Node { node, .. } => node.downcast(),
109 Self::IFrameViewport { iframe_element, .. } => Some(iframe_element.upcast()),
110 Self::Viewport => None,
111 }
112 }
113
114 /// <https://html.spec.whatwg.org/multipage/#dom-anchor>
115 pub(crate) fn dom_anchor(&self, document: &Document) -> DomRoot<Node> {
116 match self {
117 Self::Node { node, .. } => node.clone(),
118 Self::IFrameViewport { iframe_element, .. } => {
119 DomRoot::from_ref(iframe_element.upcast())
120 },
121 Self::Viewport => DomRoot::from_ref(document.upcast()),
122 }
123 }
124
125 pub(crate) fn focus_chain(&self) -> Vec<FocusableArea> {
126 match self {
127 FocusableArea::Node { .. } | FocusableArea::IFrameViewport { .. } => {
128 vec![self.clone(), FocusableArea::Viewport]
129 },
130 FocusableArea::Viewport => vec![self.clone()],
131 }
132 }
133}
134
135/// The [`DocumentFocusHandler`] is a structure responsible for handling and storing data related to
136/// focus for the `Document`. It exists to decrease the size of the `Document`.
137/// structure.
138#[derive(JSTraceable, MallocSizeOf)]
139#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
140pub(crate) struct DocumentFocusHandler {
141 /// The [`Window`] element for this [`DocumentFocusHandler`].
142 window: Dom<Window>,
143 /// The focused area of the [`Document`].
144 ///
145 /// <https://html.spec.whatwg.org/multipage/#focused-area-of-the-document>
146 focused_area: DomRefCell<FocusableArea>,
147 /// The last sequence number sent to the constellation.
148 #[no_trace]
149 focus_sequence: Cell<FocusSequenceNumber>,
150 /// Indicates whether the container is included in the top-level browsing
151 /// context's focus chain (not considering system focus). Permanently `true`
152 /// for a top-level document.
153 has_focus: Cell<bool>,
154 /// <https://html.spec.whatwg.org/multipage/#sequential-focus-navigation-starting-point>
155 sequential_focus_navigation_starting_point: MutNullableDom<Node>,
156}
157
158impl DocumentFocusHandler {
159 pub(crate) fn new(window: &Window, has_focus: bool) -> Self {
160 Self {
161 window: Dom::from_ref(window),
162 focused_area: Default::default(),
163 focus_sequence: Cell::new(FocusSequenceNumber::default()),
164 has_focus: Cell::new(has_focus),
165 sequential_focus_navigation_starting_point: Default::default(),
166 }
167 }
168
169 pub(crate) fn has_focus(&self) -> bool {
170 self.has_focus.get()
171 }
172
173 pub(crate) fn set_has_focus(&self, has_focus: bool) {
174 self.has_focus.set(has_focus);
175 }
176
177 /// Return the element that currently has focus. If `None` is returned the viewport itself has focus.
178 pub(crate) fn focused_area(&self) -> Ref<'_, FocusableArea> {
179 self.focused_area.borrow()
180 }
181
182 /// Set the element that currently has focus and update the focus state for both the previously
183 /// set element (if any) and the new one, as well as the new one. This will not do anything if
184 /// the new element is the same as the previous one. Note that this *will not* fire any focus
185 /// events. If that is necessary the [`DocumentFocusHandler::focus`] should be used.
186 pub(crate) fn set_focused_area(&self, new_focusable_area: FocusableArea) {
187 if new_focusable_area == *self.focused_area.borrow() {
188 return;
189 }
190
191 // From <https://html.spec.whatwg.org/multipage/#selector-focus>
192 // > For the purposes of the CSS :focus pseudo-class, an element has the focus when:
193 // > - it is not itself a navigable container; and
194 // > - any of the following are true:
195 // > - it is one of the elements listed in the current focus chain of the top-level
196 // > traversable; or
197 // > - its shadow root shadowRoot is not null and shadowRoot is the root of at least one
198 // > element that has the focus.
199 //
200 // We are trying to accomplish the last requirement here, by walking up the tree and
201 // marking each shadow host as focused.
202 fn recursively_set_focus_status(element: &Element, new_state: bool) {
203 element.set_focus_state(new_state);
204
205 let Some(shadow_root) = element.containing_shadow_root() else {
206 return;
207 };
208 recursively_set_focus_status(&shadow_root.Host(), new_state);
209 }
210
211 if let Some(previously_focused_element) = self.focused_area.borrow().element() {
212 recursively_set_focus_status(previously_focused_element, false);
213 }
214 if let Some(newly_focused_element) = new_focusable_area.element() {
215 recursively_set_focus_status(newly_focused_element, true);
216 }
217
218 *self.focused_area.borrow_mut() = new_focusable_area;
219 }
220
221 /// Get the last sequence number sent to the constellation.
222 ///
223 /// Received focus-related messages with sequence numbers less than the one
224 /// returned by this method must be discarded.
225 pub fn focus_sequence(&self) -> FocusSequenceNumber {
226 self.focus_sequence.get()
227 }
228
229 /// Generate the next sequence number for focus-related messages.
230 fn increment_fetch_focus_sequence(&self) -> FocusSequenceNumber {
231 self.focus_sequence.set(FocusSequenceNumber(
232 self.focus_sequence
233 .get()
234 .0
235 .checked_add(1)
236 .expect("too many focus messages have been sent"),
237 ));
238 self.focus_sequence.get()
239 }
240
241 /// <https://html.spec.whatwg.org/multipage/#current-focus-chain-of-a-top-level-traversable>
242 pub(crate) fn current_focus_chain(&self) -> Vec<FocusableArea> {
243 // > The current focus chain of a top-level traversable is the focus chain of the
244 // > currently focused area of traversable, if traversable is non-null, or an empty list
245 // > otherwise.
246
247 // We cannot easily get the full focus chain of the top-level traversable, so we just
248 // get the bits that intersect with this `Document`. The rest will be handled
249 // internally in [`Self::focus_update_steps`].
250 if !self.has_focus() {
251 return vec![];
252 }
253 self.focused_area().focus_chain()
254 }
255
256 /// Reassign the focus context to the element that last requested focus during this
257 /// transaction, or the document if no elements requested it.
258 pub(crate) fn focus(&self, cx: &mut JSContext, new_focus_target: FocusableArea) {
259 let old_focus_chain = self.current_focus_chain();
260 let new_focus_chain = new_focus_target.focus_chain();
261 self.focus_update_steps(cx, new_focus_chain, old_focus_chain, &new_focus_target);
262
263 // Advertise the change in the focus chain.
264 // <https://html.spec.whatwg.org/multipage/#focus-chain>
265 // <https://html.spec.whatwg.org/multipage/#focusing-steps>
266 //
267 // TODO: Integrate this into the "focus update steps."
268 //
269 // If the top-level BC doesn't have system focus, this won't
270 // have an immediate effect, but it will when we gain system
271 // focus again. Therefore we still have to send `ScriptMsg::
272 // Focus`.
273 //
274 // When a container with a non-null nested browsing context is
275 // focused, its active document becomes the focused area of the
276 // top-level browsing context instead. Therefore we need to let
277 // the constellation know if such a container is focused.
278 //
279 // > The focusing steps for an object `new focus target` [...]
280 // >
281 // > 3. If `new focus target` is a browsing context container
282 // > with non-null nested browsing context, then set
283 // > `new focus target` to the nested browsing context's
284 // > active document.
285 let child_browsing_context_id = match new_focus_target {
286 FocusableArea::IFrameViewport { iframe_element, .. } => {
287 iframe_element.browsing_context_id()
288 },
289 _ => None,
290 };
291 let sequence = self.increment_fetch_focus_sequence();
292
293 debug!(
294 "Advertising the focus request to the constellation \
295 with sequence number {sequence:?} and child \
296 {child_browsing_context_id:?}",
297 );
298 self.window.send_to_constellation(
299 ScriptToConstellationMessage::FocusAncestorBrowsingContextsForFocusingSteps(
300 child_browsing_context_id,
301 sequence,
302 ),
303 );
304 }
305
306 /// <https://html.spec.whatwg.org/multipage/#focus-update-steps>
307 pub(crate) fn focus_update_steps(
308 &self,
309 cx: &mut JSContext,
310 mut new_focus_chain: Vec<FocusableArea>,
311 mut old_focus_chain: Vec<FocusableArea>,
312 new_focus_target: &FocusableArea,
313 ) {
314 let new_focus_chain_was_empty = new_focus_chain.is_empty();
315
316 // Step 1: If the last entry in old chain and the last entry in new chain are the same,
317 // pop the last entry from old chain and the last entry from new chain and redo this
318 // step.
319 //
320 // We avoid recursion here.
321 while let (Some(last_new), Some(last_old)) =
322 (new_focus_chain.last(), old_focus_chain.last())
323 {
324 if last_new == last_old {
325 new_focus_chain.pop();
326 old_focus_chain.pop();
327 } else {
328 break;
329 }
330 }
331
332 // If the two focus chains are both empty, focus hasn't changed. This isn't in the
333 // specification, but we must do it because we set the focused area to the viewport
334 // before blurring. If no focus changes, that would mean the currently focused element
335 // loses focus.
336 if old_focus_chain.is_empty() && new_focus_chain.is_empty() {
337 return;
338 }
339 // Although the "focusing steps" in the HTML specification say to wait until after firing
340 // the "blur" event to change the currently focused area of the Document, browsers tend
341 // to set it to the viewport before firing the "blur" event.
342 //
343 // See https://github.com/whatwg/html/issues/1569
344 self.set_focused_area(FocusableArea::Viewport);
345
346 // Step 2: For each entry entry in old chain, in order, run these substeps:
347 // Note: `old_focus_chain` might be empty!
348 let last_old_focus_chain_entry = old_focus_chain.len().saturating_sub(1);
349 for (index, entry) in old_focus_chain.iter().enumerate() {
350 // Step 2.1: If entry is an input element, and the change event applies to the element,
351 // and the element does not have a defined activation behavior, and the user has
352 // changed the element's value or its list of selected files while the control was
353 // focused without committing that change (such that it is different to what it was
354 // when the control was first focused), then:
355 // Step 2.1.1: Set entry's user validity to true.
356 // Step 2.1.2: Fire an event named change at the element, with the bubbles attribute initialized to true.
357 // TODO: Implement this.
358
359 // Step 2.2:
360 // - If entry is an element, let blur event target be entry.
361 // - If entry is a Document object, let blur event target be that Document object's
362 // relevant global object.
363 // - Otherwise, let blur event target be null.
364 //
365 // Note: We always send focus and blur events for `<iframe>` elements, but other
366 // browsers only seem to do that conditionally. This needs a bit more research.
367 let blur_event_target = match entry {
368 FocusableArea::Node { node, .. } => Some(node.upcast::<EventTarget>()),
369 FocusableArea::IFrameViewport { iframe_element, .. } => {
370 Some(iframe_element.upcast())
371 },
372 FocusableArea::Viewport => Some(self.window.upcast::<EventTarget>()),
373 };
374
375 // Step 2.3: If entry is the last entry in old chain, and entry is an Element, and
376 // the last entry in new chain is also an Element, then let related blur target be
377 // the last entry in new chain. Otherwise, let related blur target be null.
378 //
379 // Note: This can only happen when the focused `Document` doesn't change and we are
380 // moving focus from one element to another. These elements are the last in the chain
381 // because of the popping we do at the start of these steps.
382 let related_blur_target = match new_focus_chain.last() {
383 Some(FocusableArea::Node { node, .. })
384 if index == last_old_focus_chain_entry &&
385 matches!(entry, FocusableArea::Node { .. }) =>
386 {
387 Some(node.upcast())
388 },
389 _ => None,
390 };
391
392 // Step 2.4: If blur event target is not null, fire a focus event named blur at
393 // blur event target, with related blur target as the related target.
394 if let Some(blur_event_target) = blur_event_target {
395 self.fire_focus_event(
396 cx,
397 FocusEventType::Blur,
398 blur_event_target,
399 related_blur_target,
400 );
401 }
402 }
403
404 // Step 3: Apply any relevant platform-specific conventions for focusing new focus
405 // target. (For example, some platforms select the contents of a text control when that
406 // control is focused.)
407 if &*self.focused_area() != new_focus_target &&
408 let Some(html_element) = new_focus_target
409 .element()
410 .and_then(|element| element.downcast::<HTMLElement>())
411 {
412 html_element.handle_focus_state_for_contenteditable(cx);
413 }
414
415 self.set_has_focus(!new_focus_chain_was_empty);
416
417 // Step 4: For each entry entry in new chain, in reverse order, run these substeps:
418 // Note: `new_focus_chain` might be empty!
419 let last_new_focus_chain_entry = new_focus_chain.len().saturating_sub(1); // Might be empty, so calculated here.
420 for (index, entry) in new_focus_chain.iter().enumerate().rev() {
421 // Step 4.1: If entry is a focusable area, and the focused area of the document is
422 // not entry:
423 //
424 // Here we deviate from the specification a bit, as all focus chain elements are
425 // focusable areas currently. We just assume that it means the first entry of the
426 // chain, which is the new focus target
427 if index == 0 {
428 // Step 4.1.1: Set document's relevant global object's navigation API's focus
429 // changed during ongoing navigation to true.
430 // TODO: Implement this.
431
432 // Step 4.1.2: Designate entry as the focused area of the document.
433 self.set_focused_area(entry.clone());
434 }
435
436 // Step 4.2:
437 // - If entry is an element, let focus event target be entry.
438 // - If entry is a Document object, let focus event target be that Document
439 // object's relevant global object.
440 // - Otherwise, let focus event target be null.
441 //
442 // Note: We always send focus and blur events for `<iframe>` elements, but other
443 // browsers only seem to do that conditionally. This needs a bit more research.
444 let focus_event_target = match entry {
445 FocusableArea::Node { node, .. } => Some(node.upcast::<EventTarget>()),
446 FocusableArea::IFrameViewport { iframe_element, .. } => {
447 Some(iframe_element.upcast())
448 },
449 FocusableArea::Viewport => Some(self.window.upcast::<EventTarget>()),
450 };
451
452 // Step 4.3: If entry is the last entry in new chain, and entry is an Element, and
453 // the last entry in old chain is also an Element, then let related focus target be
454 // the last entry in old chain. Otherwise, let related focus target be null.
455 //
456 // Note: This can only happen when the focused `Document` doesn't change and we are
457 // moving focus from one element to another. These elements are the last in the chain
458 // because of the popping we do at the start of these steps.
459 let related_focus_target = match old_focus_chain.last() {
460 Some(FocusableArea::Node { node, .. })
461 if index == last_new_focus_chain_entry &&
462 matches!(entry, FocusableArea::Node { .. }) =>
463 {
464 Some(node.upcast())
465 },
466 _ => None,
467 };
468
469 // Step 4.4: If focus event target is not null, fire a focus event named focus at
470 // focus event target, with related focus target as the related target.
471 if let Some(focus_event_target) = focus_event_target {
472 self.fire_focus_event(
473 cx,
474 FocusEventType::Focus,
475 focus_event_target,
476 related_focus_target,
477 );
478 }
479 }
480 }
481
482 /// <https://html.spec.whatwg.org/multipage/#fire-a-focus-event>
483 pub(crate) fn fire_focus_event(
484 &self,
485 cx: &mut JSContext,
486 focus_event_type: FocusEventType,
487 event_target: &EventTarget,
488 related_target: Option<&EventTarget>,
489 ) {
490 let event_name = match focus_event_type {
491 FocusEventType::Focus => "focus".into(),
492 FocusEventType::Blur => "blur".into(),
493 };
494
495 let event = FocusEvent::new(
496 cx,
497 &self.window,
498 event_name,
499 EventBubbles::DoesNotBubble,
500 EventCancelable::NotCancelable,
501 Some(&self.window),
502 0i32,
503 related_target,
504 );
505 let event = event.upcast::<Event>();
506 event.set_trusted(true);
507 event.fire(cx, event_target);
508 }
509
510 /// <https://html.spec.whatwg.org/multipage/#focus-fixup-rule>
511 /// > For each doc of docs, if the focused area of doc is not a focusable area, then run the
512 /// > focusing steps for doc's viewport, and set doc's relevant global object's navigation API's
513 /// > focus changed during ongoing navigation to false.
514 ///
515 /// TODO: Handle the "focus changed during ongoing navigation" flag.
516 pub(crate) fn perform_focus_fixup_rule(&self, cx: &mut JSContext) {
517 if self
518 .focused_area
519 .borrow()
520 .element()
521 .is_none_or(|focused| focused.is_focusable_area())
522 {
523 return;
524 }
525 self.focus(cx, FocusableArea::Viewport);
526 }
527
528 pub(crate) fn set_sequential_focus_navigation_starting_point(&self, node: &Node) {
529 self.sequential_focus_navigation_starting_point
530 .set(Some(node));
531 }
532
533 fn sequential_focus_navigation_starting_point(&self) -> Option<DomRoot<Node>> {
534 self.sequential_focus_navigation_starting_point
535 .get()
536 .filter(|node| node.is_connected())
537 }
538
539 pub(crate) fn sequential_focus_navigation_via_keyboard_event(
540 &self,
541 cx: &mut JSContext,
542 event: &KeyboardEvent,
543 ) {
544 let direction = if event.modifiers().contains(Modifiers::SHIFT) {
545 SequentialFocusDirection::Backward
546 } else {
547 SequentialFocusDirection::Forward
548 };
549
550 self.sequential_focus_navigation(cx, direction);
551 }
552
553 /// <https://html.spec.whatwg.org/multipage/#sequential-focus-navigation:currently-focused-area-of-a-top-level-traversable>
554 fn sequential_focus_navigation(&self, cx: &mut JSContext, direction: SequentialFocusDirection) {
555 // > When the user requests that focus move from the currently focused area of a top-level
556 // > traversable to the next or previous focusable area (e.g., as the default action of
557 // > pressing the tab key), or when the user requests that focus sequentially move to a
558 // > top-level traversable in the first place (e.g., from the browser's location bar), the
559 // > user agent must use the following algorithm:
560
561 // > 1. Let starting point be the currently focused area of a top-level traversable, if the
562 // > user requested to move focus sequentially from there, or else the top-level traversable
563 // > itself, if the user instead requested to move focus from outside the top-level
564 // > traversable.
565 //
566 // Note: Here `None` represents the current traversible.
567 let mut starting_point = self
568 .focused_area()
569 .element()
570 .map(|element| DomRoot::from_ref(element.upcast::<Node>()));
571
572 // > 2. If there is a sequential focus navigation starting point defined and it is inside
573 // > starting point, then let starting point be the sequential focus navigation starting point
574 // > instead.
575 if let Some(sequential_focus_navigation_starting_point) =
576 self.sequential_focus_navigation_starting_point() &&
577 starting_point.as_ref().is_none_or(|starting_point| {
578 starting_point.is_ancestor_of(&sequential_focus_navigation_starting_point)
579 })
580 {
581 starting_point = Some(sequential_focus_navigation_starting_point);
582 }
583
584 // > 3. Let direction be "forward" if the user requested the next control, and "backward" if
585 // > the user requested the previous control.
586 //
587 // Note: This is handled by the `direction` argument to this method.
588 self.sequential_focus_navigation_loop(
589 cx,
590 starting_point,
591 direction,
592 false, /* allow_focusing_viewport */
593 );
594 }
595
596 /// The inner loop ("Loop") of:
597 /// <https://html.spec.whatwg.org/multipage/#sequential-focus-navigation:currently-focused-area-of-a-top-level-traversable>
598 fn sequential_focus_navigation_loop(
599 &self,
600 cx: &mut JSContext,
601 starting_point: Option<DomRoot<Node>>,
602 direction: SequentialFocusDirection,
603 allow_focusing_viewport: bool,
604 ) {
605 // > 4. Loop: Let selection mechanism be "sequential" if starting point is a navigable or if
606 // > starting point is in its Document's sequential focus navigation order.
607 // > Otherwise, starting point is not in its Document's sequential focus navigation order;
608 // > let selection mechanism be "DOM".
609 let starting_point_is_navigable = starting_point
610 .as_ref()
611 .is_none_or(|starting_point| starting_point.is::<HTMLIFrameElement>());
612 let selection_mechanism = starting_point
613 .as_ref()
614 .and_then(|node| node.downcast::<Element>())
615 .filter(|element| element.is_sequentially_focusable())
616 .map(|element| {
617 SequentialFocusNavigationMechanism::Sequential(
618 element.explicitly_set_tab_index().unwrap_or_default(),
619 )
620 })
621 .unwrap_or_else(|| {
622 if starting_point_is_navigable {
623 SequentialFocusNavigationMechanism::FirstOrLast
624 } else {
625 SequentialFocusNavigationMechanism::Dom
626 }
627 });
628
629 // > 5. Let candidate be the result of running the sequential navigation search algorithm
630 // > with starting point, direction, and selection mechanism.
631 let candidate = SequentialFocusNavigationSearch::new(
632 starting_point
633 .as_ref()
634 .and_then(|node| node.containing_focus_navigation_scope_owner())
635 .unwrap_or_else(|| FocusNavigationScopeOwner::Document(self.window.Document())),
636 direction,
637 selection_mechanism,
638 starting_point,
639 )
640 .search();
641
642 // > 6. If candidate is not null, then run the focusing steps for candidate and return.
643 if let Some(candidate) = candidate {
644 let document = self.window.Document();
645 let event_handler = document.event_handler();
646 event_handler.focus_and_scroll_to_element_for_key_event(cx, &candidate);
647 // We can't simply run the focusing steps, because:
648 // 1. The focusing steps do not scroll to the element.
649 // 2. When focus shifts to a child navigable (iframe) we have special behavior to reach
650 // across document boundaries to focus the first focusable element in the iframe.
651 match candidate.downcast::<HTMLIFrameElement>() {
652 Some(iframe_element) => self.sequentially_focus_child_iframe_local_or_remote(
653 cx,
654 iframe_element,
655 direction,
656 ),
657 None => event_handler.focus_and_scroll_to_element_for_key_event(cx, &candidate),
658 }
659 return;
660 }
661
662 // > 7. Otherwise, unset the sequential focus navigation starting point.
663 self.sequential_focus_navigation_starting_point.clear();
664
665 // This is not in the specification, but there's a difference between moving focus into
666 // a child `<iframe>` and within a Document. If no suitable focusable area can be found
667 // when moving into an `<iframe>`, we want to focus the `<iframe>`'s viewport itself.
668 if allow_focusing_viewport {
669 self.focus(cx, FocusableArea::Viewport);
670 return;
671 }
672
673 // > 8. If starting point is a top-level traversable, or a focusable area in the top-level
674 // > traversable, the user agent should transfer focus to its own controls appropriately (if
675 // > any), honouring direction, and then return.
676 // TODO: Implement this.
677 if self.window.is_top_level() {
678 return;
679 }
680
681 // > 9. Otherwise, starting point is a focusable area in a child navigable. Set starting
682 // > point to that child navigable's parent and return to the step labeled loop.
683 self.sequentially_focus_parent_local_or_remote(cx, direction);
684 }
685
686 fn sequentially_focus_child_iframe_local_or_remote(
687 &self,
688 cx: &mut JSContext,
689 iframe_element: &HTMLIFrameElement,
690 direction: SequentialFocusDirection,
691 ) {
692 if let Some(content_document) = iframe_element.GetContentDocument() {
693 // The <iframe> is in the same `ScriptThread` and we have direct access to it. We can
694 // move the focus directly.
695 content_document
696 .focus_handler()
697 .sequential_focus_from_another_document(cx, None, direction);
698 } else if let Some(browsing_context_id) = iframe_element.browsing_context_id() {
699 self.window.send_to_constellation(
700 ScriptToConstellationMessage::FocusRemoteBrowsingContext(
701 browsing_context_id,
702 RemoteFocusOperation::Sequential(direction, None),
703 ),
704 );
705 } else {
706 iframe_element
707 .upcast::<Node>()
708 .run_the_focusing_steps(cx, None);
709 }
710 }
711
712 fn sequentially_focus_parent_local_or_remote(
713 &self,
714 cx: &mut JSContext,
715 direction: SequentialFocusDirection,
716 ) {
717 let window_proxy = self.window.window_proxy();
718 if let Some(iframe) = window_proxy.frame_element() {
719 // The parent browsing context is in the same `ScriptThread` and we have direct access
720 // to it. We can move the focus directly.
721 let browsing_context_id = iframe
722 .downcast::<HTMLIFrameElement>()
723 .and_then(|iframe_element| iframe_element.browsing_context_id());
724 iframe
725 .owner_document()
726 .focus_handler()
727 .sequential_focus_from_another_document(cx, browsing_context_id, direction);
728 } else if let Some(browsing_context_id) = window_proxy
729 .parent()
730 .map(|parent| parent.browsing_context_id())
731 {
732 self.window.send_to_constellation(
733 ScriptToConstellationMessage::FocusRemoteBrowsingContext(
734 browsing_context_id,
735 RemoteFocusOperation::Sequential(
736 direction,
737 Some(window_proxy.browsing_context_id()),
738 ),
739 ),
740 );
741 }
742 }
743
744 pub(crate) fn sequential_focus_from_another_document(
745 &self,
746 cx: &mut JSContext,
747 browsing_context_id: Option<BrowsingContextId>,
748 direction: SequentialFocusDirection,
749 ) {
750 let mut realm = enter_auto_realm(cx, &*self.window);
751 let cx = &mut realm.current_realm();
752 let starting_point = browsing_context_id.and_then(|browsing_context_id| {
753 self.window
754 .Document()
755 .iframes()
756 .get(browsing_context_id)
757 .map(|iframe| DomRoot::from_ref(iframe.element.upcast::<Node>()))
758 });
759 self.sequential_focus_navigation_loop(
760 cx,
761 starting_point,
762 direction,
763 true, /* allow focusing viewport */
764 );
765 }
766}
767
768/// <https://html.spec.whatwg.org/multipage/#selection-mechanism>
769///
770/// This also incorporates the case where the starting point is a navigable
771/// as that is a distinct set of behaviors from the two kinds of mechanisms
772/// listed in the specification.
773#[derive(Clone, Copy, Debug)]
774pub(crate) enum SequentialFocusNavigationMechanism {
775 Dom,
776 Sequential(i32 /* focused_element_tab_index */),
777 /// This case isn't mentioned explicitly in the specification, but it's implied. It works
778 /// like `Sequential`, but without a starting point. This kind of search will return the
779 /// first or last (depending on direction) sequentially focusable element in sequential
780 /// focus order. This is used in two situations:
781 ///
782 /// - When the starting point is a navigable
783 /// - When descending into a nested focus scope
784 FirstOrLast,
785}
786
787#[derive(PartialEq)]
788enum Continue {
789 Yes,
790 No,
791}
792
793#[derive(PartialEq)]
794pub(crate) enum SequentialFocusNavigationSearchContext {
795 /// The focus scope that initiated this search. Containing contexts and nested contexts can
796 /// both be searched.
797 Original,
798 /// The search has descended into a nested search scope. Containing contexts should never
799 /// be searched.
800 Nested,
801 /// The search has ascended to search a containing search scope. The starting point's focus
802 /// scope should never be searched to avoid cycles.
803 Containing,
804}
805
806/// This structure is used to do a traversal search of the DOM in order to find an
807/// appropriate target when doing sequential focus navigation, such as when handling
808/// tab key presses.
809///
810/// The specification talks about the [flattened tabindex-ordered focus navigation scope],
811/// which represents all of the [tabindex-ordered focus navigation scope]s of a particular
812/// page, flattened into a single list of all the sequentially focusable areas of the
813/// page. Then, the specification describes how to search this list during sequential focus
814/// navigation.
815///
816/// The choice that Servo and other browsers make is to trade updating this flattened list
817/// during every DOM mutation (frequent) with a DOM traversal of, potentially, the entire
818/// document during sequential focus navigation (infrequent).
819///
820/// The search done via [`SequentialFocusNavigationSearch`] matches the semantics of the
821/// flattened tabindex-ordered focus navigation scope without having to maintain the
822/// flattened list. It uses a series of nested traversals (one per focus scope) that
823/// only considers each focusable area of a page at most once.
824///
825/// The search performs a linear DOM traversal starting at the containing focus scope of
826/// the search start point. When encountering a nested focus scope, if that scope could
827/// contain the final target for the search, the search recurses into the nested scope. If
828/// the search reaches the end of a focus scope without finding a candidate, the search
829/// continues in the focus scope's containing scope (though never re-ascending back into a
830/// scope it recursed from).
831///
832/// [flattened tabindex-ordered focus navigation scope]: https://html.spec.whatwg.org/multipage/#flattened-tabindex-ordered-focus-navigation-scope
833/// [tabindex-ordered focus navigation scope]: https://html.spec.whatwg.org/multipage/#tabindex-ordered-focus-navigation-scope
834pub(crate) struct SequentialFocusNavigationSearch {
835 focus_navigation_scope_owner: FocusNavigationScopeOwner,
836 direction: SequentialFocusDirection,
837 mechanism: SequentialFocusNavigationMechanism,
838 starting_point: Option<DomRoot<Node>>,
839 current_winner: Option<(DomRoot<Element>, i32)>,
840 passed_starting_point: bool,
841 search_context: SequentialFocusNavigationSearchContext,
842}
843
844impl SequentialFocusNavigationSearch {
845 pub(crate) fn new(
846 focus_navigation_scope_owner: FocusNavigationScopeOwner,
847 direction: SequentialFocusDirection,
848 mechanism: SequentialFocusNavigationMechanism,
849 starting_point: Option<DomRoot<Node>>,
850 ) -> Self {
851 // If there's no starting point, the starting point is actually the root element, which
852 // we always have passed.
853 let passed_starting_point = starting_point.is_none();
854 Self {
855 focus_navigation_scope_owner,
856 direction,
857 mechanism,
858 starting_point,
859 current_winner: Default::default(),
860 passed_starting_point,
861 search_context: SequentialFocusNavigationSearchContext::Original,
862 }
863 }
864
865 pub(crate) fn search(mut self) -> Option<DomRoot<Element>> {
866 for node in self.focus_navigation_scope_owner.iterator() {
867 if self.process_node(&node) == Continue::No {
868 break;
869 }
870 }
871
872 if let Some(winner) = self.current_winner.take() {
873 return Some(winner.0);
874 }
875
876 // If searching a nested focus navigation scope, never try to search the containing
877 // scope, as that will lead to an endless cycle.
878 if self.search_context != SequentialFocusNavigationSearchContext::Nested {
879 return self.maybe_search_in_containing_focus_navigation_scope();
880 }
881
882 None
883 }
884
885 fn maybe_search_in_containing_focus_navigation_scope(&self) -> Option<DomRoot<Element>> {
886 let containing_node = self.focus_navigation_scope_owner.node();
887 let containing_focus_navigation_scope_owner =
888 containing_node.containing_focus_navigation_scope_owner()?;
889
890 let tab_index = containing_node
891 .downcast::<Element>()?
892 .explicitly_set_tab_index()
893 .unwrap_or_default();
894 let mechanism = match &self.mechanism {
895 // If the traversal was sequential, but the containing focus navigation scope owner was
896 // explicitly marked as not sequentially focusable, the search in the containing scope
897 // needs to work like a DOM traversal i.e. take the first sequentially focusable target
898 // after this one in the parent traversal.
899 SequentialFocusNavigationMechanism::Sequential(..) if tab_index == -1 => {
900 SequentialFocusNavigationMechanism::Dom
901 },
902 SequentialFocusNavigationMechanism::Sequential(..) => {
903 SequentialFocusNavigationMechanism::Sequential(tab_index)
904 },
905 mechanism => *mechanism,
906 };
907
908 if self.direction == SequentialFocusDirection::Backward &&
909 let Some(containing_element) = containing_node.downcast::<Element>() &&
910 containing_element.is_sequentially_focusable()
911 {
912 return Some(DomRoot::from_ref(containing_element));
913 }
914
915 Self {
916 focus_navigation_scope_owner: containing_focus_navigation_scope_owner,
917 direction: self.direction,
918 mechanism,
919 starting_point: Some(DomRoot::from_ref(containing_node)),
920 current_winner: Default::default(),
921 passed_starting_point: false,
922 search_context: SequentialFocusNavigationSearchContext::Containing,
923 }
924 .search()
925 }
926
927 fn process_node(&mut self, node: &Node) -> Continue {
928 if Some(node) == self.starting_point.as_deref() {
929 self.passed_starting_point = true;
930 } else if self.process_node_as_sequentially_focusable_node(node) == Continue::No {
931 return Continue::No;
932 }
933
934 self.process_node_as_focus_scope_owner(node)
935 }
936
937 /// If this node is sequentially focusable, consider whether or not to accept it
938 /// as the new winner.
939 fn process_node_as_sequentially_focusable_node(&mut self, node: &Node) -> Continue {
940 let Some(element) = node.downcast::<Element>() else {
941 return Continue::Yes;
942 };
943 if !element.is_sequentially_focusable() {
944 return Continue::Yes;
945 }
946
947 let tab_index = element.explicitly_set_tab_index().unwrap_or_default();
948 let (is_new_winner, should_continue) = self.process_candidate_with_tab_index(tab_index);
949 if is_new_winner {
950 self.current_winner = Some((DomRoot::from_ref(element), tab_index));
951 }
952 should_continue
953 }
954
955 /// If this node itself forms a nested sequential focus scope, decide whether or
956 /// not to descend and consider its contained focusable areas as candidates.
957 fn process_node_as_focus_scope_owner(&mut self, node: &Node) -> Continue {
958 // Never try to recurse into the same focus scope that we are in. This path
959 // might be reached if we are in the root focus scope where the document is
960 // one of the nodes processed.
961 if self.focus_navigation_scope_owner.node() == node {
962 return Continue::Yes;
963 }
964
965 // If the search has ascended into a containing scope, never try to search back down
966 // into the scope that originated this part of the search. Otherwise the search would
967 // cycle endlessly.
968 if Some(node) == self.starting_point.as_deref() &&
969 self.search_context == SequentialFocusNavigationSearchContext::Containing
970 {
971 return Continue::Yes;
972 }
973
974 let Some(focus_navigation_scope_owner) = node.as_focus_navigation_scope_owner() else {
975 return Continue::Yes;
976 };
977
978 // The candidate inherits the tab index of the node that establishes its containing
979 // sequential focus navigation scope.
980 let tab_index = focus_navigation_scope_owner
981 .node()
982 .downcast::<Element>()
983 .and_then(Element::explicitly_set_tab_index)
984 .unwrap_or_default();
985 let (is_new_winner, should_continue) = self.process_candidate_with_tab_index(tab_index);
986 if !is_new_winner {
987 return should_continue;
988 }
989
990 let mechanism = match self.mechanism {
991 // If we were searching without regard to sequential focus order, keep doing that.
992 SequentialFocusNavigationMechanism::Dom => SequentialFocusNavigationMechanism::Dom,
993 // If we were searching taking into account sequential focus order, keep doing that, but
994 // take the first candidate in sequential focus order without regard to the outer scope's
995 // starting point or tab index.
996 _ => SequentialFocusNavigationMechanism::FirstOrLast,
997 };
998
999 let element = Self {
1000 focus_navigation_scope_owner,
1001 direction: self.direction,
1002 mechanism,
1003 starting_point: None,
1004 current_winner: Default::default(),
1005 passed_starting_point: self.passed_starting_point,
1006 search_context: SequentialFocusNavigationSearchContext::Nested,
1007 }
1008 .search();
1009
1010 let Some(element) = element else {
1011 return Continue::Yes;
1012 };
1013
1014 self.current_winner = Some((element, tab_index));
1015 should_continue
1016 }
1017
1018 /// Process the node or focus scope owner with the provided tab index according to this
1019 /// search's search mechanism. Returns a boolean that is true if this candidate is the new
1020 /// winner and a [`Continue`] which says whether to keep searching or stop.
1021 fn process_candidate_with_tab_index(&mut self, candidate_tab_index: i32) -> (bool, Continue) {
1022 match self.mechanism {
1023 SequentialFocusNavigationMechanism::Dom => self.process_element_for_dom_traversal(),
1024 SequentialFocusNavigationMechanism::Sequential(focused_element_tab_index) => self
1025 .process_element_for_sequential_traversal(
1026 candidate_tab_index,
1027 focused_element_tab_index,
1028 ),
1029 SequentialFocusNavigationMechanism::FirstOrLast => (
1030 self.process_element_for_first_or_last_traversal(candidate_tab_index),
1031 Continue::Yes,
1032 ),
1033 }
1034 }
1035
1036 /// Process the node or focus scope owner, given the state of [`Self::passed_starting_point`]
1037 /// with for searches with the [`SequentialFocusNavigationMechanism::Dom`] search mechanism.
1038 /// Returns a boolean that is true if this candidate is the new winner and a [`Continue`] which
1039 /// says whether to keep searching or stop.
1040 fn process_element_for_dom_traversal(&self) -> (bool, Continue) {
1041 match self.direction {
1042 // direction is "forward"
1043 // > Let candidate be the first suitable sequentially focusable area after starting point,
1044 // > in starting point's Document's sequential focus navigation order, if any; or else
1045 // > null
1046 SequentialFocusDirection::Forward if self.passed_starting_point => (true, Continue::No),
1047 // If searching forward, do not consider anything until passing the starting point.
1048 SequentialFocusDirection::Forward => (false, Continue::Yes),
1049 // direction is "backward"
1050 // > Let candidate be the last suitable sequentially focusable area before starting
1051 // > point, in starting point's Document's sequential focus navigation order, if any; or
1052 // > else null
1053 SequentialFocusDirection::Backward if !self.passed_starting_point => {
1054 (true, Continue::Yes)
1055 },
1056 // There is no possible winner after the starting point when searching backward.
1057 SequentialFocusDirection::Backward => (false, Continue::No),
1058 }
1059 }
1060
1061 /// Process the node or focus scope owner with the provided tab index for searches with the
1062 /// [`SequentialFocusNavigationMechanism::FirstOrLast`] search mechanism. Returns a boolean
1063 /// that is true if this candidate is the new winner.
1064 fn process_element_for_first_or_last_traversal(
1065 &self,
1066 candidate_element_tab_index: i32,
1067 ) -> bool {
1068 let Some((_, winning_tab_index)) = self.current_winner else {
1069 return true;
1070 };
1071
1072 let candidate_and_current_winner_ordering =
1073 compare_tab_indices(candidate_element_tab_index, winning_tab_index);
1074 match self.direction {
1075 // direction is "forward"
1076 // > Let candidate be the first suitable sequentially focusable area in starting point's
1077 // > active document, if any; or else null
1078 //
1079 // There's an ambiguity in the specification here. In this case it says to choose
1080 // the "first suitable sequentially focusable area." It's possible to interpret this
1081 // as the first in DOM order, but browsers seem to agree to follow tab index
1082 // order instead.
1083 //
1084 // Pick the lowest, prioritizing the earlier node when equal.
1085 SequentialFocusDirection::Forward
1086 if candidate_and_current_winner_ordering == Ordering::Less =>
1087 {
1088 true
1089 },
1090 // direction is "backward"
1091 // > Let candidate be the last suitable sequentially focusable area in starting point's
1092 // > active document, if any; or else null
1093 //
1094 // There's an ambiguity in the specification here. In this case it says to choose
1095 // the "last suitable sequentially focusable area" It's possible to interpret this
1096 // as the first in DOM order, but browsers seem to agree to following tab index
1097 // order instead.
1098 //
1099 // Pick the highest, prioritizing the later node when equal.
1100 SequentialFocusDirection::Backward
1101 if candidate_and_current_winner_ordering != Ordering::Less =>
1102 {
1103 true
1104 },
1105 _ => false,
1106 }
1107 }
1108
1109 /// Process the node or focus scope owner with the provided tab index for searches with the
1110 /// [`SequentialFocusNavigationMechanism::Sequential`] search mechanism. Returns a boolean
1111 /// that is true if this candidate is the new winner and a [`Continue`] which says whether to
1112 /// keep searching or stop.
1113 fn process_element_for_sequential_traversal(
1114 &self,
1115 candidate_element_tab_index: i32,
1116 focused_element_tab_index: i32,
1117 ) -> (bool, Continue) {
1118 let candidate_and_focused_ordering =
1119 compare_tab_indices(candidate_element_tab_index, focused_element_tab_index);
1120 match self.direction {
1121 SequentialFocusDirection::Forward => {
1122 // If moving forward the first element with equal tab index after the current
1123 // element is the winner.
1124 if self.passed_starting_point && candidate_and_focused_ordering == Ordering::Equal {
1125 return (true, Continue::No);
1126 }
1127 // If the candidate element does not have a greater tab index, then discard it.
1128 if candidate_and_focused_ordering != Ordering::Greater {
1129 return (false, Continue::Yes);
1130 }
1131 let Some((_, winning_tab_index)) = self.current_winner else {
1132 // If this candidate has a tab index which is one greater than the current
1133 // tab index, then we know it is the winner, because we give precedence to
1134 // elements earlier in the DOM.
1135 if candidate_element_tab_index == focused_element_tab_index + 1 {
1136 return (true, Continue::No);
1137 }
1138
1139 return (true, Continue::Yes);
1140 };
1141
1142 // If the candidate element has a lesser tab index than the current winner,
1143 // then it becomes the winner.
1144 let should_select =
1145 compare_tab_indices(candidate_element_tab_index, winning_tab_index) ==
1146 Ordering::Less;
1147
1148 (should_select, Continue::Yes)
1149 },
1150 SequentialFocusDirection::Backward => {
1151 // If moving backward the last element with an equal tab index that precedes
1152 // the focused element in the DOM is the winner.
1153 if !self.passed_starting_point && candidate_and_focused_ordering == Ordering::Equal
1154 {
1155 return (true, Continue::Yes);
1156 }
1157 // If the candidate does not have a lesser tab index, then discard it.
1158 if candidate_and_focused_ordering != Ordering::Less {
1159 return (false, Continue::Yes);
1160 }
1161 let Some((_, winning_tab_index)) = self.current_winner else {
1162 return (true, Continue::Yes);
1163 };
1164 // If the candidate element's tab index is not less than the current winner,
1165 // then it becomes the new winner. This means that when the tab indices are
1166 // equal, we give preference to the last one in DOM order.
1167 let should_select =
1168 compare_tab_indices(candidate_element_tab_index, winning_tab_index) !=
1169 Ordering::Less;
1170 (should_select, Continue::Yes)
1171 },
1172 }
1173 }
1174}
1175
1176/// Compare two tab indices according to <https://html.spec.whatwg.org/multipage/#tabindex-value>.
1177///
1178/// `Ordering::Less`: The index should come before the other in sequential focus order.
1179/// `Ordering::Equal`: The two indices should be processed in DOM order, respecting focus direction
1180/// and focus scopes.
1181/// `Ordering::Greater`: The index should come after the other in sequential focus order.
1182///
1183/// Note that a tabindex of 0 should come after all others, which is essentially why we need this
1184/// function.
1185fn compare_tab_indices(a: i32, b: i32) -> Ordering {
1186 if a == b {
1187 Ordering::Equal
1188 } else if a == 0 {
1189 Ordering::Greater
1190 } else if b == 0 {
1191 Ordering::Less
1192 } else {
1193 a.cmp(&b)
1194 }
1195}