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    pub(crate) fn disabled(&self) -> bool {
112        match self {
113            // <https://html.spec.whatwg.org/multipage#using-the-a-element-to-define-a-command>
114            // > The Disabled State facet of the command is true if the element or one of its
115            // > ancestors is inert, and false otherwise.
116            // TODO: We do not support `inert` yet.
117            InteractiveElementCommand::Anchor(..) => false,
118            // <https://html.spec.whatwg.org/multipage/#using-the-button-element-to-define-a-command>
119            // > The Disabled State of the command is true if the element or one of its ancestors
120            // > is inert, or if the element's disabled state is set, and false otherwise.
121            // TODO: We do not support `inert` yet.
122            InteractiveElementCommand::Button(button) => button.Disabled(),
123            // <https://html.spec.whatwg.org/multipage/#using-the-input-element-to-define-a-command>
124            // > The Disabled State of the command is true if the element or one of its ancestors is
125            // > inert, or if the element's disabled state is set, and false otherwise.
126            // TODO: We do not support `inert` yet.
127            InteractiveElementCommand::Input(input) => input.Disabled(),
128            // <https://html.spec.whatwg.org/multipage/#using-the-option-element-to-define-a-command>
129            // > The Disabled State of the command is true if the element is disabled, or if its
130            // > nearest ancestor select element is disabled, or if it or one of its ancestors is
131            // > inert, and false otherwise.
132            // TODO: We do not support `inert` yet.
133            InteractiveElementCommand::Option(option) => {
134                option.Disabled() ||
135                    option
136                        .nearest_ancestor_select()
137                        .is_some_and(|select| select.Disabled())
138            },
139            // <https://html.spec.whatwg.org/multipage#using-the-accesskey-attribute-to-define-a-command-on-other-elements>
140            // > The Disabled State of the command is true if the element or one of its ancestors is
141            // > inert, and false otherwise.
142            // TODO: We do not support `inert` yet.
143            InteractiveElementCommand::HTMLElement(..) => false,
144        }
145    }
146
147    pub(crate) fn hidden(&self) -> bool {
148        let html_element: &HTMLElement = match self {
149            InteractiveElementCommand::Anchor(anchor_element) => anchor_element.upcast(),
150            InteractiveElementCommand::Button(button_element) => button_element.upcast(),
151            InteractiveElementCommand::Input(input_element) => input_element.upcast(),
152            InteractiveElementCommand::Option(option_element) => option_element.upcast(),
153            InteractiveElementCommand::HTMLElement(html_element) => html_element,
154        };
155        html_element.Hidden()
156    }
157
158    pub(crate) fn perform_action(&self, cx: &mut JSContext) {
159        match self {
160            // <https://html.spec.whatwg.org/multipage#using-the-a-element-to-define-a-command>
161            // > The Action of the command is to fire a click event at the element.
162            // <https://html.spec.whatwg.org/multipage/#fire-a-click-event>
163            // > Firing a click event at target means firing a synthetic pointer event named click at target.
164            InteractiveElementCommand::Anchor(anchor_element) => anchor_element
165                .upcast::<Node>()
166                .fire_synthetic_pointer_event_not_trusted(cx, atom!("click")),
167            // <https://html.spec.whatwg.org/multipage/#using-the-button-element-to-define-a-command>
168            // > The Label, Access Key, Hidden State, and Action facets of the command are
169            // > determined as for a elements (see the previous section).
170            InteractiveElementCommand::Button(button_element) => button_element
171                .upcast::<Node>()
172                .fire_synthetic_pointer_event_not_trusted(cx, atom!("click")),
173            // <https://html.spec.whatwg.org/multipage/#using-the-input-element-to-define-a-command>
174            // > The Action of the command is to fire a click event at the element.
175            InteractiveElementCommand::Input(input_element) => input_element
176                .upcast::<Node>()
177                .fire_synthetic_pointer_event_not_trusted(cx, atom!("click")),
178            // <https://html.spec.whatwg.org/multipage/#using-the-option-element-to-define-a-command>
179            // > If the option's nearest ancestor select element has a multiple attribute, the
180            // > Action of the command is to toggle the option element. Otherwise, the Action is to
181            // > pick the option element.
182            // Note: setSelected takes care of whether or not the owner has the `multiple` attribute.
183            InteractiveElementCommand::Option(option_element) => {
184                option_element.SetSelected(cx, true)
185            },
186            // > The Action of the command is to run the following steps:
187            // >  1. Run the focusing steps for the element.
188            // >  2. Fire a click event at the element.
189            InteractiveElementCommand::HTMLElement(html_element) => {
190                let node: &Node = html_element.upcast();
191                node.run_the_focusing_steps(cx, None, FocusTrigger::Other);
192                node.fire_synthetic_pointer_event_not_trusted(cx, atom!("click"));
193            },
194        }
195    }
196}