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