Skip to main content

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