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}