Skip to main content

script/dom/document/
interactive_element_command.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::HTMLButtonElementBinding::HTMLButtonElementMethods;
7use script_bindings::codegen::GenericBindings::HTMLElementBinding::HTMLElementMethods;
8use script_bindings::codegen::GenericBindings::HTMLInputElementBinding::HTMLInputElementMethods;
9use script_bindings::codegen::GenericBindings::HTMLOptionElementBinding::HTMLOptionElementMethods;
10use script_bindings::codegen::GenericBindings::HTMLSelectElementBinding::HTMLSelectElementMethods;
11use script_bindings::codegen::GenericBindings::NodeBinding::NodeMethods;
12use script_bindings::inheritance::Castable;
13use script_bindings::root::DomRoot;
14
15use crate::dom::iterators::ShadowIncluding;
16use crate::dom::node::focus::FocusTrigger;
17use crate::dom::node::{Node, NodeTraits};
18use crate::dom::types::{
19    HTMLAnchorElement, HTMLButtonElement, HTMLElement, HTMLFieldSetElement, HTMLInputElement,
20    HTMLLabelElement, HTMLLegendElement, HTMLOptionElement,
21};
22
23/// This is an implementation of <https://html.spec.whatwg.org/multipage/#concept-command>. Note
24/// that there are various things called "commands" on the web platform, but this is the one that
25/// is mainly associated with access keys.
26pub(crate) enum InteractiveElementCommand {
27    Anchor(DomRoot<HTMLAnchorElement>),
28    Button(DomRoot<HTMLButtonElement>),
29    Input(DomRoot<HTMLInputElement>),
30    Option(DomRoot<HTMLOptionElement>),
31    HTMLElement(DomRoot<HTMLElement>),
32}
33
34impl TryFrom<&HTMLLegendElement> for InteractiveElementCommand {
35    type Error = ();
36
37    /// From <https://html.spec.whatwg.org/multipage/#using-the-accesskey-attribute-on-a-legend-element-to-define-a-command>
38    /// A legend element defines a command if all of the following are true:
39    ///  - It has an assigned access key.
40    ///  - It is a child of a fieldset element.
41    ///  - Its parent has a descendant that defines a command that is neither a label element nor
42    ///    a legend element. This element, if it exists, is the legend element's accesskey
43    ///    delegatee.
44    fn try_from(legend_element: &HTMLLegendElement) -> Result<Self, Self::Error> {
45        if !legend_element
46            .owner_document()
47            .event_handler()
48            .has_assigned_access_key(legend_element.upcast())
49        {
50            return Err(());
51        }
52
53        let node = legend_element.upcast::<Node>();
54        let Some(parent) = node.GetParentElement() else {
55            return Err(());
56        };
57        if !parent.is::<HTMLFieldSetElement>() {
58            return Err(());
59        }
60        for node in parent
61            .upcast::<Node>()
62            .traverse_preorder(ShadowIncluding::No)
63        {
64            if node.is::<HTMLLabelElement>() || node.is::<HTMLLegendElement>() {
65                continue;
66            }
67            let Some(html_element) = node.downcast::<HTMLElement>() else {
68                continue;
69            };
70            if let Ok(command) = Self::try_from(html_element) {
71                return Ok(command);
72            }
73        }
74
75        Err(())
76    }
77}
78
79impl TryFrom<&HTMLElement> for InteractiveElementCommand {
80    type Error = ();
81
82    fn try_from(html_element: &HTMLElement) -> Result<Self, Self::Error> {
83        if let Some(anchor_element) = html_element.downcast::<HTMLAnchorElement>() {
84            return Ok(Self::Anchor(DomRoot::from_ref(anchor_element)));
85        }
86        if let Some(button_element) = html_element.downcast::<HTMLButtonElement>() {
87            return Ok(Self::Button(DomRoot::from_ref(button_element)));
88        }
89        if let Some(input_element) = html_element.downcast::<HTMLInputElement>() {
90            return Ok(Self::Input(DomRoot::from_ref(input_element)));
91        }
92        if let Some(option_element) = html_element.downcast::<HTMLOptionElement>() {
93            return Ok(Self::Option(DomRoot::from_ref(option_element)));
94        }
95        if let Some(legend_element) = html_element.downcast::<HTMLLegendElement>() {
96            return Self::try_from(legend_element);
97        }
98        if html_element
99            .owner_document()
100            .event_handler()
101            .has_assigned_access_key(html_element)
102        {
103            return Ok(Self::HTMLElement(DomRoot::from_ref(html_element)));
104        }
105
106        Err(())
107    }
108}
109
110impl InteractiveElementCommand {
111    fn is_node_or_ancestor_inert(node: &Node) -> bool {
112        node.inclusive_ancestors(ShadowIncluding::No)
113            .any(|node| node.is_inert())
114    }
115
116    pub(crate) fn disabled(&self) -> bool {
117        match self {
118            // <https://html.spec.whatwg.org/multipage#using-the-a-element-to-define-a-command>
119            // > The Disabled State facet of the command is true if the element or one of its
120            // > ancestors is inert, and false otherwise.
121            InteractiveElementCommand::Anchor(anchor) => {
122                Self::is_node_or_ancestor_inert(anchor.upcast::<Node>())
123            },
124            // <https://html.spec.whatwg.org/multipage/#using-the-button-element-to-define-a-command>
125            // > The Disabled State of the command is true if the element or one of its ancestors
126            // > is inert, or if the element's disabled state is set, and false otherwise.
127            InteractiveElementCommand::Button(button) => {
128                Self::is_node_or_ancestor_inert(button.upcast::<Node>()) || button.Disabled()
129            },
130            // <https://html.spec.whatwg.org/multipage/#using-the-input-element-to-define-a-command>
131            // > The Disabled State of the command is true if the element or one of its ancestors is
132            // > inert, or if the element's disabled state is set, and false otherwise.
133            InteractiveElementCommand::Input(input) => {
134                Self::is_node_or_ancestor_inert(input.upcast::<Node>()) || input.Disabled()
135            },
136            // <https://html.spec.whatwg.org/multipage/#using-the-option-element-to-define-a-command>
137            // > The Disabled State of the command is true if the element is disabled, or if its
138            // > nearest ancestor select element is disabled, or if it or one of its ancestors is
139            // > inert, and false otherwise.
140            InteractiveElementCommand::Option(option) => {
141                option.Disabled() ||
142                    option
143                        .nearest_ancestor_select()
144                        .is_some_and(|select| select.Disabled()) ||
145                    Self::is_node_or_ancestor_inert(option.upcast::<Node>())
146            },
147            // <https://html.spec.whatwg.org/multipage#using-the-accesskey-attribute-to-define-a-command-on-other-elements>
148            // > The Disabled State of the command is true if the element or one of its ancestors is
149            // > inert, and false otherwise.
150            InteractiveElementCommand::HTMLElement(element) => {
151                Self::is_node_or_ancestor_inert(element.upcast::<Node>())
152            },
153        }
154    }
155
156    pub(crate) fn hidden(&self) -> bool {
157        let html_element: &HTMLElement = match self {
158            InteractiveElementCommand::Anchor(anchor_element) => anchor_element.upcast(),
159            InteractiveElementCommand::Button(button_element) => button_element.upcast(),
160            InteractiveElementCommand::Input(input_element) => input_element.upcast(),
161            InteractiveElementCommand::Option(option_element) => option_element.upcast(),
162            InteractiveElementCommand::HTMLElement(html_element) => html_element,
163        };
164        html_element.Hidden()
165    }
166
167    pub(crate) fn perform_action(&self, cx: &mut JSContext) {
168        match self {
169            // <https://html.spec.whatwg.org/multipage#using-the-a-element-to-define-a-command>
170            // > The Action of the command is to fire a click event at the element.
171            // <https://html.spec.whatwg.org/multipage/#fire-a-click-event>
172            // > Firing a click event at target means firing a synthetic pointer event named click at target.
173            InteractiveElementCommand::Anchor(anchor_element) => anchor_element
174                .upcast::<Node>()
175                .fire_synthetic_pointer_event_not_trusted(cx, atom!("click")),
176            // <https://html.spec.whatwg.org/multipage/#using-the-button-element-to-define-a-command>
177            // > The Label, Access Key, Hidden State, and Action facets of the command are
178            // > determined as for a elements (see the previous section).
179            InteractiveElementCommand::Button(button_element) => button_element
180                .upcast::<Node>()
181                .fire_synthetic_pointer_event_not_trusted(cx, atom!("click")),
182            // <https://html.spec.whatwg.org/multipage/#using-the-input-element-to-define-a-command>
183            // > The Action of the command is to fire a click event at the element.
184            InteractiveElementCommand::Input(input_element) => input_element
185                .upcast::<Node>()
186                .fire_synthetic_pointer_event_not_trusted(cx, atom!("click")),
187            // <https://html.spec.whatwg.org/multipage/#using-the-option-element-to-define-a-command>
188            // > If the option's nearest ancestor select element has a multiple attribute, the
189            // > Action of the command is to toggle the option element. Otherwise, the Action is to
190            // > pick the option element.
191            // Note: setSelected takes care of whether or not the owner has the `multiple` attribute.
192            InteractiveElementCommand::Option(option_element) => {
193                option_element.SetSelected(cx, true)
194            },
195            // > The Action of the command is to run the following steps:
196            // >  1. Run the focusing steps for the element.
197            // >  2. Fire a click event at the element.
198            InteractiveElementCommand::HTMLElement(html_element) => {
199                let node: &Node = html_element.upcast();
200                node.run_the_focusing_steps(cx, None, FocusTrigger::Other);
201                node.fire_synthetic_pointer_event_not_trusted(cx, atom!("click"));
202            },
203        }
204    }
205}