script/dom/element/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 script_bindings::codegen::GenericBindings::ElementBinding::ElementMethods;
6use script_bindings::codegen::GenericBindings::ShadowRootBinding::ShadowRootMethods;
7use script_bindings::codegen::InheritTypes::{ElementTypeId, HTMLElementTypeId, NodeTypeId};
8use script_bindings::inheritance::Castable;
9use style::computed_values::visibility::T as Visibility;
10use style::values::computed::Overflow;
11use xml5ever::local_name;
12
13use crate::dom::Node;
14use crate::dom::document::focus::FocusableAreaKind;
15use crate::dom::types::{Element, HTMLElement};
16
17impl Element {
18 /// <https://html.spec.whatwg.org/multipage/#focusable-area>
19 ///
20 /// The list of focusable areas at this point in the specification is both incomplete and leaves
21 /// a lot up to the user agent. In addition, the specifications for "click focusable" and
22 /// "sequentially focusable" are written in a way that they are subsets of all focusable areas.
23 /// In order to avoid having to first determine whether an element is a focusable area and then
24 /// work backwards to figure out what kind it is, this function attempts to classify the
25 /// different types of focusable areas ahead of time so that the logic is useful for answering
26 /// both "Is this element a focusable area?" and "Is this element click (or sequentially)
27 /// focusable."
28 pub(crate) fn focusable_area_kind(&self) -> FocusableAreaKind {
29 // Do not allow unrendered, disconnected, or disabled nodes to be focusable areas ever.
30 let node: &Node = self.upcast();
31 if !node.is_connected() || !self.has_css_layout_box() || self.is_actually_disabled() {
32 return Default::default();
33 }
34
35 // <https://www.w3.org/TR/css-display-4/#visibility>
36 // Invisible elements are removed from navigation.
37 if self
38 .style()
39 .is_some_and(|style| style.get_inherited_box().visibility != Visibility::Visible)
40 {
41 return Default::default();
42 }
43
44 // An element with a shadow root that delegates focus should never itself be a focusable area.
45 if self
46 .shadow_root()
47 .is_some_and(|shadow_root| shadow_root.DelegatesFocus())
48 {
49 return Default::default();
50 }
51
52 // > Elements that meet all the following criteria:
53 // > the element's tabindex value is non-null, or the element is determined by the user agent to be focusable;
54 // > the element is either not a shadow host, or has a shadow root whose delegates focus is false;
55 // Note: Checked above
56 // > the element is not actually disabled;
57 // Note: Checked above
58 // > the element is not inert;
59 // TODO: Handle this.
60 // > the element is either being rendered, delegating its rendering to its children, or
61 // > being used as relevant canvas fallback content.
62 // Note: Checked above
63 // TODO: Handle fallback canvas content.
64 match self.explicitly_set_tab_index() {
65 // From <https://html.spec.whatwg.org/multipage/#tabindex-ordered-focus-navigation-scope>:
66 // > A tabindex-ordered focus navigation scope is a list of focusable areas and focus
67 // > navigation scope owners. Every focus navigation scope owner owner has tabindex-ordered
68 // > focus navigation scope, whose contents are determined as follows:
69 // > - It contains all elements in owner's focus navigation scope that are themselves focus
70 // > navigation scope owners, except the elements whose tabindex value is a negative integer.
71 // > - It contains all of the focusable areas whose DOM anchor is an element in owner's focus
72 // > navigation scope, except the focusable areas whose tabindex value is a negative integer.
73 Some(tab_index) if tab_index < 0 => return FocusableAreaKind::Click,
74 Some(_) => return FocusableAreaKind::Click | FocusableAreaKind::Sequential,
75 None => {},
76 }
77
78 // From <https://html.spec.whatwg.org/multipage/#tabindex-value>
79 // > If the value is null
80 // > ...
81 // > Modulo platform conventions, it is suggested that the following elements should be
82 // > considered as focusable areas and be sequentially focusable:
83 let is_focusable_area_due_to_type = match node.type_id() {
84 // > - a elements that have an href attribute
85 NodeTypeId::Element(ElementTypeId::HTMLElement(
86 HTMLElementTypeId::HTMLAnchorElement,
87 )) => self.has_attribute(&local_name!("href")),
88
89 // > - input elements whose type attribute are not in the Hidden state
90 // > - button elements
91 // > - select elements
92 // > - textarea elements
93 // > - Navigable containers
94 //
95 // Note: the `hidden` attribute is checked above for all elements.
96 NodeTypeId::Element(ElementTypeId::HTMLElement(
97 HTMLElementTypeId::HTMLInputElement |
98 HTMLElementTypeId::HTMLButtonElement |
99 HTMLElementTypeId::HTMLSelectElement |
100 HTMLElementTypeId::HTMLTextAreaElement |
101 HTMLElementTypeId::HTMLIFrameElement,
102 )) => true,
103 _ => {
104 // > - summary elements that are the first summary element child of a details element
105 // > - Editing hosts
106 // > - Elements with a draggable attribute set, if that would enable the user agent to allow
107 // > the user to begin drag operations for those elements without the use of a pointing device
108 self.downcast::<HTMLElement>()
109 .is_some_and(|html_element| html_element.is_a_summary_for_its_parent_details()) ||
110 self.is_editing_host() ||
111 self.get_string_attribute(&local_name!("draggable")) == "true"
112 },
113 };
114
115 if is_focusable_area_due_to_type {
116 return FocusableAreaKind::Click | FocusableAreaKind::Sequential;
117 }
118
119 // > The scrollable regions of elements that are being rendered and are not inert.
120 //
121 // Note that these kind of focusable areas are only focusable via the keyboard.
122 //
123 // TODO: Handle inert.
124 if self
125 .upcast::<Node>()
126 .effective_overflow()
127 .is_some_and(|axes_overflow| {
128 // This is checking whether there is an input event scrollable overflow value in
129 // a given axis and also overflow in that same axis.
130 (matches!(axes_overflow.x, Overflow::Auto | Overflow::Scroll) &&
131 self.ScrollWidth() > self.ClientWidth()) ||
132 (matches!(axes_overflow.y, Overflow::Auto | Overflow::Scroll) &&
133 self.ScrollHeight() > self.ClientHeight())
134 })
135 {
136 return FocusableAreaKind::Sequential;
137 }
138
139 Default::default()
140 }
141
142 /// <https://html.spec.whatwg.org/multipage/#sequentially-focusable>.
143 pub(crate) fn is_sequentially_focusable(&self) -> bool {
144 self.focusable_area_kind()
145 .contains(FocusableAreaKind::Sequential)
146 }
147
148 /// <https://html.spec.whatwg.org/multipage/#focusable-area>
149 pub(crate) fn is_focusable_area(&self) -> bool {
150 !self.focusable_area_kind().is_empty()
151 }
152}