Auto merge of #25488 - pshaughn:clickactivate, r=jdm

Event dispatch rewritten to align to spec, activate on clicks better

I went over the changes to the event dispatch spec that had accumulated over the past few years, rewriting dispatch/invoke/inner-invoke almost completely and modifying other code where it was relevant. Most of the remaining obvious deviations from spec are things that will only come up when we start handling events in shadow DOM.

I am pushing now because I want to see CI test results, but please do not approve this PR just if automated test improvements look good. I may have broken some actual UI interactions in the course of fixing synthetic events, and some manual testing is needed, including checking that manual interactions with interactive content continue to fire the events they're supposed to.

---
<!-- Thank you for contributing to Servo! Please replace each `[ ]` by `[X]` when the step is complete, and replace `___` with appropriate data: -->
- [X] `./mach build -d` does not report any errors
- [X] `./mach test-tidy` does not report any errors
- [X] These changes fix #25384 and fix #22783 and fix #25199

<!-- Either: -->
- [ ] There are automated tests for the synthetic-click parts of these changes, BUT the effects on real UI events need some manual testing before merging

<!-- Also, please make sure that "Allow edits from maintainers" checkbox is checked, so that we can help you if you get stuck somewhere along the way.-->

<!-- Pull requests that do not address these steps are welcome, but they will require additional verification as part of the review process. -->
This commit is contained in:
bors-servo 2020-02-13 17:37:12 -05:00 committed by GitHub
commit e697e6cca7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 466 additions and 556 deletions

View file

@ -2,6 +2,9 @@ DOMContentLoaded
abort abort
activate activate
addtrack addtrack
animationend
animationiteration
animationstart
beforeunload beforeunload
button button
canplay canplay
@ -133,5 +136,9 @@ visibilitychange
volumechange volumechange
waiting waiting
webglcontextcreationerror webglcontextcreationerror
webkitAnimationEnd
webkitAnimationIteration
webkitAnimationStart
webkitTransitionEnd
week week
width width

View file

@ -2,17 +2,13 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use crate::dom::bindings::codegen::Bindings::EventBinding::EventMethods;
use crate::dom::bindings::inheritance::Castable;
use crate::dom::bindings::str::DOMString;
use crate::dom::element::Element; use crate::dom::element::Element;
use crate::dom::event::{Event, EventBubbles, EventCancelable}; use crate::dom::event::Event;
use crate::dom::eventtarget::EventTarget; use crate::dom::eventtarget::EventTarget;
use crate::dom::mouseevent::MouseEvent; use crate::dom::htmlinputelement::InputActivationState;
use crate::dom::node::window_from_node; use crate::dom::node::window_from_node;
use crate::dom::window::ReflowReason; use crate::dom::window::ReflowReason;
use script_layout_interface::message::ReflowGoal; use script_layout_interface::message::ReflowGoal;
use script_traits::MouseButton;
/// Trait for elements with defined activation behavior /// Trait for elements with defined activation behavior
pub trait Activatable { pub trait Activatable {
@ -21,13 +17,17 @@ pub trait Activatable {
// Is this particular instance of the element activatable? // Is this particular instance of the element activatable?
fn is_instance_activatable(&self) -> bool; fn is_instance_activatable(&self) -> bool;
// https://html.spec.whatwg.org/multipage/#run-pre-click-activation-steps // https://dom.spec.whatwg.org/#eventtarget-legacy-pre-activation-behavior
fn pre_click_activation(&self); fn legacy_pre_activation_behavior(&self) -> Option<InputActivationState> {
None
}
// https://html.spec.whatwg.org/multipage/#run-canceled-activation-steps // https://dom.spec.whatwg.org/#eventtarget-legacy-canceled-activation-behavior
fn canceled_activation(&self); fn legacy_canceled_activation_behavior(&self, _state_before: Option<InputActivationState>) {}
// https://html.spec.whatwg.org/multipage/#run-post-click-activation-steps // https://dom.spec.whatwg.org/#eventtarget-activation-behavior
// event and target are used only by HTMLAnchorElement, in the case
// where the target is an <img ismap> so the href gets coordinates appended
fn activation_behavior(&self, event: &Event, target: &EventTarget); fn activation_behavior(&self, event: &Event, target: &EventTarget);
// https://html.spec.whatwg.org/multipage/#concept-selector-active // https://html.spec.whatwg.org/multipage/#concept-selector-active
@ -45,75 +45,3 @@ pub trait Activatable {
win.reflow(ReflowGoal::Full, ReflowReason::ElementStateChanged); win.reflow(ReflowGoal::Full, ReflowReason::ElementStateChanged);
} }
} }
/// Whether an activation was initiated via the click() method
#[derive(PartialEq)]
pub enum ActivationSource {
FromClick,
NotFromClick,
}
// https://html.spec.whatwg.org/multipage/#run-synthetic-click-activation-steps
pub fn synthetic_click_activation(
element: &Element,
ctrl_key: bool,
shift_key: bool,
alt_key: bool,
meta_key: bool,
source: ActivationSource,
) {
// Step 1
if element.click_in_progress() {
return;
}
// Step 2
element.set_click_in_progress(true);
// Step 3
let activatable = element.as_maybe_activatable();
if let Some(a) = activatable {
a.pre_click_activation();
}
// Step 4
// https://html.spec.whatwg.org/multipage/#fire-a-synthetic-mouse-event
let win = window_from_node(element);
let target = element.upcast::<EventTarget>();
let mouse = MouseEvent::new(
&win,
DOMString::from("click"),
EventBubbles::DoesNotBubble,
EventCancelable::NotCancelable,
Some(&win),
1,
0,
0,
0,
0,
ctrl_key,
shift_key,
alt_key,
meta_key,
0,
MouseButton::Left as u16,
None,
None,
);
let event = mouse.upcast::<Event>();
if source == ActivationSource::FromClick {
event.set_trusted(false);
}
target.dispatch_event(event);
// Step 5
if let Some(a) = activatable {
if event.DefaultPrevented() {
a.canceled_activation();
} else {
// post click activation
a.activation_behavior(event, target);
}
}
// Step 6
element.set_click_in_progress(false);
}

View file

