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