From 3782163b6654b20460b357ff2426e82ee2c06c37 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 16 Mar 2026 14:28:57 -0500 Subject: [PATCH 01/15] fix autofill events --- packages/web/src/events/mod.rs | 40 +++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/packages/web/src/events/mod.rs b/packages/web/src/events/mod.rs index cb56594717..13de848c1f 100644 --- a/packages/web/src/events/mod.rs +++ b/packages/web/src/events/mod.rs @@ -45,6 +45,38 @@ impl Synthetic { pub(crate) struct WebEventConverter; +/// Fallback for when a "keydown"/"keyup" event is not actually a KeyboardEvent +/// (e.g. datalist autocomplete dispatches a plain Event with type "keydown"). +/// Wraps the raw web_sys::Event so it remains accessible via `as_any`/downcast. +struct GenericKeyboardEvent(web_sys::Event); + +impl dioxus_html::HasKeyboardData for GenericKeyboardEvent { + fn key(&self) -> dioxus_html::Key { + dioxus_html::Key::Unidentified + } + fn code(&self) -> dioxus_html::Code { + dioxus_html::Code::Unidentified + } + fn location(&self) -> dioxus_html::Location { + dioxus_html::Location::Standard + } + fn is_auto_repeating(&self) -> bool { + false + } + fn is_composing(&self) -> bool { + false + } + fn as_any(&self) -> &dyn std::any::Any { + &self.0 + } +} + +impl dioxus_html::ModifiersInteraction for GenericKeyboardEvent { + fn modifiers(&self) -> dioxus_html::Modifiers { + dioxus_html::Modifiers::empty() + } +} + #[inline(always)] fn downcast_event(event: &dioxus_html::PlatformEventData) -> &GenericWebSysEvent { event @@ -116,7 +148,13 @@ impl HtmlEventConverter for WebEventConverter { &self, event: &dioxus_html::PlatformEventData, ) -> dioxus_html::KeyboardData { - Synthetic::::from(downcast_event(event).raw.clone()).into() + let raw = &downcast_event(event).raw; + // Datalist autocomplete can dispatch a plain Event with type "keydown" + // that isn't actually a KeyboardEvent. Fall back to defaults. + match raw.dyn_ref::() { + Some(kb) => Synthetic::new(kb.clone()).into(), + None => dioxus_html::KeyboardData::new(GenericKeyboardEvent(raw.clone())), + } } #[inline(always)] From 6e2848cc541ad0ae9f956e3c57dae16417fb8e4f Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Tue, 17 Mar 2026 10:06:18 -0500 Subject: [PATCH 02/15] ignore the wrong type of event like we do in desktop --- Cargo.toml | 5 +++++ packages/web/src/dom.rs | 12 ++++++++++ packages/web/src/events/mod.rs | 40 +--------------------------------- 3 files changed, 18 insertions(+), 39 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1739b85c91..ab61ef7068 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -937,6 +937,11 @@ name = "on_visible" path = "examples/08-apis/on_visible.rs" doc-scrape-examples = true +[[example]] +name = "datalist_autocomplete" +path = "examples/09-reference/datalist_autocomplete.rs" +doc-scrape-examples = true + [[example]] name = "all_events" path = "examples/09-reference/all_events.rs" diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index df2cfb73e8..eb72569186 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -94,6 +94,18 @@ impl WebsysDom { return; }; + // Datalist autocomplete can dispatch a plain Event with type + // "keydown" that isn't actually a KeyboardEvent. Silently drop + // it — the event lacks keyboard properties and will fail to + // deserialize on other platforms too. + if matches!(name.as_str(), "keydown" | "keyup" | "keypress") + && web_sys_event + .dyn_ref::() + .is_none() + { + return; + } + let data = virtual_event_from_websys_event(web_sys_event.clone(), target); let event = dioxus_core::Event::new(Rc::new(data) as Rc, bubbles); diff --git a/packages/web/src/events/mod.rs b/packages/web/src/events/mod.rs index 13de848c1f..cb56594717 100644 --- a/packages/web/src/events/mod.rs +++ b/packages/web/src/events/mod.rs @@ -45,38 +45,6 @@ impl Synthetic { pub(crate) struct WebEventConverter; -/// Fallback for when a "keydown"/"keyup" event is not actually a KeyboardEvent -/// (e.g. datalist autocomplete dispatches a plain Event with type "keydown"). -/// Wraps the raw web_sys::Event so it remains accessible via `as_any`/downcast. -struct GenericKeyboardEvent(web_sys::Event); - -impl dioxus_html::HasKeyboardData for GenericKeyboardEvent { - fn key(&self) -> dioxus_html::Key { - dioxus_html::Key::Unidentified - } - fn code(&self) -> dioxus_html::Code { - dioxus_html::Code::Unidentified - } - fn location(&self) -> dioxus_html::Location { - dioxus_html::Location::Standard - } - fn is_auto_repeating(&self) -> bool { - false - } - fn is_composing(&self) -> bool { - false - } - fn as_any(&self) -> &dyn std::any::Any { - &self.0 - } -} - -impl dioxus_html::ModifiersInteraction for GenericKeyboardEvent { - fn modifiers(&self) -> dioxus_html::Modifiers { - dioxus_html::Modifiers::empty() - } -} - #[inline(always)] fn downcast_event(event: &dioxus_html::PlatformEventData) -> &GenericWebSysEvent { event @@ -148,13 +116,7 @@ impl HtmlEventConverter for WebEventConverter { &self, event: &dioxus_html::PlatformEventData, ) -> dioxus_html::KeyboardData { - let raw = &downcast_event(event).raw; - // Datalist autocomplete can dispatch a plain Event with type "keydown" - // that isn't actually a KeyboardEvent. Fall back to defaults. - match raw.dyn_ref::() { - Some(kb) => Synthetic::new(kb.clone()).into(), - None => dioxus_html::KeyboardData::new(GenericKeyboardEvent(raw.clone())), - } + Synthetic::::from(downcast_event(event).raw.clone()).into() } #[inline(always)] From aa2b384a4c4dacea365f7ac55846e5171c58b084 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Tue, 17 Mar 2026 10:49:17 -0500 Subject: [PATCH 03/15] check the event type before dispatch instead of panicing on downcast --- .../09-reference/datalist_autocomplete.rs | 90 +++++++++++++++++++ packages/web/src/dom.rs | 61 +++++++++++-- 2 files changed, 142 insertions(+), 9 deletions(-) create mode 100644 examples/09-reference/datalist_autocomplete.rs diff --git a/examples/09-reference/datalist_autocomplete.rs b/examples/09-reference/datalist_autocomplete.rs new file mode 100644 index 0000000000..700ad7fe49 --- /dev/null +++ b/examples/09-reference/datalist_autocomplete.rs @@ -0,0 +1,90 @@ +//! Regression test for datalist autocomplete + keyboard events. +//! +//! Browsers (and desktop webviews) may dispatch a plain `Event` with type "keydown" +//! when the user selects a datalist suggestion. Because it is not a real `KeyboardEvent`, +//! it lacks properties like `key`, `code`, and `keyCode`. Without proper handling, this +//! causes a panic (BorrowMutError or deserialization failure). +//! +//! To test: +//! 1. Run this example with `dx serve --example datalist_autocomplete` (web) or +//! `dx serve --example datalist_autocomplete --platform desktop` (desktop). +//! 2. Click on the input field and type "a". +//! 3. A datalist suggestion "apple" should appear. +//! 4. Click on the suggestion to select it. +//! 5. The app should NOT crash. The event log should show the events that fired. +//! +//! See https://github.com/DioxusLabs/dioxus/issues/5375 + +use dioxus::prelude::*; +use std::collections::VecDeque; + +fn main() { + dioxus::launch(app); +} + +fn app() -> Element { + let mut events = use_signal(VecDeque::::new); + + let mut log = move |msg: String| { + let mut ev = events.write(); + if ev.len() >= 30 { + ev.pop_front(); + } + ev.push_back(msg); + }; + + rsx! { + div { style: "font-family: sans-serif; max-width: 600px; margin: 40px auto;", + h2 { "Datalist autocomplete test" } + p { "Type \"a\" in the input below and select a suggestion from the dropdown." } + p { "The app should not crash when you pick a suggestion." } + + div { + style: "padding: 16px; border: 1px solid #ccc; border-radius: 8px; margin-bottom: 16px;", + onkeydown: move |e: KeyboardEvent| { + log(format!("keydown: key={:?} code={:?}", e.key(), e.code())); + }, + onkeyup: move |e: KeyboardEvent| { + log(format!("keyup: key={:?} code={:?}", e.key(), e.code())); + }, + oninput: move |e: FormEvent| { + log(format!("input: value={:?}", e.value())); + }, + onchange: move |e: FormEvent| { + log(format!("change: value={:?}", e.value())); + }, + + label { r#for: "fruit", "Pick a fruit: " } + input { + id: "fruit", + list: "fruit-list", + placeholder: "Start typing...", + style: "padding: 8px; font-size: 16px; width: 100%;", + } + datalist { id: "fruit-list", + option { value: "apple" } + option { value: "apricot" } + option { value: "avocado" } + option { value: "banana" } + option { value: "blueberry" } + option { value: "cherry" } + option { value: "grape" } + option { value: "orange" } + option { value: "peach" } + option { value: "strawberry" } + } + } + + h3 { "Event log" } + div { + style: "background: #f5f5f5; padding: 12px; border-radius: 8px; font-family: monospace; font-size: 13px; max-height: 300px; overflow-y: auto;", + if events.read().is_empty() { + p { style: "color: #999;", "No events yet..." } + } + for (i, event) in events.read().iter().enumerate() { + div { key: "{i}", "{event}" } + } + } + } + } +} diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index eb72569186..e689645d90 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -94,15 +94,11 @@ impl WebsysDom { return; }; - // Datalist autocomplete can dispatch a plain Event with type - // "keydown" that isn't actually a KeyboardEvent. Silently drop - // it — the event lacks keyboard properties and will fail to - // deserialize on other platforms too. - if matches!(name.as_str(), "keydown" | "keyup" | "keypress") - && web_sys_event - .dyn_ref::() - .is_none() - { + // Some browser features (e.g. datalist autocomplete) dispatch + // a plain Event with a typed name like "keydown" that isn't + // actually a KeyboardEvent. Drop events whose JS type doesn't + // match what the converters will unchecked-cast to. + if !event_type_matches(name.as_str(), web_sys_event) { return; } @@ -186,3 +182,50 @@ fn walk_element_for_id(target: &Node) -> Option<(ElementId, web_sys::Element)> { } } } + +/// Check that the JS event object actually inherits from the interface the +/// converters will `unchecked_into`. Events that just wrap the raw `Event` +/// (cancel, clipboard, form, media, scroll, selection, toggle, image) don't +/// need checking because no typed cast is performed. +fn event_type_matches(name: &str, event: &web_sys::Event) -> bool { + match name { + "keydown" | "keyup" | "keypress" => event.is_instance_of::(), + + "click" | "contextmenu" | "dblclick" | "doubleclick" | "mousedown" | "mouseenter" + | "mouseleave" | "mousemove" | "mouseout" | "mouseover" | "mouseup" => { + event.is_instance_of::() + } + + "compositionend" | "compositionstart" | "compositionupdate" => { + event.is_instance_of::() + } + + "blur" | "focus" | "focusin" | "focusout" => { + event.is_instance_of::() + } + + "drag" | "dragend" | "dragenter" | "dragexit" | "dragleave" | "dragover" + | "dragstart" | "drop" => event.is_instance_of::(), + + "pointerdown" | "pointermove" | "pointerup" | "pointerover" | "pointerout" + | "pointerenter" | "pointerleave" | "gotpointercapture" | "lostpointercapture" + | "pointerlockchange" | "pointerlockerror" | "auxclick" => { + event.is_instance_of::() + } + + "touchcancel" | "touchend" | "touchmove" | "touchstart" => { + event.is_instance_of::() + } + + "wheel" => event.is_instance_of::(), + + "animationstart" | "animationend" | "animationiteration" => { + event.is_instance_of::() + } + + "transitionend" => event.is_instance_of::(), + + // All other events use the raw Event — no typed cast, no check needed. + _ => true, + } +} From 5809b35ec1c4ffacc09dcff24ffb06d431e9bf9f Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 18 Mar 2026 10:37:09 -0500 Subject: [PATCH 04/15] clean up conversion --- .../09-reference/datalist_autocomplete.rs | 12 +- packages/web/src/dom.rs | 51 +-- packages/web/src/events/clipboard.rs | 6 - packages/web/src/events/mod.rs | 317 ++++++++---------- 4 files changed, 158 insertions(+), 228 deletions(-) diff --git a/examples/09-reference/datalist_autocomplete.rs b/examples/09-reference/datalist_autocomplete.rs index 700ad7fe49..ec97b8ac6d 100644 --- a/examples/09-reference/datalist_autocomplete.rs +++ b/examples/09-reference/datalist_autocomplete.rs @@ -47,12 +47,6 @@ fn app() -> Element { onkeyup: move |e: KeyboardEvent| { log(format!("keyup: key={:?} code={:?}", e.key(), e.code())); }, - oninput: move |e: FormEvent| { - log(format!("input: value={:?}", e.value())); - }, - onchange: move |e: FormEvent| { - log(format!("change: value={:?}", e.value())); - }, label { r#for: "fruit", "Pick a fruit: " } input { @@ -60,6 +54,12 @@ fn app() -> Element { list: "fruit-list", placeholder: "Start typing...", style: "padding: 8px; font-size: 16px; width: 100%;", + oninput: move |e: FormEvent| { + log(format!("input: value={:?}", e.value())); + }, + onchange: move |e: FormEvent| { + log(format!("change: value={:?}", e.value())); + }, } datalist { id: "fruit-list", option { value: "apple" } diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index e689645d90..6e61ca530f 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -15,7 +15,9 @@ use rustc_hash::FxHashMap; use wasm_bindgen::{closure::Closure, JsCast}; use web_sys::{Document, Event, Node}; -use crate::{load_document, virtual_event_from_websys_event, Config, WebEventConverter}; +use crate::{ + event_type_matches, load_document, virtual_event_from_websys_event, Config, WebEventConverter, +}; pub struct WebsysDom { #[allow(dead_code)] @@ -182,50 +184,3 @@ fn walk_element_for_id(target: &Node) -> Option<(ElementId, web_sys::Element)> { } } } - -/// Check that the JS event object actually inherits from the interface the -/// converters will `unchecked_into`. Events that just wrap the raw `Event` -/// (cancel, clipboard, form, media, scroll, selection, toggle, image) don't -/// need checking because no typed cast is performed. -fn event_type_matches(name: &str, event: &web_sys::Event) -> bool { - match name { - "keydown" | "keyup" | "keypress" => event.is_instance_of::(), - - "click" | "contextmenu" | "dblclick" | "doubleclick" | "mousedown" | "mouseenter" - | "mouseleave" | "mousemove" | "mouseout" | "mouseover" | "mouseup" => { - event.is_instance_of::() - } - - "compositionend" | "compositionstart" | "compositionupdate" => { - event.is_instance_of::() - } - - "blur" | "focus" | "focusin" | "focusout" => { - event.is_instance_of::() - } - - "drag" | "dragend" | "dragenter" | "dragexit" | "dragleave" | "dragover" - | "dragstart" | "drop" => event.is_instance_of::(), - - "pointerdown" | "pointermove" | "pointerup" | "pointerover" | "pointerout" - | "pointerenter" | "pointerleave" | "gotpointercapture" | "lostpointercapture" - | "pointerlockchange" | "pointerlockerror" | "auxclick" => { - event.is_instance_of::() - } - - "touchcancel" | "touchend" | "touchmove" | "touchstart" => { - event.is_instance_of::() - } - - "wheel" => event.is_instance_of::(), - - "animationstart" | "animationend" | "animationiteration" => { - event.is_instance_of::() - } - - "transitionend" => event.is_instance_of::(), - - // All other events use the raw Event — no typed cast, no check needed. - _ => true, - } -} diff --git a/packages/web/src/events/clipboard.rs b/packages/web/src/events/clipboard.rs index fa243c5063..9243ad23bd 100644 --- a/packages/web/src/events/clipboard.rs +++ b/packages/web/src/events/clipboard.rs @@ -3,12 +3,6 @@ use web_sys::Event; use super::{Synthetic, WebEventExt}; -impl From<&Event> for Synthetic { - fn from(e: &Event) -> Self { - Synthetic::new(e.clone()) - } -} - impl HasClipboardData for Synthetic { fn as_any(&self) -> &dyn std::any::Any { &self.event diff --git a/packages/web/src/events/mod.rs b/packages/web/src/events/mod.rs index cb56594717..ff65034159 100644 --- a/packages/web/src/events/mod.rs +++ b/packages/web/src/events/mod.rs @@ -52,81 +52,123 @@ fn downcast_event(event: &dioxus_html::PlatformEventData) -> &GenericWebSysEvent .expect("event should be a GenericWebSysEvent") } -impl HtmlEventConverter for WebEventConverter { - #[inline(always)] - fn convert_animation_data( - &self, - event: &dioxus_html::PlatformEventData, - ) -> dioxus_html::AnimationData { - Synthetic::::from(downcast_event(event).raw.clone()).into() - } +/// Single source of truth for event-name → web_sys type mappings. Generates +/// `event_type_matches()` and the `HtmlEventConverter` impl for `WebEventConverter`. +/// +/// Each event entry takes one of two forms: +/// +/// Default conversion: +/// ```ignore +/// #[events = name, ...] +/// #[event_type = web_sys::Type] +/// fn converter(event: &PlatformEventData) -> ReturnType; +/// ``` +/// +/// Custom conversion: +/// ```ignore +/// #[events = name, ...] +/// #[event_type = web_sys::Type] +/// fn converter(event: &PlatformEventData) -> ReturnType { body } +/// ``` +macro_rules! web_events { + ( + $( + #[events = $($name:ident),+] + #[event_type = $ws:ty] + fn $conv:ident ( $evt:ident : $evt_ty:ty ) -> $ret:ty $( $body:block )?; + )+ + ) => { + pub(crate) fn event_type_matches(name: &str, event: &web_sys::Event) -> bool { + let m = match name { + $( $(stringify!($name))|+ => event.is_instance_of::<$ws>(), )+ + _ => true, + }; + if !m { + tracing::warn!("Ignoring \"{name}\": not the expected type: {event:?}"); + } + m + } - #[inline(always)] - fn convert_cancel_data( - &self, - event: &dioxus_html::PlatformEventData, - ) -> dioxus_html::CancelData { - Synthetic::new(downcast_event(event).raw.clone()).into() - } + impl HtmlEventConverter for WebEventConverter { + $( web_events!(@method $ws, $conv, $evt -> $ret $(, $body)?); )+ + } + }; - #[inline(always)] - fn convert_clipboard_data( - &self, - event: &dioxus_html::PlatformEventData, - ) -> dioxus_html::ClipboardData { - Synthetic::new(downcast_event(event).raw.clone()).into() - } + // Default conversion: construct Synthetic directly via unchecked_into + (@method $ws:ty, $conv:ident, $evt:ident -> $ret:ty) => { + #[inline(always)] + fn $conv(&self, $evt: &PlatformEventData) -> $ret { + Synthetic::new(downcast_event($evt).raw.clone().unchecked_into::<$ws>()).into() + } + }; - #[inline(always)] - fn convert_composition_data( - &self, - event: &dioxus_html::PlatformEventData, - ) -> dioxus_html::CompositionData { - Synthetic::::from(downcast_event(event).raw.clone()).into() - } + // Custom conversion body + (@method $ws:ty, $conv:ident, $evt:ident -> $ret:ty, $body:block) => { + #[inline(always)] + fn $conv(&self, $evt: &PlatformEventData) -> $ret $body + }; +} - #[inline(always)] - fn convert_drag_data(&self, event: &dioxus_html::PlatformEventData) -> dioxus_html::DragData { +web_events! { + #[events = animationstart, animationend, animationiteration] + #[event_type = web_sys::AnimationEvent] + fn convert_animation_data(event: &PlatformEventData) -> dioxus_html::AnimationData; + + #[events = cancel] + #[event_type = web_sys::Event] + fn convert_cancel_data(event: &PlatformEventData) -> dioxus_html::CancelData; + + #[events = copy, cut, paste] + #[event_type = web_sys::Event] + fn convert_clipboard_data(event: &PlatformEventData) -> dioxus_html::ClipboardData; + + #[events = compositionend, compositionstart, compositionupdate] + #[event_type = web_sys::CompositionEvent] + fn convert_composition_data(event: &PlatformEventData) -> dioxus_html::CompositionData; + + #[events = drag, dragend, dragenter, dragexit, dragleave, + dragover, dragstart, drop] + #[event_type = web_sys::DragEvent] + fn convert_drag_data(event: &PlatformEventData) -> DragData { let event = downcast_event(event); DragData::new(Synthetic::new( event.raw.clone().unchecked_into::(), )) - } + }; - #[inline(always)] - fn convert_focus_data(&self, event: &dioxus_html::PlatformEventData) -> dioxus_html::FocusData { - Synthetic::::from(downcast_event(event).raw.clone()).into() - } + #[events = blur, focus, focusin, focusout] + #[event_type = web_sys::FocusEvent] + fn convert_focus_data(event: &PlatformEventData) -> dioxus_html::FocusData; - #[inline(always)] - fn convert_form_data(&self, event: &dioxus_html::PlatformEventData) -> dioxus_html::FormData { + #[events = change, input, invalid, reset, submit] + #[event_type = web_sys::Event] + fn convert_form_data(event: &PlatformEventData) -> FormData { let event = downcast_event(event); FormData::new(WebFormData::new(event.element.clone(), event.raw.clone())) - } + }; - #[inline(always)] - fn convert_image_data(&self, event: &dioxus_html::PlatformEventData) -> dioxus_html::ImageData { + #[events = error, load] + #[event_type = web_sys::Event] + fn convert_image_data(event: &PlatformEventData) -> ImageData { let event = downcast_event(event); - let error = event.raw.type_() == "error"; - ImageData::new(WebImageEvent::new(event.raw.clone(), error)) - } - - #[inline(always)] - fn convert_keyboard_data( - &self, - event: &dioxus_html::PlatformEventData, - ) -> dioxus_html::KeyboardData { - Synthetic::::from(downcast_event(event).raw.clone()).into() - } - - #[inline(always)] - fn convert_media_data(&self, event: &dioxus_html::PlatformEventData) -> dioxus_html::MediaData { - Synthetic::new(downcast_event(event).raw.clone()).into() - } + ImageData::new(WebImageEvent::new(event.raw.clone(), event.raw.type_() == "error")) + }; - #[allow(unused_variables)] - #[inline(always)] - fn convert_mounted_data(&self, event: &dioxus_html::PlatformEventData) -> MountedData { + #[events = keydown, keyup, keypress] + #[event_type = web_sys::KeyboardEvent] + fn convert_keyboard_data(event: &PlatformEventData) -> dioxus_html::KeyboardData; + + #[events = abort, canplay, canplaythrough, durationchange, emptied, + encrypted, ended, loadeddata, loadedmetadata, loadstart, + pause, play, playing, progress, ratechange, seeked, + seeking, stalled, suspend, timeupdate, volumechange, + waiting] + #[event_type = web_sys::Event] + fn convert_media_data(event: &PlatformEventData) -> dioxus_html::MediaData; + + #[events = mounted] + #[event_type = web_sys::Element] + fn convert_mounted_data(event: &PlatformEventData) -> MountedData { #[cfg(feature = "mounted")] { Synthetic::new( @@ -139,81 +181,55 @@ impl HtmlEventConverter for WebEventConverter { } #[cfg(not(feature = "mounted"))] { - panic!("mounted events are not supported without the mounted feature on the dioxus-web crate enabled") + let _ = event; + panic!("mounted events require the `mounted` feature on dioxus-web") } - } - - #[inline(always)] - fn convert_mouse_data(&self, event: &dioxus_html::PlatformEventData) -> dioxus_html::MouseData { - Synthetic::::from(downcast_event(event).raw.clone()).into() - } - - #[inline(always)] - fn convert_pointer_data( - &self, - event: &dioxus_html::PlatformEventData, - ) -> dioxus_html::PointerData { - Synthetic::::from(downcast_event(event).raw.clone()).into() - } - - #[inline(always)] - fn convert_resize_data( - &self, - event: &dioxus_html::PlatformEventData, - ) -> dioxus_html::ResizeData { - Synthetic::::from(downcast_event(event).raw.clone()).into() - } - - #[inline(always)] - fn convert_scroll_data( - &self, - event: &dioxus_html::PlatformEventData, - ) -> dioxus_html::ScrollData { - Synthetic::new(downcast_event(event).raw.clone()).into() - } - - #[inline(always)] - fn convert_selection_data( - &self, - event: &dioxus_html::PlatformEventData, - ) -> dioxus_html::SelectionData { - Synthetic::new(downcast_event(event).raw.clone()).into() - } - - #[inline(always)] - fn convert_toggle_data( - &self, - event: &dioxus_html::PlatformEventData, - ) -> dioxus_html::ToggleData { - Synthetic::new(downcast_event(event).raw.clone()).into() - } - - #[inline(always)] - fn convert_touch_data(&self, event: &dioxus_html::PlatformEventData) -> dioxus_html::TouchData { - Synthetic::::from(downcast_event(event).raw.clone()).into() - } - - #[inline(always)] - fn convert_transition_data( - &self, - event: &dioxus_html::PlatformEventData, - ) -> dioxus_html::TransitionData { - Synthetic::::from(downcast_event(event).raw.clone()).into() - } - - #[inline(always)] - fn convert_visible_data( - &self, - event: &dioxus_html::PlatformEventData, - ) -> dioxus_html::VisibleData { - Synthetic::::from(downcast_event(event).raw.clone()) - .into() - } + }; - #[inline(always)] - fn convert_wheel_data(&self, event: &dioxus_html::PlatformEventData) -> dioxus_html::WheelData { - Synthetic::::from(downcast_event(event).raw.clone()).into() - } + #[events = click, contextmenu, dblclick, doubleclick, + mousedown, mouseenter, mouseleave, mousemove, + mouseout, mouseover, mouseup] + #[event_type = web_sys::MouseEvent] + fn convert_mouse_data(event: &PlatformEventData) -> dioxus_html::MouseData; + + #[events = pointerdown, pointermove, pointerup, pointerover, + pointerout, pointerenter, pointerleave, + gotpointercapture, lostpointercapture, + pointerlockchange, pointerlockerror, auxclick] + #[event_type = web_sys::PointerEvent] + fn convert_pointer_data(event: &PlatformEventData) -> dioxus_html::PointerData; + + #[events = resize] + #[event_type = web_sys::ResizeObserverEntry] + fn convert_resize_data(event: &PlatformEventData) -> dioxus_html::ResizeData; + + #[events = scroll, scrollend] + #[event_type = web_sys::Event] + fn convert_scroll_data(event: &PlatformEventData) -> dioxus_html::ScrollData; + + #[events = select, selectstart, selectionchange] + #[event_type = web_sys::Event] + fn convert_selection_data(event: &PlatformEventData) -> dioxus_html::SelectionData; + + #[events = toggle, beforetoggle] + #[event_type = web_sys::Event] + fn convert_toggle_data(event: &PlatformEventData) -> dioxus_html::ToggleData; + + #[events = touchcancel, touchend, touchmove, touchstart] + #[event_type = web_sys::TouchEvent] + fn convert_touch_data(event: &PlatformEventData) -> dioxus_html::TouchData; + + #[events = transitionend] + #[event_type = web_sys::TransitionEvent] + fn convert_transition_data(event: &PlatformEventData) -> dioxus_html::TransitionData; + + #[events = visible] + #[event_type = web_sys::IntersectionObserverEntry] + fn convert_visible_data(event: &PlatformEventData) -> dioxus_html::VisibleData; + + #[events = wheel] + #[event_type = web_sys::WheelEvent] + fn convert_wheel_data(event: &PlatformEventData) -> dioxus_html::WheelData; } /// A extension trait for web-sys events that provides a way to get the event as a web-sys event. @@ -262,38 +278,3 @@ pub(crate) fn load_document() -> Document { .document() .expect("should have access to the Document") } - -macro_rules! uncheck_convert { - ($t:ty) => { - impl From for Synthetic<$t> { - #[inline] - fn from(e: Event) -> Self { - let e: $t = e.unchecked_into(); - Self::new(e) - } - } - - impl From<&Event> for Synthetic<$t> { - #[inline] - fn from(e: &Event) -> Self { - let e: &$t = e.unchecked_ref(); - Self::new(e.clone()) - } - } - }; - ($($t:ty),+ $(,)?) => { - $(uncheck_convert!($t);)+ - }; -} - -uncheck_convert![ - web_sys::CompositionEvent, - web_sys::KeyboardEvent, - web_sys::TouchEvent, - web_sys::PointerEvent, - web_sys::WheelEvent, - web_sys::AnimationEvent, - web_sys::TransitionEvent, - web_sys::MouseEvent, - web_sys::FocusEvent, -]; From 7b614b52e449792d5e76e2b4e04df3d47b898b0b Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 18 Mar 2026 12:32:52 -0500 Subject: [PATCH 05/15] remove example --- .../09-reference/datalist_autocomplete.rs | 90 ------------------- 1 file changed, 90 deletions(-) delete mode 100644 examples/09-reference/datalist_autocomplete.rs diff --git a/examples/09-reference/datalist_autocomplete.rs b/examples/09-reference/datalist_autocomplete.rs deleted file mode 100644 index ec97b8ac6d..0000000000 --- a/examples/09-reference/datalist_autocomplete.rs +++ /dev/null @@ -1,90 +0,0 @@ -//! Regression test for datalist autocomplete + keyboard events. -//! -//! Browsers (and desktop webviews) may dispatch a plain `Event` with type "keydown" -//! when the user selects a datalist suggestion. Because it is not a real `KeyboardEvent`, -//! it lacks properties like `key`, `code`, and `keyCode`. Without proper handling, this -//! causes a panic (BorrowMutError or deserialization failure). -//! -//! To test: -//! 1. Run this example with `dx serve --example datalist_autocomplete` (web) or -//! `dx serve --example datalist_autocomplete --platform desktop` (desktop). -//! 2. Click on the input field and type "a". -//! 3. A datalist suggestion "apple" should appear. -//! 4. Click on the suggestion to select it. -//! 5. The app should NOT crash. The event log should show the events that fired. -//! -//! See https://github.com/DioxusLabs/dioxus/issues/5375 - -use dioxus::prelude::*; -use std::collections::VecDeque; - -fn main() { - dioxus::launch(app); -} - -fn app() -> Element { - let mut events = use_signal(VecDeque::::new); - - let mut log = move |msg: String| { - let mut ev = events.write(); - if ev.len() >= 30 { - ev.pop_front(); - } - ev.push_back(msg); - }; - - rsx! { - div { style: "font-family: sans-serif; max-width: 600px; margin: 40px auto;", - h2 { "Datalist autocomplete test" } - p { "Type \"a\" in the input below and select a suggestion from the dropdown." } - p { "The app should not crash when you pick a suggestion." } - - div { - style: "padding: 16px; border: 1px solid #ccc; border-radius: 8px; margin-bottom: 16px;", - onkeydown: move |e: KeyboardEvent| { - log(format!("keydown: key={:?} code={:?}", e.key(), e.code())); - }, - onkeyup: move |e: KeyboardEvent| { - log(format!("keyup: key={:?} code={:?}", e.key(), e.code())); - }, - - label { r#for: "fruit", "Pick a fruit: " } - input { - id: "fruit", - list: "fruit-list", - placeholder: "Start typing...", - style: "padding: 8px; font-size: 16px; width: 100%;", - oninput: move |e: FormEvent| { - log(format!("input: value={:?}", e.value())); - }, - onchange: move |e: FormEvent| { - log(format!("change: value={:?}", e.value())); - }, - } - datalist { id: "fruit-list", - option { value: "apple" } - option { value: "apricot" } - option { value: "avocado" } - option { value: "banana" } - option { value: "blueberry" } - option { value: "cherry" } - option { value: "grape" } - option { value: "orange" } - option { value: "peach" } - option { value: "strawberry" } - } - } - - h3 { "Event log" } - div { - style: "background: #f5f5f5; padding: 12px; border-radius: 8px; font-family: monospace; font-size: 13px; max-height: 300px; overflow-y: auto;", - if events.read().is_empty() { - p { style: "color: #999;", "No events yet..." } - } - for (i, event) in events.read().iter().enumerate() { - div { key: "{i}", "{event}" } - } - } - } - } -} From 591f20ea9c8b697ea51444215522c90ad9edcf04 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 19 Mar 2026 14:37:19 -0500 Subject: [PATCH 06/15] pull out shared event list --- Cargo.toml | 5 - packages/html/src/events/animation.rs | 13 - packages/html/src/events/cancel.rs | 7 - packages/html/src/events/clipboard.rs | 13 - packages/html/src/events/composition.rs | 13 - packages/html/src/events/drag.rs | 28 -- packages/html/src/events/focus.rs | 16 -- packages/html/src/events/form.rs | 62 ----- packages/html/src/events/generated.rs | 332 ++++++++++++++++++++++++ packages/html/src/events/image.rs | 10 - packages/html/src/events/keyboard.rs | 13 - packages/html/src/events/media.rs | 75 ------ packages/html/src/events/mod.rs | 180 +------------ packages/html/src/events/mounted.rs | 67 +---- packages/html/src/events/mouse.rs | 60 ----- packages/html/src/events/pointer.rs | 36 --- packages/html/src/events/resize.rs | 7 - packages/html/src/events/scroll.rs | 10 - packages/html/src/events/selection.rs | 13 - packages/html/src/events/toggle.rs | 10 - packages/html/src/events/touch.rs | 15 -- packages/html/src/events/transition.rs | 7 - packages/html/src/events/visible.rs | 7 - packages/html/src/events/wheel.rs | 7 - packages/html/src/transit.rs | 128 ++------- packages/web/src/events/mod.rs | 20 +- 26 files changed, 360 insertions(+), 794 deletions(-) create mode 100644 packages/html/src/events/generated.rs diff --git a/Cargo.toml b/Cargo.toml index ab61ef7068..1739b85c91 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -937,11 +937,6 @@ name = "on_visible" path = "examples/08-apis/on_visible.rs" doc-scrape-examples = true -[[example]] -name = "datalist_autocomplete" -path = "examples/09-reference/datalist_autocomplete.rs" -doc-scrape-examples = true - [[example]] name = "all_events" path = "examples/09-reference/all_events.rs" diff --git a/packages/html/src/events/animation.rs b/packages/html/src/events/animation.rs index 23f6219121..217c1f59e2 100644 --- a/packages/html/src/events/animation.rs +++ b/packages/html/src/events/animation.rs @@ -130,16 +130,3 @@ pub trait HasAnimationData: std::any::Any { /// return self as Any fn as_any(&self) -> &dyn std::any::Any; } - -impl_event! [ - AnimationData; - - /// onanimationstart - onanimationstart - - /// onanimationend - onanimationend - - /// onanimationiteration - onanimationiteration -]; diff --git a/packages/html/src/events/cancel.rs b/packages/html/src/events/cancel.rs index ff36d5e359..507a0d7698 100644 --- a/packages/html/src/events/cancel.rs +++ b/packages/html/src/events/cancel.rs @@ -79,10 +79,3 @@ pub trait HasCancelData: std::any::Any { /// return self as Any fn as_any(&self) -> &dyn std::any::Any; } - -impl_event! { - CancelData; - - /// oncancel - oncancel -} diff --git a/packages/html/src/events/clipboard.rs b/packages/html/src/events/clipboard.rs index 265a747e9a..f7d5d7c067 100644 --- a/packages/html/src/events/clipboard.rs +++ b/packages/html/src/events/clipboard.rs @@ -79,16 +79,3 @@ pub trait HasClipboardData: std::any::Any { /// return self as Any fn as_any(&self) -> &dyn std::any::Any; } - -impl_event![ - ClipboardData; - - /// oncopy - oncopy - - /// oncut - oncut - - /// onpaste - onpaste -]; diff --git a/packages/html/src/events/composition.rs b/packages/html/src/events/composition.rs index 4d161436ed..cf4bfb78b0 100644 --- a/packages/html/src/events/composition.rs +++ b/packages/html/src/events/composition.rs @@ -94,16 +94,3 @@ pub trait HasCompositionData: std::any::Any { /// return self as Any fn as_any(&self) -> &dyn std::any::Any; } - -impl_event! [ - CompositionData; - - /// oncompositionstart - oncompositionstart - - /// oncompositionend - oncompositionend - - /// oncompositionupdate - oncompositionupdate -]; diff --git a/packages/html/src/events/drag.rs b/packages/html/src/events/drag.rs index 0767b917f1..f36d4e6555 100644 --- a/packages/html/src/events/drag.rs +++ b/packages/html/src/events/drag.rs @@ -226,31 +226,3 @@ pub trait HasDragData: HasMouseData + crate::HasFileData + crate::HasDataTransfe /// return self as Any fn as_any(&self) -> &dyn std::any::Any; } - -impl_event! { - DragData; - - /// ondrag - ondrag - - /// ondragend - ondragend - - /// ondragenter - ondragenter - - /// ondragexit - ondragexit - - /// ondragleave - ondragleave - - /// ondragover - ondragover - - /// ondragstart - ondragstart - - /// ondrop - ondrop -} diff --git a/packages/html/src/events/focus.rs b/packages/html/src/events/focus.rs index c5571e7ffd..9815629abd 100644 --- a/packages/html/src/events/focus.rs +++ b/packages/html/src/events/focus.rs @@ -79,19 +79,3 @@ pub trait HasFocusData: std::any::Any { /// return self as Any fn as_any(&self) -> &dyn std::any::Any; } - -impl_event! [ - FocusData; - - /// onfocus - onfocus - - // onfocusout - onfocusout - - // onfocusin - onfocusin - - /// onblur - onblur -]; diff --git a/packages/html/src/events/form.rs b/packages/html/src/events/form.rs index e394b114d7..3212a67e6d 100644 --- a/packages/html/src/events/form.rs +++ b/packages/html/src/events/form.rs @@ -346,65 +346,3 @@ mod serialize { } } } - -impl_event! { - FormData; - - /// onchange - onchange - - /// The `oninput` event is fired when the value of a ``, `