@ -3,7 +3,6 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use crate::document_loader::{DocumentLoader, LoadType}; use crate::document_loader::{DocumentLoader, LoadType};
use crate::dom::activation::{synthetic_click_activation, ActivationSource};
use crate::dom::attr::Attr; use crate::dom::attr::Attr;
use crate::dom::beforeunloadevent::BeforeUnloadEvent; use crate::dom::beforeunloadevent::BeforeUnloadEvent;
use crate::dom::bindings::callback::ExceptionHandling; use crate::dom::bindings::callback::ExceptionHandling;
@ -557,8 +556,7 @@ impl Document {
let event = event.upcast::<Event>(); let event = event.upcast::<Event>();
event.set_trusted(true); event.set_trusted(true);
// FIXME(nox): Why are errors silenced here? // FIXME(nox): Why are errors silenced here?
let _ = window.upcast::<EventTarget>().dispatch_event_with_target( let _ = window.dispatch_event_with_target_override(
document.upcast(),
&event, &event,
); );
}), }),
@ -1016,7 +1014,11 @@ impl Document {
// https://html.spec.whatwg.org/multipage/#run-authentic-click-activation-steps // https://html.spec.whatwg.org/multipage/#run-authentic-click-activation-steps
let activatable = el.as_maybe_activatable(); let activatable = el.as_maybe_activatable();
match mouse_event_type { match mouse_event_type {
MouseEventType::Click => el.authentic_click_activation(event), MouseEventType::Click => {
el.set_click_in_progress(true);
event.fire(node.upcast());
el.set_click_in_progress(false);
},
MouseEventType::MouseDown => { MouseEventType::MouseDown => {
if let Some(a) = activatable { if let Some(a) = activatable {
a.enter_formal_activation_state(); a.enter_formal_activation_state();
@ -1478,16 +1480,9 @@ impl Document {
if (keyboard_event.key == Key::Enter || keyboard_event.code == Code::Space) && if (keyboard_event.key == Key::Enter || keyboard_event.code == Code::Space) &&
keyboard_event.state == KeyState::Up keyboard_event.state == KeyState::Up
{ {
let maybe_elem = target.downcast::<Element>(); if let Some(elem) = target.downcast::<Element>() {
if let Some(el) = maybe_elem { elem.upcast::<Node>()
synthetic_click_activation( .fire_synthetic_mouse_event_not_trusted(DOMString::from("click"));
el,
false,
false,
false,
false,
ActivationSource::NotFromClick,
)
} }
} }
} }
@ -1803,7 +1798,6 @@ impl Document {
// Step 2 // Step 2
self.incr_ignore_opens_during_unload_counter(); self.incr_ignore_opens_during_unload_counter();
//Step 3-5. //Step 3-5.
let document = Trusted::new(self);
let beforeunload_event = BeforeUnloadEvent::new( let beforeunload_event = BeforeUnloadEvent::new(
&self.window, &self.window,
atom!("beforeunload"), atom!("beforeunload"),
@ -1814,7 +1808,7 @@ impl Document {
event.set_trusted(true); event.set_trusted(true);
let event_target = self.window.upcast::<EventTarget>(); let event_target = self.window.upcast::<EventTarget>();
let has_listeners = event.has_listeners_for(&event_target, &atom!("beforeunload")); let has_listeners = event.has_listeners_for(&event_target, &atom!("beforeunload"));
event_target.dispatch_event_with_target(document.root().upcast(), &event); self.window.dispatch_event_with_target_override(&event);
// TODO: Step 6, decrease the event loop's termination nesting level by 1. // TODO: Step 6, decrease the event loop's termination nesting level by 1.
// Step 7 // Step 7
if has_listeners { if has_listeners {
@ -1858,7 +1852,6 @@ impl Document {
// TODO: Step 1, increase the event loop's termination nesting level by 1. // TODO: Step 1, increase the event loop's termination nesting level by 1.
// Step 2 // Step 2
self.incr_ignore_opens_during_unload_counter(); self.incr_ignore_opens_during_unload_counter();
let document = Trusted::new(self);
// Step 3-6 // Step 3-6
if self.page_showing.get() { if self.page_showing.get() {
self.page_showing.set(false); self.page_showing.set(false);
@ -1871,10 +1864,7 @@ impl Document {
); );
let event = event.upcast::<Event>(); let event = event.upcast::<Event>();
event.set_trusted(true); event.set_trusted(true);
let _ = self let _ = self.window.dispatch_event_with_target_override(&event);
.window
.upcast::<EventTarget>()
.dispatch_event_with_target(document.root().upcast(), &event);
// TODO Step 6, document visibility steps. // TODO Step 6, document visibility steps.
} }
// Step 7 // Step 7
@ -1888,7 +1878,7 @@ impl Document {
event.set_trusted(true); event.set_trusted(true);
let event_target = self.window.upcast::<EventTarget>(); let event_target = self.window.upcast::<EventTarget>();
let has_listeners = event.has_listeners_for(&event_target, &atom!("unload")); let has_listeners = event.has_listeners_for(&event_target, &atom!("unload"));
let _ = event_target.dispatch_event_with_target(document.root().upcast(), &event); let _ = self.window.dispatch_event_with_target_override(&event);
self.fired_unload.set(true); self.fired_unload.set(true);
// Step 9 // Step 9
if has_listeners { if has_listeners {
@ -1984,8 +1974,7 @@ impl Document {
debug!("About to dispatch load for {:?}", document.url()); debug!("About to dispatch load for {:?}", document.url());
// FIXME(nox): Why are errors silenced here? // FIXME(nox): Why are errors silenced here?
let _ = window.upcast::<EventTarget>().dispatch_event_with_target( let _ = window.dispatch_event_with_target_override(
document.upcast(),
&event, &event,
); );
@ -2029,8 +2018,7 @@ impl Document {
event.set_trusted(true); event.set_trusted(true);
// FIXME(nox): Why are errors silenced here? // FIXME(nox): Why are errors silenced here?
let _ = window.upcast::<EventTarget>().dispatch_event_with_target( let _ = window.dispatch_event_with_target_override(
document.upcast(),
&event, &event,
); );
}), }),

View file

@ -11,7 +11,6 @@ use crate::dom::bindings::codegen::Bindings::AttrBinding::AttrMethods;
use crate::dom::bindings::codegen::Bindings::DocumentBinding::DocumentMethods; use crate::dom::bindings::codegen::Bindings::DocumentBinding::DocumentMethods;
use crate::dom::bindings::codegen::Bindings::ElementBinding; use crate::dom::bindings::codegen::Bindings::ElementBinding;
use crate::dom::bindings::codegen::Bindings::ElementBinding::ElementMethods; use crate::dom::bindings::codegen::Bindings::ElementBinding::ElementMethods;
use crate::dom::bindings::codegen::Bindings::EventBinding::EventMethods;
use crate::dom::bindings::codegen::Bindings::FunctionBinding::Function; use crate::dom::bindings::codegen::Bindings::FunctionBinding::Function;
use crate::dom::bindings::codegen::Bindings::HTMLTemplateElementBinding::HTMLTemplateElementMethods; use crate::dom::bindings::codegen::Bindings::HTMLTemplateElementBinding::HTMLTemplateElementMethods;
use crate::dom::bindings::codegen::Bindings::NodeBinding::NodeMethods; use crate::dom::bindings::codegen::Bindings::NodeBinding::NodeMethods;
@ -39,7 +38,6 @@ use crate::dom::document::{determine_policy_for_token, Document, LayoutDocumentH
use crate::dom::documentfragment::DocumentFragment; use crate::dom::documentfragment::DocumentFragment;
use crate::dom::domrect::DOMRect; use crate::dom::domrect::DOMRect;
use crate::dom::domtokenlist::DOMTokenList; use crate::dom::domtokenlist::DOMTokenList;
use crate::dom::event::Event;
use crate::dom::eventtarget::EventTarget; use crate::dom::eventtarget::EventTarget;
use crate::dom::htmlanchorelement::HTMLAnchorElement; use crate::dom::htmlanchorelement::HTMLAnchorElement;
use crate::dom::htmlbodyelement::{HTMLBodyElement, HTMLBodyElementLayoutHelpers}; use crate::dom::htmlbodyelement::{HTMLBodyElement, HTMLBodyElementLayoutHelpers};
@ -3283,52 +3281,6 @@ impl Element {
} }
} }
/// Please call this method *only* for real click events
///
/// <https://html.spec.whatwg.org/multipage/#run-authentic-click-activation-steps>
///
/// Use an element's synthetic click activation (or handle_event) for any script-triggered clicks.
/// If the spec says otherwise, check with Manishearth first
pub fn authentic_click_activation(&self, event: &Event) {
// Not explicitly part of the spec, however this helps enforce the invariants
// required to save state between pre-activation and post-activation
// since we cannot nest authentic clicks (unlike synthetic click activation, where
// the script can generate more click events from the handler)
assert!(!self.click_in_progress());
let target = self.upcast();
// Step 2 (requires canvas support)
// Step 3
self.set_click_in_progress(true);
// Step 4
let e = self.nearest_activable_element();
match e {
Some(ref el) => match el.as_maybe_activatable() {
Some(elem) => {
// Step 5-6
elem.pre_click_activation();
event.fire(target);
if !event.DefaultPrevented() {
// post click activation
elem.activation_behavior(event, target);
} else {
elem.canceled_activation();
}
},
// Step 6
None => {
event.fire(target);
},
},
// Step 6
None => {
event.fire(target);
},
}
// Step 7
self.set_click_in_progress(false);
}
// https://html.spec.whatwg.org/multipage/#language // https://html.spec.whatwg.org/multipage/#language
pub fn get_lang(&self) -> String { pub fn get_lang(&self) -> String {
self.upcast::<Node>() self.upcast::<Node>()

View file

@ -8,16 +8,20 @@ use crate::dom::bindings::codegen::Bindings::EventBinding;
use crate::dom::bindings::codegen::Bindings::EventBinding::{EventConstants, EventMethods}; use crate::dom::bindings::codegen::Bindings::EventBinding::{EventConstants, EventMethods};
use crate::dom::bindings::codegen::Bindings::PerformanceBinding::DOMHighResTimeStamp; use crate::dom::bindings::codegen::Bindings::PerformanceBinding::DOMHighResTimeStamp;
use crate::dom::bindings::codegen::Bindings::PerformanceBinding::PerformanceBinding::PerformanceMethods; use crate::dom::bindings::codegen::Bindings::PerformanceBinding::PerformanceBinding::PerformanceMethods;
use crate::dom::bindings::codegen::Bindings::WindowBinding::WindowMethods;
use crate::dom::bindings::error::Fallible; use crate::dom::bindings::error::Fallible;
use crate::dom::bindings::inheritance::Castable; use crate::dom::bindings::inheritance::Castable;
use crate::dom::bindings::refcounted::Trusted; use crate::dom::bindings::refcounted::Trusted;
use crate::dom::bindings::reflector::{reflect_dom_object, DomObject, Reflector}; use crate::dom::bindings::reflector::{reflect_dom_object, DomObject, Reflector};
use crate::dom::bindings::root::{DomRoot, DomSlice, MutNullableDom}; use crate::dom::bindings::root::{DomRoot, MutNullableDom};
use crate::dom::bindings::str::DOMString; use crate::dom::bindings::str::DOMString;
use crate::dom::document::Document; use crate::dom::document::Document;
use crate::dom::element::Element;
use crate::dom::eventtarget::{CompiledEventListener, EventTarget, ListenerPhase}; use crate::dom::eventtarget::{CompiledEventListener, EventTarget, ListenerPhase};
use crate::dom::globalscope::GlobalScope; use crate::dom::globalscope::GlobalScope;
use crate::dom::node::Node; use crate::dom::htmlinputelement::InputActivationState;
use crate::dom::mouseevent::MouseEvent;
use crate::dom::node::{Node, ShadowIncluding};
use crate::dom::performance::reduce_timing_resolution; use crate::dom::performance::reduce_timing_resolution;
use crate::dom::virtualmethods::vtable_for; use crate::dom::virtualmethods::vtable_for;
use crate::dom::window::Window; use crate::dom::window::Window;
@ -122,14 +126,24 @@ impl Event {
} }
// https://dom.spec.whatwg.org/#event-path // https://dom.spec.whatwg.org/#event-path
// TODO: shadow roots put special flags in the path,
// and it will stop just being a list of bare EventTargets
fn construct_event_path(&self, target: &EventTarget) -> Vec<DomRoot<EventTarget>> { fn construct_event_path(&self, target: &EventTarget) -> Vec<DomRoot<EventTarget>> {
let mut event_path = vec![]; let mut event_path = vec![];
// The "invoke" algorithm is only used on `target` separately,
// so we don't put it in the path.
if let Some(target_node) = target.downcast::<Node>() { if let Some(target_node) = target.downcast::<Node>() {
for ancestor in target_node.ancestors() { // ShadowIncluding::Yes might be closer to right than ::No,
// but still wrong since things about the path change when crossing
// shadow attachments; getting it right needs to change
// more than just that.
for ancestor in target_node.inclusive_ancestors(ShadowIncluding::No) {
event_path.push(DomRoot::from_ref(ancestor.upcast::<EventTarget>())); event_path.push(DomRoot::from_ref(ancestor.upcast::<EventTarget>()));
} }
// Most event-target-to-parent relationships are node parent
// relationships, but the document-to-global one is not,
// so that's handled separately here.
// (an EventTarget.get_parent_event_target could save
// some redundancy, especially when shadow DOM relationships
// also need to be respected)
let top_most_ancestor_or_target = event_path let top_most_ancestor_or_target = event_path
.last() .last()
.cloned() .cloned()
@ -139,6 +153,11 @@ impl Event {
event_path.push(DomRoot::from_ref(document.window().upcast())); event_path.push(DomRoot::from_ref(document.window().upcast()));
} }
} }
} else {
// a non-node EventTarget, likely a global.
// No parent to propagate up to, but we still
// need it on the path.
event_path.push(DomRoot::from_ref(target));
} }
event_path event_path
} }
@ -147,49 +166,185 @@ impl Event {
pub fn dispatch( pub fn dispatch(
&self, &self,
target: &EventTarget, target: &EventTarget,
target_override: Option<&EventTarget>, legacy_target_override: bool,
// TODO legacy_did_output_listeners_throw_flag for indexeddb
) -> EventStatus { ) -> EventStatus {
assert!(!self.dispatching());
assert!(self.initialized());
assert_eq!(self.phase.get(), EventPhase::None);
assert!(self.GetCurrentTarget().is_none());
// Step 1. // Step 1.
self.dispatching.set(true); self.dispatching.set(true);
// Step 2. // Step 2.
self.target.set(Some(target_override.unwrap_or(target))); let target_override_document; // upcasted EventTarget's lifetime depends on this
let target_override = if legacy_target_override {
target_override_document = target
.downcast::<Window>()
.expect("legacy_target_override must be true only when target is a Window")
.Document();
target_override_document.upcast::<EventTarget>()
} else {
target
};
if self.stop_propagation.get() { // Step 3 - since step 5 always happens, we can wait until 5.5
// If the event's stop propagation flag is set, we can skip everything because
// it prevents the calls of the invoke algorithm in the spec.
// Step 10-12. // Step 4 TODO: "retargeting" concept depends on shadow DOM
self.clear_dispatching_flags();
// Step 14. // Step 5, outer if-statement, is always true until step 4 is implemented
return self.status(); // Steps 5.1-5.2 TODO: touch target lists don't exist yet
}
// Step 3-4. // Steps 5.3 and most of 5.9
// A change in whatwg/dom#240 specifies that
// the event path belongs to the event itself, rather than being
// a local variable of the dispatch algorithm, but this is mostly
// related to shadow DOM requirements that aren't otherwise
// implemented right now. The path also needs to contain
// various flags instead of just bare event targets.
let path = self.construct_event_path(&target); let path = self.construct_event_path(&target);
rooted_vec!(let event_path <- path.into_iter()); rooted_vec!(let event_path <- path.into_iter());
// Steps 5-9. In a separate function to short-circuit various things easily.
dispatch_to_listeners(self, target, event_path.r());
// Default action. // Step 5.4
if let Some(target) = self.GetTarget() { let is_activation_event = self.is::<MouseEvent>() && self.type_() == atom!("click");
if let Some(node) = target.downcast::<Node>() {
let vtable = vtable_for(&node); // Step 5.5
vtable.handle_event(self); let mut activation_target = if is_activation_event {
target
.downcast::<Element>()
.and_then(|e| e.as_maybe_activatable())
} else {
// Step 3
None
};
// Steps 5-6 - 5.7 are shadow DOM slot things
// Step 5.9.8.1, not covered in construct_event_path
// This what makes sure that clicking on e.g. an <img> inside
// an <a> will cause activation of the activatable ancestor.
if is_activation_event && activation_target.is_none() && self.bubbles.get() {
for object in event_path.iter() {
if let Some(activatable_ancestor) = object
.downcast::<Element>()
.and_then(|e| e.as_maybe_activatable())
{
activation_target = Some(activatable_ancestor);
// once activation_target isn't null, we stop
// looking at ancestors for it.
break;
}
} }
} }
// Step 10-12. // Steps 5.10-5.11 are shadow DOM
self.clear_dispatching_flags();
// Step 14. // Not specified in dispatch spec overtly; this is because
self.status() // the legacy canceled activation behavior of a checkbox
// or radio button needs to know what happened in the
// corresponding pre-activation behavior.
let mut pre_activation_result: Option<InputActivationState> = None;
// Step 5.12
if is_activation_event {
if let Some(maybe_checkbox) = activation_target {
pre_activation_result = maybe_checkbox.legacy_pre_activation_behavior();
}
}
let timeline_window = match DomRoot::downcast::<Window>(target.global()) {
Some(window) => {
if window.need_emit_timeline_marker(TimelineMarkerType::DOMEvent) {
Some(window)
} else {
None
}
},
_ => None,
};
// Step 5.13
for object in event_path.iter().rev() {
if &**object == &*target {
self.phase.set(EventPhase::AtTarget);
} else {
self.phase.set(EventPhase::Capturing);
}
// setting self.target is step 1 of invoke,
// but done here because our event_path isn't a member of self
// (without shadow DOM, target_override is always the
// target to set to)
self.target.set(Some(target_override));
invoke(
timeline_window.as_deref(),
object,
self,
Some(ListenerPhase::Capturing),
);
}
// Step 5.14
for object in event_path.iter() {
let at_target = &**object == &*target;
if at_target || self.bubbles.get() {
self.phase.set(if at_target {
EventPhase::AtTarget
} else {
EventPhase::Bubbling
});
self.target.set(Some(target_override));
invoke(
timeline_window.as_deref(),
object,
self,
Some(ListenerPhase::Bubbling),
);
}
}
// Step 6
self.phase.set(EventPhase::None);
// FIXME: The UIEvents spec still expects firing an event
// to carry a "default action" semantic, but the HTML spec
// has removed this concept. Nothing in either spec currently
// (as of Jan 11 2020) says that, e.g., a keydown event on an
// input element causes a character to be typed; the UIEvents
// spec assumes the HTML spec is covering it, and the HTML spec
// no longer specifies any UI event other than mouse click as
// causing an element to perform an action.
// Compare:
// https://w3c.github.io/uievents/#default-action
// https://dom.spec.whatwg.org/#action-versus-occurance
if !self.DefaultPrevented() {
if let Some(target) = self.GetTarget() {
if let Some(node) = target.downcast::<Node>() {
let vtable = vtable_for(&node);
vtable.handle_event(self);
}
}
}
// Step 7
self.current_target.set(None);
// Step 8 TODO: if path were in the event struct, we'd clear it now
// Step 9
self.dispatching.set(false);
self.stop_propagation.set(false);
self.stop_immediate.set(false);
// Step 10 TODO: condition is always false until there's shadow DOM
// Step 11
if let Some(activation_target) = activation_target {
if self.DefaultPrevented() {
activation_target.legacy_canceled_activation_behavior(pre_activation_result);
} else {
activation_target.activation_behavior(self, target);
}
}
return self.status();
} }
pub fn status(&self) -> EventStatus { pub fn status(&self) -> EventStatus {
@ -205,18 +360,6 @@ impl Event {
self.dispatching.get() self.dispatching.get()
} }
#[inline]
// https://dom.spec.whatwg.org/#concept-event-dispatch Steps 10-12.
fn clear_dispatching_flags(&self) {
assert!(self.dispatching.get());
self.dispatching.set(false);
self.stop_propagation.set(false);
self.stop_immediate.set(false);
self.phase.set(EventPhase::None);
self.current_target.set(None);
}
#[inline] #[inline]
pub fn initialized(&self) -> bool { pub fn initialized(&self) -> bool {
self.initialized.get() self.initialized.get()
@ -468,98 +611,55 @@ impl TaskOnce for SimpleEventTask {
} }
} }
// See dispatch_event. // https://dom.spec.whatwg.org/#concept-event-listener-invoke
// https://dom.spec.whatwg.org/#concept-event-dispatch fn invoke(
fn dispatch_to_listeners(event: &Event, target: &EventTarget, event_path: &[&EventTarget]) { timeline_window: Option<&Window>,
assert!(!event.stop_propagation.get()); object: &EventTarget,
assert!(!event.stop_immediate.get()); event: &Event,
phase: Option<ListenerPhase>,
// TODO legacy_output_did_listeners_throw for indexeddb
) {
// Step 1: Until shadow DOM puts the event path in the
// event itself, this is easier to do in dispatch before
// calling invoke.
let window = match DomRoot::downcast::<Window>(target.global()) { // Step 2 TODO: relatedTarget only matters for shadow DOM
Some(window) => {
if window.need_emit_timeline_marker(TimelineMarkerType::DOMEvent) {
Some(window)
} else {
None
}
},
_ => None,
};
// Step 5. // Step 3 TODO: touch target lists not implemented
event.phase.set(EventPhase::Capturing);
// Step 6. // Step 4.
for object in event_path.iter().rev() {
invoke(
window.as_deref(),
object,
event,
Some(ListenerPhase::Capturing),
);
if event.stop_propagation.get() {
return;
}
}
assert!(!event.stop_propagation.get());
assert!(!event.stop_immediate.get());
// Step 7.
event.phase.set(EventPhase::AtTarget);
// Step 8.
invoke(window.as_deref(), target, event, None);
if event.stop_propagation.get() { if event.stop_propagation.get() {
return; return;
} }
assert!(!event.stop_propagation.get()); // Step 5.
assert!(!event.stop_immediate.get());
if !event.bubbles.get() {
return;
}
// Step 9.1.
event.phase.set(EventPhase::Bubbling);
// Step 9.2.
for object in event_path {
invoke(
window.as_deref(),
object,
event,
Some(ListenerPhase::Bubbling),
);
if event.stop_propagation.get() {
return;
}
}
}
// https://dom.spec.whatwg.org/#concept-event-listener-invoke
fn invoke(
window: Option<&Window>,
object: &EventTarget,
event: &Event,
specific_listener_phase: Option<ListenerPhase>,
) {
// Step 1.
assert!(!event.stop_propagation.get());
// Steps 2-3.
let listeners = object.get_listeners_for(&event.type_(), specific_listener_phase);
// Step 4.
event.current_target.set(Some(object)); event.current_target.set(Some(object));
// Step 5. // Step 6
inner_invoke(window, object, event, &listeners); let listeners = object.get_listeners_for(&event.type_(), phase);
// TODO: step 6. // Step 7.
let found = inner_invoke(timeline_window, object, event, &listeners);
// Step 8
if !found && event.trusted.get() {
if let Some(legacy_type) = match event.type_() {
atom!("animationend") => Some(atom!("webkitAnimationEnd")),
atom!("animationiteration") => Some(atom!("webkitAnimationIteration")),
atom!("animationstart") => Some(atom!("webkitAnimationStart")),
atom!("transitionend") => Some(atom!("webkitTransitionEnd")),
_ => None,
} {
let original_type = event.type_();
*event.type_.borrow_mut() = legacy_type;
inner_invoke(timeline_window, object, event, &listeners);
*event.type_.borrow_mut() = original_type;
}
}
} }
// https://dom.spec.whatwg.org/#concept-event-listener-inner-invoke // https://dom.spec.whatwg.org/#concept-event-listener-inner-invoke
fn inner_invoke( fn inner_invoke(
window: Option<&Window>, timeline_window: Option<&Window>,
object: &EventTarget, object: &EventTarget,
event: &Event, event: &Event,
listeners: &[CompiledEventListener], listeners: &[CompiledEventListener],
@ -569,6 +669,15 @@ fn inner_invoke(
// Step 2. // Step 2.
for listener in listeners { for listener in listeners {
// FIXME(#25479): We need an "if !listener.removed()" here,
// but there's a subtlety. Where Servo is currently using the
// CompiledEventListener, we really need something that maps to
// https://dom.spec.whatwg.org/#concept-event-listener
// which is not the same thing as the EventListener interface.
// script::dom::eventtarget::EventListenerEntry is the closest
// match we have, and is already holding the "once" flag,
// but it's not a drop-in replacement.
// Steps 2.1 and 2.3-2.4 are not done because `listeners` contain only the // Steps 2.1 and 2.3-2.4 are not done because `listeners` contain only the
// relevant ones for this invoke call during the dispatch algorithm. // relevant ones for this invoke call during the dispatch algorithm.
@ -580,17 +689,35 @@ fn inner_invoke(
object.remove_listener_if_once(&event.type_(), &event_listener); object.remove_listener_if_once(&event.type_(), &event_listener);
} }
// Step 2.6. // Step 2.6-2.8
// FIXME(#25478): we need to get the global that the event
// listener is going to be called on, then if it's a Window
// set its .event to the event, remembering the previous
// value of its .event. This allows events to just use
// the word "event" instead of taking the event as an argument.
// Step 2.9 TODO: EventListener passive option not implemented
// Step 2.10
let marker = TimelineMarker::start("DOMEvent".to_owned()); let marker = TimelineMarker::start("DOMEvent".to_owned());
// Step 2.10
listener.call_or_handle_event(object, event, ExceptionHandling::Report); listener.call_or_handle_event(object, event, ExceptionHandling::Report);
if let Some(window) = window {
if let Some(window) = timeline_window {
window.emit_timeline_marker(marker.end()); window.emit_timeline_marker(marker.end());
} }
// Step 2.11 TODO: passive not implemented
// Step 2.12
// TODO This is where we put back the .event we
// had before step 2.6.
// Step 2.13: short-circuit instead of going to next listener
if event.stop_immediate.get() { if event.stop_immediate.get() {
return found; return found;
} }
// TODO: step 2.7.
} }
// Step 3. // Step 3.

View file

@ -73,7 +73,7 @@ impl CommonEventHandler {
} }
} }
#[derive(Clone, Copy, JSTraceable, MallocSizeOf, PartialEq)] #[derive(Clone, Copy, Debug, JSTraceable, MallocSizeOf, PartialEq)]
pub enum ListenerPhase { pub enum ListenerPhase {
Capturing, Capturing,
Bubbling, Bubbling,
@ -248,6 +248,8 @@ impl CompiledEventListener {
} }
} }
// https://dom.spec.whatwg.org/#concept-event-listener
// (as distinct from https://dom.spec.whatwg.org/#callbackdef-eventlistener)
#[derive(Clone, DenyPublicFields, JSTraceable, MallocSizeOf)] #[derive(Clone, DenyPublicFields, JSTraceable, MallocSizeOf)]
/// A listener in a collection of event listeners. /// A listener in a collection of event listeners.
struct EventListenerEntry { struct EventListenerEntry {
@ -367,23 +369,13 @@ impl EventTarget {
}) })
} }
pub fn dispatch_event_with_target(&self, target: &EventTarget, event: &Event) -> EventStatus {
if let Some(window) = target.global().downcast::<Window>() {
if window.has_document() {
assert!(window.Document().can_invoke_script());
}
};
event.dispatch(self, Some(target))
}
pub fn dispatch_event(&self, event: &Event) -> EventStatus { pub fn dispatch_event(&self, event: &Event) -> EventStatus {
if let Some(window) = self.global().downcast::<Window>() { if let Some(window) = self.global().downcast::<Window>() {
if window.has_document() { if window.has_document() {
assert!(window.Document().can_invoke_script()); assert!(window.Document().can_invoke_script());
} }
}; };
event.dispatch(self, None) event.dispatch(self, false)
} }
pub fn remove_all_listeners(&self) { pub fn remove_all_listeners(&self) {

View file

@ -542,13 +542,6 @@ impl Activatable for HTMLAnchorElement {
self.as_element().has_attribute(&local_name!("href")) self.as_element().has_attribute(&local_name!("href"))
} }
//TODO:https://html.spec.whatwg.org/multipage/#the-a-element
fn pre_click_activation(&self) {}
//TODO:https://html.spec.whatwg.org/multipage/#the-a-element
// https://html.spec.whatwg.org/multipage/#run-canceled-activation-steps
fn canceled_activation(&self) {}
//https://html.spec.whatwg.org/multipage/#the-a-element:activation-behaviour //https://html.spec.whatwg.org/multipage/#the-a-element:activation-behaviour
fn activation_behavior(&self, event: &Event, target: &EventTarget) { fn activation_behavior(&self, event: &Event, target: &EventTarget) {
let element = self.as_element(); let element = self.as_element();

View file

@ -322,10 +322,6 @@ impl Activatable for HTMLAreaElement {
self.as_element().has_attribute(&local_name!("href")) self.as_element().has_attribute(&local_name!("href"))
} }
fn pre_click_activation(&self) {}
fn canceled_activation(&self) {}
fn activation_behavior(&self, _event: &Event, _target: &EventTarget) { fn activation_behavior(&self, _event: &Event, _target: &EventTarget) {
follow_hyperlink(self.as_element(), None); follow_hyperlink(self.as_element(), None);
} }

View file

@ -290,13 +290,6 @@ impl Activatable for HTMLButtonElement {
!self.upcast::<Element>().disabled_state() !self.upcast::<Element>().disabled_state()
} }
// https://html.spec.whatwg.org/multipage/#run-pre-click-activation-steps
// https://html.spec.whatwg.org/multipage/#the-button-element:activation-behavior
fn pre_click_activation(&self) {}
// https://html.spec.whatwg.org/multipage/#run-canceled-activation-steps
fn canceled_activation(&self) {}
// https://html.spec.whatwg.org/multipage/#run-post-click-activation-steps // https://html.spec.whatwg.org/multipage/#run-post-click-activation-steps
fn activation_behavior(&self, _event: &Event, _target: &EventTarget) { fn activation_behavior(&self, _event: &Event, _target: &EventTarget) {
let ty = self.button_type.get(); let ty = self.button_type.get();

View file

@ -2,7 +2,6 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use crate::dom::activation::{synthetic_click_activation, ActivationSource};
use crate::dom::attr::Attr; use crate::dom::attr::Attr;
use crate::dom::bindings::codegen::Bindings::EventHandlerBinding::EventHandlerNonNull; use crate::dom::bindings::codegen::Bindings::EventHandlerBinding::EventHandlerNonNull;
use crate::dom::bindings::codegen::Bindings::EventHandlerBinding::OnErrorEventHandlerNonNull; use crate::dom::bindings::codegen::Bindings::EventHandlerBinding::OnErrorEventHandlerNonNull;
@ -389,16 +388,18 @@ impl HTMLElementMethods for HTMLElement {
// https://html.spec.whatwg.org/multipage/#dom-click // https://html.spec.whatwg.org/multipage/#dom-click
fn Click(&self) { fn Click(&self) {
if !self.upcast::<Element>().disabled_state() { let element = self.upcast::<Element>();
synthetic_click_activation( if element.disabled_state() {
self.upcast::<Element>(), return;
false,
false,
false,
false,
ActivationSource::FromClick,
)
} }
if element.click_in_progress() {
return;
}
element.set_click_in_progress(true);
self.upcast::<Node>()
.fire_synthetic_mouse_event_not_trusted(DOMString::from("click"));
element.set_click_in_progress(false);
} }
// https://html.spec.whatwg.org/multipage/#dom-focus // https://html.spec.whatwg.org/multipage/#dom-focus

View file

@ -2,7 +2,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use crate::dom::activation::{synthetic_click_activation, Activatable, ActivationSource}; use crate::dom::activation::Activatable;
use crate::dom::attr::Attr; use crate::dom::attr::Attr;
use crate::dom::bindings::cell::DomRefCell; use crate::dom::bindings::cell::DomRefCell;
use crate::dom::bindings::codegen::Bindings::ElementBinding::ElementMethods; use crate::dom::bindings::codegen::Bindings::ElementBinding::ElementMethods;
@ -11,12 +11,11 @@ use crate::dom::bindings::codegen::Bindings::FileListBinding::FileListMethods;
use crate::dom::bindings::codegen::Bindings::HTMLFormElementBinding::SelectionMode; use crate::dom::bindings::codegen::Bindings::HTMLFormElementBinding::SelectionMode;
use crate::dom::bindings::codegen::Bindings::HTMLInputElementBinding; use crate::dom::bindings::codegen::Bindings::HTMLInputElementBinding;
use crate::dom::bindings::codegen::Bindings::HTMLInputElementBinding::HTMLInputElementMethods; use crate::dom::bindings::codegen::Bindings::HTMLInputElementBinding::HTMLInputElementMethods;
use crate::dom::bindings::codegen::Bindings::KeyboardEventBinding::KeyboardEventMethods;
use crate::dom::bindings::codegen::Bindings::NodeBinding::{GetRootNodeOptions, NodeMethods}; use crate::dom::bindings::codegen::Bindings::NodeBinding::{GetRootNodeOptions, NodeMethods};
use crate::dom::bindings::error::{Error, ErrorResult}; use crate::dom::bindings::error::{Error, ErrorResult};
use crate::dom::bindings::inheritance::Castable; use crate::dom::bindings::inheritance::Castable;
use crate::dom::bindings::reflector::DomObject; use crate::dom::bindings::reflector::DomObject;
use crate::dom::bindings::root::{Dom, DomRoot, LayoutDom, MutNullableDom}; use crate::dom::bindings::root::{DomRoot, LayoutDom, MutNullableDom};
use crate::dom::bindings::str::{DOMString, USVString}; use crate::dom::bindings::str::{DOMString, USVString};
use crate::dom::compositionevent::CompositionEvent; use crate::dom::compositionevent::CompositionEvent;
use crate::dom::document::Document; use crate::dom::document::Document;
@ -246,7 +245,6 @@ pub struct HTMLInputElement {
minlength: Cell<i32>, minlength: Cell<i32>,
#[ignore_malloc_size_of = "#7193"] #[ignore_malloc_size_of = "#7193"]
textinput: DomRefCell<TextInput<ScriptToConstellationChan>>, textinput: DomRefCell<TextInput<ScriptToConstellationChan>>,
activation_state: DomRefCell<InputActivationState>,
// https://html.spec.whatwg.org/multipage/#concept-input-value-dirty-flag // https://html.spec.whatwg.org/multipage/#concept-input-value-dirty-flag
value_dirty: Cell<bool>, value_dirty: Cell<bool>,
// not specified explicitly, but implied by the fact that sanitization can't // not specified explicitly, but implied by the fact that sanitization can't
@ -260,30 +258,13 @@ pub struct HTMLInputElement {
} }
#[derive(JSTraceable)] #[derive(JSTraceable)]
#[unrooted_must_root_lint::must_root] pub struct InputActivationState {
#[derive(MallocSizeOf)]
struct InputActivationState {
indeterminate: bool, indeterminate: bool,
checked: bool, checked: bool,
checked_changed: bool, checked_radio: Option<DomRoot<HTMLInputElement>>,
checked_radio: Option<Dom<HTMLInputElement>>,
// In case mutability changed
was_mutable: bool,
// In case the type changed // In case the type changed
old_type: InputType, old_type: InputType,
} // was_mutable is implied: pre-activation would return None if it wasn't
impl InputActivationState {
fn new() -> InputActivationState {
InputActivationState {
indeterminate: false,
checked: false,
checked_changed: false,
checked_radio: None,
was_mutable: false,
old_type: Default::default(),
}
}
} }
static DEFAULT_INPUT_SIZE: u32 = 20; static DEFAULT_INPUT_SIZE: u32 = 20;
@ -323,7 +304,6 @@ impl HTMLInputElement {
None, None,
SelectionDirection::None, SelectionDirection::None,
)), )),
activation_state: DomRefCell::new(InputActivationState::new()),
value_dirty: Cell::new(false), value_dirty: Cell::new(false),
sanitization_flag: Cell::new(true), sanitization_flag: Cell::new(true),
filelist: MutNullableDom::new(None), filelist: MutNullableDom::new(None),
@ -1756,7 +1736,7 @@ impl HTMLInputElement {
// https://html.spec.whatwg.org/multipage/#implicit-submission // https://html.spec.whatwg.org/multipage/#implicit-submission
#[allow(unsafe_code)] #[allow(unsafe_code)]
fn implicit_submission(&self, ctrl_key: bool, shift_key: bool, alt_key: bool, meta_key: bool) { fn implicit_submission(&self) {
let doc = document_from_node(self); let doc = document_from_node(self);
let node = doc.upcast::<Node>(); let node = doc.upcast::<Node>();
let owner = self.form_owner(); let owner = self.form_owner();
@ -1777,14 +1757,11 @@ impl HTMLInputElement {
match submit_button { match submit_button {
Some(ref button) => { Some(ref button) => {
if button.is_instance_activatable() { if button.is_instance_activatable() {
synthetic_click_activation( // spec does not actually say to set the not trusted flag,
button.as_element(), // but we can get here from synthetic keydown events
ctrl_key, button
shift_key, .upcast::<Node>()
alt_key, .fire_synthetic_mouse_event_not_trusted(DOMString::from("click"));
meta_key,
ActivationSource::NotFromClick,
)
} }
}, },
None => { None => {
@ -2199,14 +2176,19 @@ impl VirtualMethods for HTMLInputElement {
} }
} }
// This represents behavior for which the UIEvents spec and the
// DOM/HTML specs are out of sync.
// Compare:
// https://w3c.github.io/uievents/#default-action
// https://dom.spec.whatwg.org/#action-versus-occurance
fn handle_event(&self, event: &Event) { fn handle_event(&self, event: &Event) {
if let Some(s) = self.super_type() { if let Some(s) = self.super_type() {
s.handle_event(event); s.handle_event(event);
} }
if event.type_() == atom!("click") && !event.DefaultPrevented() { if event.type_() == atom!("click") && !event.DefaultPrevented() {
// TODO: Dispatch events for non activatable inputs // WHATWG-specified activation behaviors are handled elsewhere;
// https://html.spec.whatwg.org/multipage/#common-input-element-events // this is for all the other things a UI click might do
//TODO: set the editing position for text inputs //TODO: set the editing position for text inputs
@ -2242,12 +2224,7 @@ impl VirtualMethods for HTMLInputElement {
let action = self.textinput.borrow_mut().handle_keydown(keyevent); let action = self.textinput.borrow_mut().handle_keydown(keyevent);
match action { match action {
TriggerDefaultAction => { TriggerDefaultAction => {
self.implicit_submission( self.implicit_submission();
keyevent.CtrlKey(),
keyevent.ShiftKey(),
keyevent.AltKey(),
keyevent.MetaKey(),
);
}, },
DispatchInput => { DispatchInput => {
self.value_dirty.set(true); self.value_dirty.set(true);
@ -2365,91 +2342,90 @@ impl Activatable for HTMLInputElement {
} }
} }
// https://html.spec.whatwg.org/multipage/#run-pre-click-activation-steps // https://dom.spec.whatwg.org/#eventtarget-legacy-pre-activation-behavior
#[allow(unsafe_code)] fn legacy_pre_activation_behavior(&self) -> Option<InputActivationState> {
fn pre_click_activation(&self) { if !self.is_mutable() {
let mut cache = self.activation_state.borrow_mut(); return None;
let ty = self.input_type();
cache.old_type = ty;
cache.was_mutable = self.is_mutable();
if cache.was_mutable {
match ty {
// https://html.spec.whatwg.org/multipage/#submit-button-state-(type=submit):activation-behavior
// InputType::Submit => (), // No behavior defined
// https://html.spec.whatwg.org/multipage/#reset-button-state-(type=reset):activation-behavior
// InputType::Submit => (), // No behavior defined
InputType::Checkbox => {
/*
https://html.spec.whatwg.org/multipage/#checkbox-state-(type=checkbox):pre-click-activation-steps
cache current values of `checked` and `indeterminate`
we may need to restore them later
*/
cache.indeterminate = self.Indeterminate();
cache.checked = self.Checked();
cache.checked_changed = self.checked_changed.get();
self.SetIndeterminate(false);
self.SetChecked(!cache.checked);
},
// https://html.spec.whatwg.org/multipage/#radio-button-state-(type=radio):pre-click-activation-steps
InputType::Radio => {
let checked_member = radio_group_iter(self, self.radio_group_name().as_ref())
.find(|r| r.Checked());
cache.checked_radio = checked_member.as_deref().map(Dom::from_ref);
cache.checked_changed = self.checked_changed.get();
self.SetChecked(true);
},
_ => (),
}
} }
let ty = self.input_type();
match ty {
InputType::Checkbox => {
let was_checked = self.Checked();
let was_indeterminate = self.Indeterminate();
self.SetIndeterminate(false);
self.SetChecked(!was_checked);
return Some(InputActivationState {
checked: was_checked,
indeterminate: was_indeterminate,
checked_radio: None,
old_type: InputType::Checkbox,
});
},
InputType::Radio => {
let checked_member =
radio_group_iter(self, self.radio_group_name().as_ref()).find(|r| r.Checked());
let was_checked = self.Checked();
self.SetChecked(true);
return Some(InputActivationState {
checked: was_checked,
indeterminate: false,
checked_radio: checked_member.as_deref().map(DomRoot::from_ref),
old_type: InputType::Radio,
});
},
_ => (),
}
return None;
} }
// https://html.spec.whatwg.org/multipage/#run-canceled-activation-steps // https://dom.spec.whatwg.org/#eventtarget-legacy-canceled-activation-behavior
fn canceled_activation(&self) { fn legacy_canceled_activation_behavior(&self, cache: Option<InputActivationState>) {
let cache = self.activation_state.borrow(); // Step 1
let ty = self.input_type(); if !self.is_mutable() {
if cache.old_type != ty {
// Type changed, abandon ship
// https://www.w3.org/Bugs/Public/show_bug.cgi?id=27414
return; return;
} }
match ty { let ty = self.input_type();
// https://html.spec.whatwg.org/multipage/#submit-button-state-(type=submit):activation-behavior let cache = match cache {
// InputType::Submit => (), // No behavior defined Some(cache) => {
// https://html.spec.whatwg.org/multipage/#reset-button-state-(type=reset):activation-behavior if cache.old_type != ty {
// InputType::Reset => (), // No behavior defined // Type changed, abandon ship
// https://html.spec.whatwg.org/multipage/#checkbox-state-(type=checkbox):canceled-activation-steps // https://www.w3.org/Bugs/Public/show_bug.cgi?id=27414
InputType::Checkbox => { return;
// We want to restore state only if the element had been changed in the first place
if cache.was_mutable {
self.SetIndeterminate(cache.indeterminate);
self.SetChecked(cache.checked);
self.checked_changed.set(cache.checked_changed);
} }
cache
}, },
// https://html.spec.whatwg.org/multipage/#radio-button-state-(type=radio):canceled-activation-steps None => {
return;
},
};
match ty {
// Step 2
InputType::Checkbox => {
self.SetIndeterminate(cache.indeterminate);
self.SetChecked(cache.checked);
},
// Step 3
InputType::Radio => { InputType::Radio => {
// We want to restore state only if the element had been changed in the first place if let Some(ref o) = cache.checked_radio {
if cache.was_mutable { let tree_root = self
if let Some(ref o) = cache.checked_radio { .upcast::<Node>()
let tree_root = self .GetRootNode(&GetRootNodeOptions::empty());
.upcast::<Node>() // Avoiding iterating through the whole tree here, instead
.GetRootNode(&GetRootNodeOptions::empty()); // we can check if the conditions for radio group siblings apply
// Avoiding iterating through the whole tree here, instead if in_same_group(
// we can check if the conditions for radio group siblings apply &o,
if in_same_group( self.form_owner().as_deref(),
&o, self.radio_group_name().as_ref(),
self.form_owner().as_deref(), Some(&*tree_root),
self.radio_group_name().as_ref(), ) {
Some(&*tree_root), o.SetChecked(true);
) {
o.SetChecked(true);
} else {
self.SetChecked(false);
}
} else { } else {
self.SetChecked(false); self.SetChecked(false);
} }
self.checked_changed.set(cache.checked_changed); } else {
self.SetChecked(false);
} }
}, },
_ => (), _ => (),
@ -2459,11 +2435,6 @@ impl Activatable for HTMLInputElement {
// https://html.spec.whatwg.org/multipage/#run-post-click-activation-steps // https://html.spec.whatwg.org/multipage/#run-post-click-activation-steps
fn activation_behavior(&self, _event: &Event, _target: &EventTarget) { fn activation_behavior(&self, _event: &Event, _target: &EventTarget) {
let ty = self.input_type(); let ty = self.input_type();
if self.activation_state.borrow().old_type != ty || !self.is_mutable() {
// Type changed or input is immutable, abandon ship
// https://www.w3.org/Bugs/Public/show_bug.cgi?id=27414
return;
}
match ty { match ty {
InputType::Submit => { InputType::Submit => {
// https://html.spec.whatwg.org/multipage/#submit-button-state-(type=submit):activation-behavior // https://html.spec.whatwg.org/multipage/#submit-button-state-(type=submit):activation-behavior

View file

@ -2,10 +2,11 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use crate::dom::activation::{synthetic_click_activation, Activatable, ActivationSource}; use crate::dom::activation::Activatable;
use crate::dom::attr::Attr; use crate::dom::attr::Attr;
use crate::dom::bindings::codegen::Bindings::AttrBinding::AttrMethods; use crate::dom::bindings::codegen::Bindings::AttrBinding::AttrMethods;
use crate::dom::bindings::codegen::Bindings::ElementBinding::ElementMethods; use crate::dom::bindings::codegen::Bindings::ElementBinding::ElementMethods;
use crate::dom::bindings::codegen::Bindings::HTMLElementBinding::HTMLElementMethods;
use crate::dom::bindings::codegen::Bindings::HTMLLabelElementBinding; use crate::dom::bindings::codegen::Bindings::HTMLLabelElementBinding;
use crate::dom::bindings::codegen::Bindings::HTMLLabelElementBinding::HTMLLabelElementMethods; use crate::dom::bindings::codegen::Bindings::HTMLLabelElementBinding::HTMLLabelElementMethods;
use crate::dom::bindings::codegen::Bindings::NodeBinding::{GetRootNodeOptions, NodeMethods}; use crate::dom::bindings::codegen::Bindings::NodeBinding::{GetRootNodeOptions, NodeMethods};
@ -65,25 +66,14 @@ impl Activatable for HTMLLabelElement {
true true
} }
// https://html.spec.whatwg.org/multipage/#run-pre-click-activation-steps // https://html.spec.whatwg.org/multipage/#the-label-element:activation_behaviour
// https://html.spec.whatwg.org/multipage/#the-button-element:activation-behavior // Basically this is telling us that if activation bubbles up to the label
fn pre_click_activation(&self) {} // at all, we are free to do an implementation-dependent thing;
// firing a click event is an example, and the precise details of that
// https://html.spec.whatwg.org/multipage/#run-canceled-activation-steps // click event (e.g. isTrusted) are not specified.
fn canceled_activation(&self) {}
// https://html.spec.whatwg.org/multipage/#run-post-click-activation-steps
fn activation_behavior(&self, _event: &Event, _target: &EventTarget) { fn activation_behavior(&self, _event: &Event, _target: &EventTarget) {
if let Some(e) = self.GetControl() { if let Some(e) = self.GetControl() {
let elem = e.upcast::<Element>(); e.Click();
synthetic_click_activation(
elem,
false,
false,
false,
false,
ActivationSource::NotFromClick,
);
} }
} }
} }

View file

@ -37,6 +37,7 @@ use crate::dom::document::{Document, DocumentSource, HasBrowsingContext, IsHTMLD
use crate::dom::documentfragment::DocumentFragment; use crate::dom::documentfragment::DocumentFragment;
use crate::dom::documenttype::DocumentType; use crate::dom::documenttype::DocumentType;
use crate::dom::element::{CustomElementCreationMode, Element, ElementCreator}; use crate::dom::element::{CustomElementCreationMode, Element, ElementCreator};
use crate::dom::event::{Event, EventBubbles, EventCancelable};
use crate::dom::eventtarget::EventTarget; use crate::dom::eventtarget::EventTarget;
use crate::dom::globalscope::GlobalScope; use crate::dom::globalscope::GlobalScope;
use crate::dom::htmlbodyelement::HTMLBodyElement; use crate::dom::htmlbodyelement::HTMLBodyElement;
@ -51,6 +52,7 @@ use crate::dom::htmlmediaelement::{HTMLMediaElement, LayoutHTMLMediaElementHelpe
use crate::dom::htmlmetaelement::HTMLMetaElement; use crate::dom::htmlmetaelement::HTMLMetaElement;
use crate::dom::htmlstyleelement::HTMLStyleElement; use crate::dom::htmlstyleelement::HTMLStyleElement;
use crate::dom::htmltextareaelement::{HTMLTextAreaElement, LayoutHTMLTextAreaElementHelpers}; use crate::dom::htmltextareaelement::{HTMLTextAreaElement, LayoutHTMLTextAreaElementHelpers};
use crate::dom::mouseevent::MouseEvent;
use crate::dom::mutationobserver::{Mutation, MutationObserver, RegisteredObserver}; use crate::dom::mutationobserver::{Mutation, MutationObserver, RegisteredObserver};
use crate::dom::nodelist::NodeList; use crate::dom::nodelist::NodeList;
use crate::dom::processinginstruction::ProcessingInstruction; use crate::dom::processinginstruction::ProcessingInstruction;
@ -389,6 +391,46 @@ impl Node {
} }
}) })
} }
// https://html.spec.whatg.org/#fire_a_synthetic_mouse_event
pub fn fire_synthetic_mouse_event_not_trusted(&self, name: DOMString) {
// Spec says the choice of which global to create
// the mouse event on is not well-defined,
// and refers to heycam/webidl#135
let win = window_from_node(self);
let mouse_event = MouseEvent::new(
&win, // ambiguous in spec
name,
EventBubbles::Bubbles, // Step 3: bubbles
EventCancelable::Cancelable, // Step 3: cancelable,
Some(&win), // Step 7: view (this is unambiguous in spec)
0, // detail uninitialized
0, // coordinates uninitialized
0, // coordinates uninitialized
0, // coordinates uninitialized
0, // coordinates uninitialized
false,
false,
false,
false, // Step 6 modifier keys TODO compositor hook needed
0, // button uninitialized (and therefore left)
0, // buttons uninitialized (and therefore none)
None, // related_target uninitialized,
None, // point_in_target uninitialized,
);
// Step 4: TODO composed flag for shadow root
// Step 5
mouse_event.upcast::<Event>().set_trusted(false);
// Step 8: TODO keyboard modifiers
mouse_event
.upcast::<Event>()
.dispatch(self.upcast::<EventTarget>(), false);
}
} }
pub struct QuerySelectorIterator { pub struct QuerySelectorIterator {

View file

@ -33,7 +33,7 @@ use crate::dom::cssstyledeclaration::{CSSModificationAccess, CSSStyleDeclaration
use crate::dom::customelementregistry::CustomElementRegistry; use crate::dom::customelementregistry::CustomElementRegistry;
use crate::dom::document::{AnimationFrameCallback, Document}; use crate::dom::document::{AnimationFrameCallback, Document};
use crate::dom::element::Element; use crate::dom::element::Element;
use crate::dom::event::Event; use crate::dom::event::{Event, EventStatus};
use crate::dom::eventtarget::EventTarget; use crate::dom::eventtarget::EventTarget;
use crate::dom::globalscope::GlobalScope; use crate::dom::globalscope::GlobalScope;
use crate::dom::hashchangeevent::HashChangeEvent; use crate::dom::hashchangeevent::HashChangeEvent;
@ -528,6 +528,14 @@ impl Window {
pub fn get_event_loop_waker(&self) -> Option<Box<dyn EventLoopWaker>> { pub fn get_event_loop_waker(&self) -> Option<Box<dyn EventLoopWaker>> {
self.event_loop_waker.as_ref().map(|w| (*w).clone_box()) self.event_loop_waker.as_ref().map(|w| (*w).clone_box())
} }
// see note at https://dom.spec.whatwg.org/#concept-event-dispatch step 2
pub fn dispatch_event_with_target_override(&self, event: &Event) -> EventStatus {
if self.has_document() {
assert!(self.Document().can_invoke_script());
}
event.dispatch(self.upcast(), true)
}
} }
// https://html.spec.whatwg.org/multipage/#atob // https://html.spec.whatwg.org/multipage/#atob

View file

@ -1,20 +1,4 @@
[Event-dispatch-click.html] [Event-dispatch-click.html]
type: testharness type: testharness
expected: TIMEOUT
[basic with dispatchEvent()]
expected: FAIL
[look at parents when event bubbles]
expected: FAIL
[pick the first with activation behavior <input type=checkbox>]
expected: FAIL
[pick the first with activation behavior <a href>]
expected: TIMEOUT
[event state during post-click handling] [event state during post-click handling]
expected: TIMEOUT expected: FAIL
[redispatch during post-click handling]
expected: TIMEOUT

View file

@ -1,4 +0,0 @@
[Event-dispatch-handlers-changed.html]
[ Dispatch additional events inside an event listener ]
expected: FAIL

View file

@ -1,4 +0,0 @@
[Event-dispatch-order-at-target.html]
[Listeners are invoked in correct order (AT_TARGET phase)]
expected: FAIL

View file

@ -30,6 +30,4 @@
[If the event's initialized flag is not set, an InvalidStateError must be thrown (WheelEvent).] [If the event's initialized flag is not set, an InvalidStateError must be thrown (WheelEvent).]
expected: FAIL expected: FAIL
[Capturing event listeners should be called before non-capturing ones]
expected: FAIL

View file

@ -1,12 +0,0 @@
[button-click-submits.html]
type: testharness
expected: TIMEOUT
[clicking a button by dispatching an event should trigger a submit (form connected)]
expected: TIMEOUT
[clicking the child of a button by dispatching a bubbling event should trigger a submit]
expected: TIMEOUT
[clicking the child of a button with .click() should trigger a submit]
expected: TIMEOUT

View file

@ -1,11 +0,0 @@
[checkbox-click-events.html]
type: testharness
[clicking and preventDefaulting a checkbox causes the checkbox to be checked during the click handler but reverted]
expected: FAIL
[a checkbox input emits click, input, change events in order after dispatching click event]
expected: FAIL
[checkbox input respects cancel behavior on synthetic clicks]
expected: FAIL

View file

@ -1,5 +0,0 @@
[checkbox-detached-change-event.html]
expected: TIMEOUT
[This test will pass if <input type=checkbox> emits change events while detached from document.body]
expected: TIMEOUT

View file

@ -1,8 +0,0 @@
[checkbox.html]
type: testharness
[canceled activation steps on unchecked checkbox]
expected: FAIL
[canceled activation steps on unchecked checkbox (indeterminate=true in onclick)]
expected: FAIL

View file

@ -1,5 +0,0 @@
[radio-detached-change-event.html]
expected: TIMEOUT
[This test will pass if <input type=radio> emits change events while detached from document.body]
expected: TIMEOUT

View file

@ -1,5 +0,0 @@
[radio-input-cancel.html]
type: testharness
[radio input cancel behavior reverts state]
expected: FAIL

View file

@ -1,5 +0,0 @@
[proxy-click-to-associated-element.html]
type: testharness
[clicking a label that prevents the event's default should not proxy click events]
expected: FAIL

View file

@ -1,5 +0,0 @@
[task_microtask_ordering.html]
expected: TIMEOUT
[Level 1 bossfight (synthetic click)]
expected: TIMEOUT

View file

@ -1,5 +0,0 @@
[dispatchEvent.click.checkbox.html]
type: testharness
[Test Description: MouseEvent: Default action is performed when a synthetic click event is dispatched on a checkbox element]
expected: FAIL

View file

@ -18848,7 +18848,7 @@
"testharness" "testharness"
], ],
"mozilla/event_dispatch_order.html": [ "mozilla/event_dispatch_order.html": [
"48513cfff42b8635eb8822a903e7e85250a7ac51", "172ae368c707e695fc334df491d62c44dfb81566",
"testharness" "testharness"
], ],
"mozilla/event_handler_syntax_error.html": [ "mozilla/event_handler_syntax_error.html": [

View file

@ -1,5 +1,6 @@
<html> <html>
<head> <head>
<title>Even in the AT_TARGET phase, capture handlers fire before bubble handlers.</title>
<script src="/resources/testharness.js"></script> <script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script> <script src="/resources/testharnessreport.js"></script>
</head> </head>
@ -7,25 +8,33 @@
<div id="foo"></div> <div id="foo"></div>
<script> <script>
test(function() { test(function() {
var sawBubble = false; var sawBubble = false;
var sawCapture = false; var sawCapture = false;
var sawBubbleTwice = false; var sawBubbleTwice = false;
function handler(ev) { function handler(ev) {
// Added first, but it's a bubble so it shouldn't fire until
// after the capture.
assert_equals(ev.eventPhase, ev.AT_TARGET); assert_equals(ev.eventPhase, ev.AT_TARGET);
assert_equals(sawCapture, true);
assert_equals(sawBubble, false); assert_equals(sawBubble, false);
assert_equals(sawCapture, false); assert_equals(sawBubbleTwice, false);
sawBubble = true; sawBubble = true;
} }
function handler2(ev) { function handler2(ev) {
// Capture: this should fire before both bubbles
assert_equals(ev.eventPhase, ev.AT_TARGET); assert_equals(ev.eventPhase, ev.AT_TARGET);
assert_equals(sawBubble, true);
assert_equals(sawCapture, false); assert_equals(sawCapture, false);
assert_equals(sawBubble, false);
assert_equals(sawBubbleTwice, false);
sawCapture = true; sawCapture = true;
} }
function handler3(ev) { function handler3(ev) {
// And this one fires last.
assert_equals(ev.eventPhase, ev.AT_TARGET); assert_equals(ev.eventPhase, ev.AT_TARGET);
assert_equals(sawBubble, true);
assert_equals(sawCapture, true); assert_equals(sawCapture, true);
assert_equals(sawBubble, true);
assert_equals(sawBubbleTwice, false);
sawBubbleTwice = true; sawBubbleTwice = true;
} }