mirror of
https://github.com/servo/servo
synced 2026-05-12 01:46:28 +02:00
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>
153 lines
7.5 KiB
Rust
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()
|
|
}
|
|
}
|