Files
servo/components/script/dom/element/focus.rs
Martin Robinson ceac966c34 script: Remove focus transaction concept (#43834)
For years Servo has had the concept of a focus transaction which was
used only to allow falling back to focusing the viewport when focusing a
clicked element failed. As this concept isn't part of the specification,
this change removes it.

Instead, a `FocusableArea` (a specification concept) is passed to
the `Document` focusing code. A `FocusableArea` might also be the
`Document`'s viewport.

As part of this change, some focus-related methods are moved to `Node`
from `Element` as the `Document` is not an `Element`.  This brings the
code closer to implementing the "focusing steps" from the specification.

Testing: This should not change behavior and is thus covered by existing
tests.

Signed-off-by: Martin Robinson <mrobinson@igalia.com>
2026-04-01 15:12:21 +00:00

153 lines
7.5 KiB
Rust

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use script_bindings::codegen::GenericBindings::ElementBinding::ElementMethods;
use script_bindings::codegen::GenericBindings::ShadowRootBinding::ShadowRootMethods;
use script_bindings::codegen::InheritTypes::{ElementTypeId, HTMLElementTypeId, NodeTypeId};
use script_bindings::inheritance::Castable;
use style::computed_values::visibility::T as Visibility;
use style::values::computed::Overflow;
use xml5ever::local_name;
use crate::dom::Node;
use crate::dom::document::focus::FocusableAreaKind;
use crate::dom::types::{Element, HTMLElement};
impl Element {
/// <https://html.spec.whatwg.org/multipage/#focusable-area>
///
/// The list of focusable areas at this point in the specification is both incomplete and leaves
/// a lot up to the user agent. In addition, the specifications for "click focusable" and
/// "sequentially focusable" are written in a way that they are subsets of all focusable areas.
/// In order to avoid having to first determine whether an element is a focusable area and then
/// work backwards to figure out what kind it is, this function attempts to classify the
/// different types of focusable areas ahead of time so that the logic is useful for answering
/// both "Is this element a focusable area?" and "Is this element click (or sequentially)
/// focusable."
pub(crate) fn focusable_area_kind(&self) -> FocusableAreaKind {
// Do not allow unrendered, disconnected, or disabled nodes to be focusable areas ever.
let node: &Node = self.upcast();
if !node.is_connected() || !self.has_css_layout_box() || self.is_actually_disabled() {
return Default::default();
}
// <https://www.w3.org/TR/css-display-4/#visibility>
// Invisible elements are removed from navigation.
if self
.style()
.is_some_and(|style| style.get_inherited_box().visibility != Visibility::Visible)
{
return Default::default();
}
// An element with a shadow root that delegates focus should never itself be a focusable area.
if self
.shadow_root()
.is_some_and(|shadow_root| shadow_root.DelegatesFocus())
{
return Default::default();
}
// > Elements that meet all the following criteria:
// > the element's tabindex value is non-null, or the element is determined by the user agent to be focusable;
// > the element is either not a shadow host, or has a shadow root whose delegates focus is false;
// Note: Checked above
// > the element is not actually disabled;
// Note: Checked above
// > the element is not inert;
// TODO: Handle this.
// > the element is either being rendered, delegating its rendering to its children, or
// > being used as relevant canvas fallback content.
// Note: Checked above
// TODO: Handle fallback canvas content.
match self.explicitly_set_tab_index() {
// From <https://html.spec.whatwg.org/multipage/#tabindex-ordered-focus-navigation-scope>:
// > A tabindex-ordered focus navigation scope is a list of focusable areas and focus
// > navigation scope owners. Every focus navigation scope owner owner has tabindex-ordered
// > focus navigation scope, whose contents are determined as follows:
// > - It contains all elements in owner's focus navigation scope that are themselves focus
// > navigation scope owners, except the elements whose tabindex value is a negative integer.
// > - It contains all of the focusable areas whose DOM anchor is an element in owner's focus
// > navigation scope, except the focusable areas whose tabindex value is a negative integer.
Some(tab_index) if tab_index < 0 => return FocusableAreaKind::Click,
Some(_) => return FocusableAreaKind::Click | FocusableAreaKind::Sequential,
None => {},
}
// From <https://html.spec.whatwg.org/multipage/#tabindex-value>
// > If the value is null
// > ...
// > Modulo platform conventions, it is suggested that the following elements should be
// > considered as focusable areas and be sequentially focusable:
let is_focusable_area_due_to_type = match node.type_id() {
// > - a elements that have an href attribute
NodeTypeId::Element(ElementTypeId::HTMLElement(
HTMLElementTypeId::HTMLAnchorElement,
)) => self.has_attribute(&local_name!("href")),
// > - input elements whose type attribute are not in the Hidden state
// > - button elements
// > - select elements
// > - textarea elements
// > - Navigable containers
//
// Note: the `hidden` attribute is checked above for all elements.
NodeTypeId::Element(ElementTypeId::HTMLElement(
HTMLElementTypeId::HTMLInputElement |
HTMLElementTypeId::HTMLButtonElement |
HTMLElementTypeId::HTMLSelectElement |
HTMLElementTypeId::HTMLTextAreaElement |
HTMLElementTypeId::HTMLIFrameElement,
)) => true,
_ => {
// > - summary elements that are the first summary element child of a details element
// > - Editing hosts
// > - Elements with a draggable attribute set, if that would enable the user agent to allow
// > the user to begin drag operations for those elements without the use of a pointing device
self.downcast::<HTMLElement>()
.is_some_and(|html_element| html_element.is_a_summary_for_its_parent_details()) ||
self.is_editing_host() ||
self.get_string_attribute(&local_name!("draggable")) == "true"
},
};
if is_focusable_area_due_to_type {
return FocusableAreaKind::Click | FocusableAreaKind::Sequential;
}
// > The scrollable regions of elements that are being rendered and are not inert.
//
// Note that these kind of focusable areas are only focusable via the keyboard.
//
// TODO: Handle inert.
if self
.upcast::<Node>()
.effective_overflow()
.is_some_and(|axes_overflow| {
// This is checking whether there is an input event scrollable overflow value in
// a given axis and also overflow in that same axis.
(matches!(axes_overflow.x, Overflow::Auto | Overflow::Scroll) &&
self.ScrollWidth() > self.ClientWidth()) ||
(matches!(axes_overflow.y, Overflow::Auto | Overflow::Scroll) &&
self.ScrollHeight() > self.ClientHeight())
})
{
return FocusableAreaKind::Sequential;
}
Default::default()
}
/// <https://html.spec.whatwg.org/multipage/#sequentially-focusable>.
pub(crate) fn is_sequentially_focusable(&self) -> bool {
self.focusable_area_kind()
.contains(FocusableAreaKind::Sequential)
}
/// <https://html.spec.whatwg.org/multipage/#focusable-area>
pub(crate) fn is_focusable_area(&self) -> bool {
!self.focusable_area_kind().is_empty()
}
}