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};
6
7use bitflags::bitflags;
8use embedder_traits::FocusSequenceNumber;
9use js::context::JSContext;
10use script_bindings::codegen::GenericBindings::HTMLIFrameElementBinding::HTMLIFrameElementMethods;
11use script_bindings::codegen::GenericBindings::ShadowRootBinding::ShadowRootMethods;
12use script_bindings::codegen::GenericBindings::WindowBinding::WindowMethods;
13use script_bindings::inheritance::Castable;
14use script_bindings::root::{Dom, DomRoot};
15use script_bindings::script_runtime::CanGc;
16use servo_base::id::BrowsingContextId;
17use servo_constellation_traits::{
18 RemoteFocusOperation, ScriptToConstellationMessage, SequentialFocusDirection,
19};
20
21use crate::dom::bindings::cell::DomRefCell;
22use crate::dom::focusevent::FocusEventType;
23use crate::dom::types::{Element, EventTarget, FocusEvent, HTMLElement, HTMLIFrameElement, Window};
24use crate::dom::{Document, Event, EventBubbles, EventCancelable, Node, NodeTraits};
25use crate::realms::enter_realm;
26
27/// The kind of focusable area a [`FocusableArea`] is. A [`FocusableArea`] may be click focusable,
28/// sequentially focusable, or both.
29#[derive(Clone, Copy, Debug, Default, JSTraceable, MallocSizeOf, PartialEq)]
30pub(crate) struct FocusableAreaKind(u8);
31
32bitflags! {
33 impl FocusableAreaKind: u8 {
34 /// <https://html.spec.whatwg.org/multipage/#click-focusable>
35 ///
36 /// > A focusable area is said to be click focusable if the user agent determines that it is
37 /// > click focusable. User agents should consider focusable areas with non-null tabindex values
38 /// > to be click focusable.
39 const Click = 1 << 0;
40 /// <https://html.spec.whatwg.org/multipage/#sequentially-focusable>.
41 ///
42 /// > A focusable area is said to be sequentially focusable if it is included in its
43 /// > Document's sequential focus navigation order and the user agent determines that it is
44 /// > sequentially focusable.
45 const Sequential = 1 << 1;
46 }
47}
48
49/// <https://html.spec.whatwg.org/multipage/#focusable-area>
50#[derive(Clone, Default, JSTraceable, MallocSizeOf, PartialEq)]
51pub(crate) enum FocusableArea {
52 Node {
53 node: DomRoot<Node>,
54 kind: FocusableAreaKind,
55 },
56 /// The viewport of an `<iframe>` element in its containing `Document`. `<iframe>`s
57 /// are focusable areas, but have special behavior when focusing.
58 IFrameViewport {
59 iframe_element: DomRoot<HTMLIFrameElement>,
60 kind: FocusableAreaKind,
61 },
62 #[default]
63 Viewport,
64}
65
66impl std::fmt::Debug for FocusableArea {
67 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68 match self {
69 Self::Node { node, kind } => f
70 .debug_struct("Node")
71 .field("node", node)
72 .field("kind", kind)
73 .finish(),
74 Self::IFrameViewport {
75 iframe_element,
76 kind,
77 } => f
78 .debug_struct("IFrameViewport")
79 .field("pipeline", &iframe_element.pipeline_id())
80 .field("kind", kind)
81 .finish(),
82 Self::Viewport => write!(f, "Viewport"),
83 }
84 }
85}
86
87impl FocusableArea {
88 pub(crate) fn kind(&self) -> FocusableAreaKind {
89 match self {
90 Self::Node { kind, .. } | Self::IFrameViewport { kind, .. } => *kind,
91 Self::Viewport => FocusableAreaKind::Click | FocusableAreaKind::Sequential,
92 }
93 }
94
95 /// If this focusable area is a node, return it as an [`Element`] if it is possible, otherwise
96 /// return `None`. This is the [`Element`] to use for applying `:focus` state and for firing
97 /// `blur` and `focus` events if any.
98 ///
99 /// Note: This is currently in a transitional state while the code moves more toward the
100 /// specification.
101 pub(crate) fn element(&self) -> Option<&Element> {
102 match self {
103 Self::Node { node, .. } => node.downcast(),
104 Self::IFrameViewport { iframe_element, .. } => Some(iframe_element.upcast()),
105 Self::Viewport => None,
106 }
107 }
108
109 /// <https://html.spec.whatwg.org/multipage/#dom-anchor>
110 pub(crate) fn dom_anchor(&self, document: &Document) -> DomRoot<Node> {
111 match self {
112 Self::Node { node, .. } => node.clone(),
113 Self::IFrameViewport { iframe_element, .. } => {
114 DomRoot::from_ref(iframe_element.upcast())
115 },
116 Self::Viewport => DomRoot::from_ref(document.upcast()),
117 }
118 }
119
120 pub(crate) fn focus_chain(&self) -> Vec<FocusableArea> {
121 match self {
122 FocusableArea::Node { .. } | FocusableArea::IFrameViewport { .. } => {
123 vec![self.clone(), FocusableArea::Viewport]
124 },
125 FocusableArea::Viewport => vec![self.clone()],
126 }
127 }
128}
129
130/// The [`DocumentFocusHandler`] is a structure responsible for handling and storing data related to
131/// focus for the `Document`. It exists to decrease the size of the `Document`.
132/// structure.
133#[derive(JSTraceable, MallocSizeOf)]
134#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
135pub(crate) struct DocumentFocusHandler {
136 /// The [`Window`] element for this [`DocumentFocusHandler`].
137 window: Dom<Window>,
138 /// The focused area of the [`Document`].
139 ///
140 /// <https://html.spec.whatwg.org/multipage/#focused-area-of-the-document>
141 focused_area: DomRefCell<FocusableArea>,
142 /// The last sequence number sent to the constellation.
143 #[no_trace]
144 focus_sequence: Cell<FocusSequenceNumber>,
145 /// Indicates whether the container is included in the top-level browsing
146 /// context's focus chain (not considering system focus). Permanently `true`
147 /// for a top-level document.
148 has_focus: Cell<bool>,
149}
150
151impl DocumentFocusHandler {
152 pub(crate) fn new(window: &Window, has_focus: bool) -> Self {
153 Self {
154 window: Dom::from_ref(window),
155 focused_area: Default::default(),
156 focus_sequence: Cell::new(FocusSequenceNumber::default()),
157 has_focus: Cell::new(has_focus),
158 }
159 }
160
161 pub(crate) fn has_focus(&self) -> bool {
162 self.has_focus.get()
163 }
164
165 pub(crate) fn set_has_focus(&self, has_focus: bool) {
166 self.has_focus.set(has_focus);
167 }
168
169 /// Return the element that currently has focus. If `None` is returned the viewport itself has focus.
170 pub(crate) fn focused_area<'a>(&'a self) -> Ref<'a, FocusableArea> {
171 let focused_area = self.focused_area.borrow();
172 Ref::map(focused_area, |focused_area| focused_area)
173 }
174
175 /// Set the element that currently has focus and update the focus state for both the previously
176 /// set element (if any) and the new one, as well as the new one. This will not do anything if
177 /// the new element is the same as the previous one. Note that this *will not* fire any focus
178 /// events. If that is necessary the [`DocumentFocusHandler::focus`] should be used.
179 pub(crate) fn set_focused_area(&self, new_focusable_area: FocusableArea) {
180 if new_focusable_area == *self.focused_area.borrow() {
181 return;
182 }
183
184 // From <https://html.spec.whatwg.org/multipage/#selector-focus>
185 // > For the purposes of the CSS :focus pseudo-class, an element has the focus when:
186 // > - it is not itself a navigable container; and
187 // > - any of the following are true:
188 // > - it is one of the elements listed in the current focus chain of the top-level
189 // > traversable; or
190 // > - its shadow root shadowRoot is not null and shadowRoot is the root of at least one
191 // > element that has the focus.
192 //
193 // We are trying to accomplish the last requirement here, by walking up the tree and
194 // marking each shadow host as focused.
195 fn recursively_set_focus_status(element: &Element, new_state: bool) {
196 element.set_focus_state(new_state);
197
198 let Some(shadow_root) = element.containing_shadow_root() else {
199 return;
200 };
201 recursively_set_focus_status(&shadow_root.Host(), new_state);
202 }
203
204 if let Some(previously_focused_element) = self.focused_area.borrow().element() {
205 recursively_set_focus_status(previously_focused_element, false);
206 }
207 if let Some(newly_focused_element) = new_focusable_area.element() {
208 recursively_set_focus_status(newly_focused_element, true);
209 }
210
211 *self.focused_area.borrow_mut() = new_focusable_area;
212 }
213
214 /// Get the last sequence number sent to the constellation.
215 ///
216 /// Received focus-related messages with sequence numbers less than the one
217 /// returned by this method must be discarded.
218 pub fn focus_sequence(&self) -> FocusSequenceNumber {
219 self.focus_sequence.get()
220 }
221
222 /// Generate the next sequence number for focus-related messages.
223 fn increment_fetch_focus_sequence(&self) -> FocusSequenceNumber {
224 self.focus_sequence.set(FocusSequenceNumber(
225 self.focus_sequence
226 .get()
227 .0
228 .checked_add(1)
229 .expect("too many focus messages have been sent"),
230 ));
231 self.focus_sequence.get()
232 }
233
234 /// <https://html.spec.whatwg.org/multipage/#current-focus-chain-of-a-top-level-traversable>
235 pub(crate) fn current_focus_chain(&self) -> Vec<FocusableArea> {
236 // > The current focus chain of a top-level traversable is the focus chain of the
237 // > currently focused area of traversable, if traversable is non-null, or an empty list
238 // > otherwise.
239
240 // We cannot easily get the full focus chain of the top-level traversable, so we just
241 // get the bits that intersect with this `Document`. The rest will be handled
242 // internally in [`Self::focus_update_steps`].
243 if !self.has_focus() {
244 return vec![];
245 }
246 self.focused_area().focus_chain()
247 }
248
249 /// Reassign the focus context to the element that last requested focus during this
250 /// transaction, or the document if no elements requested it.
251 pub(crate) fn focus(&self, cx: &mut JSContext, new_focus_target: FocusableArea) {
252 let old_focus_chain = self.current_focus_chain();
253 let new_focus_chain = new_focus_target.focus_chain();
254 self.focus_update_steps(cx, new_focus_chain, old_focus_chain, &new_focus_target);
255
256 // Advertise the change in the focus chain.
257 // <https://html.spec.whatwg.org/multipage/#focus-chain>
258 // <https://html.spec.whatwg.org/multipage/#focusing-steps>
259 //
260 // TODO: Integrate this into the "focus update steps."
261 //
262 // If the top-level BC doesn't have system focus, this won't
263 // have an immediate effect, but it will when we gain system
264 // focus again. Therefore we still have to send `ScriptMsg::
265 // Focus`.
266 //
267 // When a container with a non-null nested browsing context is
268 // focused, its active document becomes the focused area of the
269 // top-level browsing context instead. Therefore we need to let
270 // the constellation know if such a container is focused.
271 //
272 // > The focusing steps for an object `new focus target` [...]
273 // >
274 // > 3. If `new focus target` is a browsing context container
275 // > with non-null nested browsing context, then set
276 // > `new focus target` to the nested browsing context's
277 // > active document.
278 let child_browsing_context_id = match new_focus_target {
279 FocusableArea::IFrameViewport { iframe_element, .. } => {
280 iframe_element.browsing_context_id()
281 },
282 _ => None,
283 };
284 let sequence = self.increment_fetch_focus_sequence();
285
286 debug!(
287 "Advertising the focus request to the constellation \
288 with sequence number {sequence:?} and child \
289 {child_browsing_context_id:?}",
290 );
291 self.window.send_to_constellation(
292 ScriptToConstellationMessage::FocusAncestorBrowsingContextsForFocusingSteps(
293 child_browsing_context_id,
294 sequence,
295 ),
296 );
297 }
298
299 /// <https://html.spec.whatwg.org/multipage/#focus-update-steps>
300 pub(crate) fn focus_update_steps(
301 &self,
302 cx: &mut JSContext,
303 mut new_focus_chain: Vec<FocusableArea>,
304 mut old_focus_chain: Vec<FocusableArea>,
305 new_focus_target: &FocusableArea,
306 ) {
307 let new_focus_chain_was_empty = new_focus_chain.is_empty();
308
309 // Step 1: If the last entry in old chain and the last entry in new chain are the same,
310 // pop the last entry from old chain and the last entry from new chain and redo this
311 // step.
312 //
313 // We avoid recursion here.
314 while let (Some(last_new), Some(last_old)) =
315 (new_focus_chain.last(), old_focus_chain.last())
316 {
317 if last_new == last_old {
318 new_focus_chain.pop();
319 old_focus_chain.pop();
320 } else {
321 break;
322 }
323 }
324
325 // If the two focus chains are both empty, focus hasn't changed. This isn't in the
326 // specification, but we must do it because we set the focused area to the viewport
327 // before blurring. If no focus changes, that would mean the currently focused element
328 // loses focus.
329 if old_focus_chain.is_empty() && new_focus_chain.is_empty() {
330 return;
331 }
332 // Although the "focusing steps" in the HTML specification say to wait until after firing
333 // the "blur" event to change the currently focused area of the Document, browsers tend
334 // to set it to the viewport before firing the "blur" event.
335 //
336 // See https://github.com/whatwg/html/issues/1569
337 self.set_focused_area(FocusableArea::Viewport);
338
339 // Step 2: For each entry entry in old chain, in order, run these substeps:
340 // Note: `old_focus_chain` might be empty!
341 let last_old_focus_chain_entry = old_focus_chain.len().saturating_sub(1);
342 for (index, entry) in old_focus_chain.iter().enumerate() {
343 // Step 2.1: If entry is an input element, and the change event applies to the element,
344 // and the element does not have a defined activation behavior, and the user has
345 // changed the element's value or its list of selected files while the control was
346 // focused without committing that change (such that it is different to what it was
347 // when the control was first focused), then:
348 // Step 2.1.1: Set entry's user validity to true.
349 // Step 2.1.2: Fire an event named change at the element, with the bubbles attribute initialized to true.
350 // TODO: Implement this.
351
352 // Step 2.2:
353 // - If entry is an element, let blur event target be entry.
354 // - If entry is a Document object, let blur event target be that Document object's
355 // relevant global object.
356 // - Otherwise, let blur event target be null.
357 //
358 // Note: We always send focus and blur events for `<iframe>` elements, but other
359 // browsers only seem to do that conditionally. This needs a bit more research.
360 let blur_event_target = match entry {
361 FocusableArea::Node { node, .. } => Some(node.upcast::<EventTarget>()),
362 FocusableArea::IFrameViewport { iframe_element, .. } => {
363 Some(iframe_element.upcast())
364 },
365 FocusableArea::Viewport => Some(self.window.upcast::<EventTarget>()),
366 };
367
368 // Step 2.3: If entry is the last entry in old chain, and entry is an Element, and
369 // the last entry in new chain is also an Element, then let related blur target be
370 // the last entry in new chain. Otherwise, let related blur target be null.
371 //
372 // Note: This can only happen when the focused `Document` doesn't change and we are
373 // moving focus from one element to another. These elements are the last in the chain
374 // because of the popping we do at the start of these steps.
375 let related_blur_target = match new_focus_chain.last() {
376 Some(FocusableArea::Node { node, .. })
377 if index == last_old_focus_chain_entry &&
378 matches!(entry, FocusableArea::Node { .. }) =>
379 {
380 Some(node.upcast())
381 },
382 _ => None,
383 };
384
385 // Step 2.4: If blur event target is not null, fire a focus event named blur at
386 // blur event target, with related blur target as the related target.
387 if let Some(blur_event_target) = blur_event_target {
388 self.fire_focus_event(
389 cx,
390 FocusEventType::Blur,
391 blur_event_target,
392 related_blur_target,
393 );
394 }
395 }
396
397 // Step 3: Apply any relevant platform-specific conventions for focusing new focus
398 // target. (For example, some platforms select the contents of a text control when that
399 // control is focused.)
400 if &*self.focused_area() != new_focus_target {
401 if let Some(html_element) = new_focus_target
402 .element()
403 .and_then(|element| element.downcast::<HTMLElement>())
404 {
405 html_element.handle_focus_state_for_contenteditable(cx);
406 }
407 }
408
409 self.set_has_focus(!new_focus_chain_was_empty);
410
411 // Step 4: For each entry entry in new chain, in reverse order, run these substeps:
412 // Note: `new_focus_chain` might be empty!
413 let last_new_focus_chain_entry = new_focus_chain.len().saturating_sub(1); // Might be empty, so calculated here.
414 for (index, entry) in new_focus_chain.iter().enumerate().rev() {
415 // Step 4.1: If entry is a focusable area, and the focused area of the document is
416 // not entry:
417 //
418 // Here we deviate from the specification a bit, as all focus chain elements are
419 // focusable areas currently. We just assume that it means the first entry of the
420 // chain, which is the new focus target
421 if index == 0 {
422 // Step 4.1.1: Set document's relevant global object's navigation API's focus
423 // changed during ongoing navigation to true.
424 // TODO: Implement this.
425
426 // Step 4.1.2: Designate entry as the focused area of the document.
427 self.set_focused_area(entry.clone());
428 }
429
430 // Step 4.2:
431 // - If entry is an element, let focus event target be entry.
432 // - If entry is a Document object, let focus event target be that Document
433 // object's relevant global object.
434 // - Otherwise, let focus event target be null.
435 //
436 // Note: We always send focus and blur events for `<iframe>` elements, but other
437 // browsers only seem to do that conditionally. This needs a bit more research.
438 let focus_event_target = match entry {
439 FocusableArea::Node { node, .. } => Some(node.upcast::<EventTarget>()),
440 FocusableArea::IFrameViewport { iframe_element, .. } => {
441 Some(iframe_element.upcast())
442 },
443 FocusableArea::Viewport => Some(self.window.upcast::<EventTarget>()),
444 };
445
446 // Step 4.3: If entry is the last entry in new chain, and entry is an Element, and
447 // the last entry in old chain is also an Element, then let related focus target be
448 // the last entry in old chain. Otherwise, let related focus target be null.
449 //
450 // Note: This can only happen when the focused `Document` doesn't change and we are
451 // moving focus from one element to another. These elements are the last in the chain
452 // because of the popping we do at the start of these steps.
453 let related_focus_target = match old_focus_chain.last() {
454 Some(FocusableArea::Node { node, .. })
455 if index == last_new_focus_chain_entry &&
456 matches!(entry, FocusableArea::Node { .. }) =>
457 {
458 Some(node.upcast())
459 },
460 _ => None,
461 };
462
463 // Step 4.4: If focus event target is not null, fire a focus event named focus at
464 // focus event target, with related focus target as the related target.
465 if let Some(focus_event_target) = focus_event_target {
466 self.fire_focus_event(
467 cx,
468 FocusEventType::Focus,
469 focus_event_target,
470 related_focus_target,
471 );
472 }
473 }
474 }
475
476 /// <https://html.spec.whatwg.org/multipage/#fire-a-focus-event>
477 pub(crate) fn fire_focus_event(
478 &self,
479 cx: &mut JSContext,
480 focus_event_type: FocusEventType,
481 event_target: &EventTarget,
482 related_target: Option<&EventTarget>,
483 ) {
484 let event_name = match focus_event_type {
485 FocusEventType::Focus => "focus".into(),
486 FocusEventType::Blur => "blur".into(),
487 };
488
489 let event = FocusEvent::new(
490 &self.window,
491 event_name,
492 EventBubbles::DoesNotBubble,
493 EventCancelable::NotCancelable,
494 Some(&self.window),
495 0i32,
496 related_target,
497 CanGc::from_cx(cx),
498 );
499 let event = event.upcast::<Event>();
500 event.set_trusted(true);
501 event.fire(event_target, CanGc::from_cx(cx));
502 }
503
504 /// <https://html.spec.whatwg.org/multipage/#focus-fixup-rule>
505 /// > For each doc of docs, if the focused area of doc is not a focusable area, then run the
506 /// > focusing steps for doc's viewport, and set doc's relevant global object's navigation API's
507 /// > focus changed during ongoing navigation to false.
508 ///
509 /// TODO: Handle the "focus changed during ongoing navigation" flag.
510 pub(crate) fn perform_focus_fixup_rule(&self, cx: &mut JSContext) {
511 if self
512 .focused_area
513 .borrow()
514 .element()
515 .is_none_or(|focused| focused.is_focusable_area())
516 {
517 return;
518 }
519 self.focus(cx, FocusableArea::Viewport);
520 }
521
522 pub(crate) fn sequentially_focus_child_iframe_local_or_remote(
523 &self,
524 cx: &mut JSContext,
525 iframe_element: &HTMLIFrameElement,
526 direction: SequentialFocusDirection,
527 ) {
528 if let Some(content_document) = iframe_element.GetContentDocument() {
529 // The <iframe> is in the same `ScriptThread` and we have direct access to it. We can
530 // move the focus directly.
531 content_document
532 .focus_handler()
533 .sequential_focus_from_another_document(cx, None, direction);
534 } else if let Some(browsing_context_id) = iframe_element.browsing_context_id() {
535 self.window.send_to_constellation(
536 ScriptToConstellationMessage::FocusRemoteBrowsingContext(
537 browsing_context_id,
538 RemoteFocusOperation::Sequential(direction, None),
539 ),
540 );
541 } else {
542 iframe_element
543 .upcast::<Node>()
544 .run_the_focusing_steps(cx, None);
545 }
546 }
547
548 pub(crate) fn sequentially_focus_parent_local_or_remote(
549 &self,
550 cx: &mut JSContext,
551 direction: SequentialFocusDirection,
552 ) {
553 let window_proxy = self.window.window_proxy();
554 if let Some(iframe) = window_proxy.frame_element() {
555 // The parent browsing context is in the same `ScriptThread` and we have direct access
556 // to it. We can move the focus directly.
557 let browsing_context_id = iframe
558 .downcast::<HTMLIFrameElement>()
559 .and_then(|iframe_element| iframe_element.browsing_context_id());
560 iframe
561 .owner_document()
562 .focus_handler()
563 .sequential_focus_from_another_document(cx, browsing_context_id, direction);
564 } else if let Some(browsing_context_id) = window_proxy
565 .parent()
566 .map(|parent| parent.browsing_context_id())
567 {
568 self.window.send_to_constellation(
569 ScriptToConstellationMessage::FocusRemoteBrowsingContext(
570 browsing_context_id,
571 RemoteFocusOperation::Sequential(
572 direction,
573 Some(window_proxy.browsing_context_id()),
574 ),
575 ),
576 );
577 }
578 }
579
580 pub(crate) fn sequential_focus_from_another_document(
581 &self,
582 cx: &mut JSContext,
583 browsing_context_id: Option<BrowsingContextId>,
584 direction: SequentialFocusDirection,
585 ) {
586 let _realm = enter_realm(&*self.window);
587 let starting_point = browsing_context_id.and_then(|browsing_context_id| {
588 self.window
589 .Document()
590 .iframes()
591 .get(browsing_context_id)
592 .map(|iframe| DomRoot::from_ref(iframe.element.upcast::<Node>()))
593 });
594 self.window
595 .Document()
596 .event_handler()
597 .sequential_focus_navigation_loop(
598 cx,
599 starting_point,
600 direction,
601 true, /* allow focusing viewport */
602 );
603 }
604}