diff --git a/src/Uno.UI/Generated/3.0.0.0/Windows.UI.Xaml/UIElement.cs b/src/Uno.UI/Generated/3.0.0.0/Windows.UI.Xaml/UIElement.cs index a808515dfc8d..f0729f2bab54 100644 --- a/src/Uno.UI/Generated/3.0.0.0/Windows.UI.Xaml/UIElement.cs +++ b/src/Uno.UI/Generated/3.0.0.0/Windows.UI.Xaml/UIElement.cs @@ -678,8 +678,8 @@ public bool CanBeScrollAnchor } } #endif - #if __ANDROID__ || __IOS__ || IS_UNIT_TESTS || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__ || __MACOS__ - [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "IS_UNIT_TESTS", "__WASM__", "__SKIA__", "__NETSTD_REFERENCE__", "__MACOS__")] + #if __ANDROID__ || __IOS__ || IS_UNIT_TESTS || __SKIA__ || __NETSTD_REFERENCE__ || __MACOS__ + [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "IS_UNIT_TESTS", "__SKIA__", "__NETSTD_REFERENCE__", "__MACOS__")] public static global::Windows.UI.Xaml.RoutedEvent PreviewKeyDownEvent { get @@ -688,8 +688,8 @@ public bool CanBeScrollAnchor } } #endif - #if __ANDROID__ || __IOS__ || IS_UNIT_TESTS || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__ || __MACOS__ - [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "IS_UNIT_TESTS", "__WASM__", "__SKIA__", "__NETSTD_REFERENCE__", "__MACOS__")] + #if __ANDROID__ || __IOS__ || IS_UNIT_TESTS || __SKIA__ || __NETSTD_REFERENCE__ || __MACOS__ + [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "IS_UNIT_TESTS", "__SKIA__", "__NETSTD_REFERENCE__", "__MACOS__")] public static global::Windows.UI.Xaml.RoutedEvent PreviewKeyUpEvent { get @@ -1246,32 +1246,32 @@ public static bool TryStartDirectManipulation( global::Windows.UI.Xaml.Input.Poi } } #endif - #if __ANDROID__ || __IOS__ || IS_UNIT_TESTS || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__ || __MACOS__ - [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "IS_UNIT_TESTS", "__WASM__", "__SKIA__", "__NETSTD_REFERENCE__", "__MACOS__")] + #if __ANDROID__ || __IOS__ || IS_UNIT_TESTS || __SKIA__ || __NETSTD_REFERENCE__ || __MACOS__ + [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "IS_UNIT_TESTS", "__SKIA__", "__NETSTD_REFERENCE__", "__MACOS__")] public event global::Windows.UI.Xaml.Input.KeyEventHandler PreviewKeyDown { - [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "IS_UNIT_TESTS", "__WASM__", "__SKIA__", "__NETSTD_REFERENCE__", "__MACOS__")] + [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "IS_UNIT_TESTS", "__SKIA__", "__NETSTD_REFERENCE__", "__MACOS__")] add { global::Windows.Foundation.Metadata.ApiInformation.TryRaiseNotImplemented("Windows.UI.Xaml.UIElement", "event KeyEventHandler UIElement.PreviewKeyDown"); } - [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "IS_UNIT_TESTS", "__WASM__", "__SKIA__", "__NETSTD_REFERENCE__", "__MACOS__")] + [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "IS_UNIT_TESTS", "__SKIA__", "__NETSTD_REFERENCE__", "__MACOS__")] remove { global::Windows.Foundation.Metadata.ApiInformation.TryRaiseNotImplemented("Windows.UI.Xaml.UIElement", "event KeyEventHandler UIElement.PreviewKeyDown"); } } #endif - #if __ANDROID__ || __IOS__ || IS_UNIT_TESTS || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__ || __MACOS__ - [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "IS_UNIT_TESTS", "__WASM__", "__SKIA__", "__NETSTD_REFERENCE__", "__MACOS__")] + #if __ANDROID__ || __IOS__ || IS_UNIT_TESTS || __SKIA__ || __NETSTD_REFERENCE__ || __MACOS__ + [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "IS_UNIT_TESTS", "__SKIA__", "__NETSTD_REFERENCE__", "__MACOS__")] public event global::Windows.UI.Xaml.Input.KeyEventHandler PreviewKeyUp { - [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "IS_UNIT_TESTS", "__WASM__", "__SKIA__", "__NETSTD_REFERENCE__", "__MACOS__")] + [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "IS_UNIT_TESTS", "__SKIA__", "__NETSTD_REFERENCE__", "__MACOS__")] add { global::Windows.Foundation.Metadata.ApiInformation.TryRaiseNotImplemented("Windows.UI.Xaml.UIElement", "event KeyEventHandler UIElement.PreviewKeyUp"); } - [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "IS_UNIT_TESTS", "__WASM__", "__SKIA__", "__NETSTD_REFERENCE__", "__MACOS__")] + [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "IS_UNIT_TESTS", "__SKIA__", "__NETSTD_REFERENCE__", "__MACOS__")] remove { global::Windows.Foundation.Metadata.ApiInformation.TryRaiseNotImplemented("Windows.UI.Xaml.UIElement", "event KeyEventHandler UIElement.PreviewKeyUp"); diff --git a/src/Uno.UI/UI/Xaml/RoutedEventFlag.cs b/src/Uno.UI/UI/Xaml/RoutedEventFlag.cs index 9d22ace628f4..cc4ed54a04e9 100644 --- a/src/Uno.UI/UI/Xaml/RoutedEventFlag.cs +++ b/src/Uno.UI/UI/Xaml/RoutedEventFlag.cs @@ -20,9 +20,9 @@ public enum RoutedEventFlag : ulong PointerWheelChanged = 1UL << 7, // Keyboard - // PreviewKeyDown = 1UL << 12 => Reserved for future usage + PreviewKeyDown = 1UL << 12, KeyDown = 1UL << 13, - // PreviewKeyUp = 1 >> 14, => Reserved for future usage + PreviewKeyUp = 1UL << 14, KeyUp = 1UL << 15, // CharacterReceived = 1UL << 16, // ProcessKeyboardAccelerators = 1UL << 17, => Reserved for future use (even if it is not an actual standard RoutedEvent) @@ -77,7 +77,9 @@ internal static class RoutedEventFlagExtensions | RoutedEventFlag.PointerWheelChanged; private const RoutedEventFlag _isKey = // 0b0000_0000_0000_0000___0000_0000_0000_0000___0000_0000_0001_1111___1111_0000_0000_0000 - RoutedEventFlag.KeyDown + RoutedEventFlag.PreviewKeyDown + | RoutedEventFlag.PreviewKeyUp + | RoutedEventFlag.KeyDown | RoutedEventFlag.KeyUp; private const RoutedEventFlag _isFocus = // 0b0000_0000_0000_0000___0000_0000_0000_0000___0111_1111_0000_0000___0000_0000_0000_0000 diff --git a/src/Uno.UI/UI/Xaml/UIElement.EventRegistration.wasm.cs b/src/Uno.UI/UI/Xaml/UIElement.EventRegistration.wasm.cs index f8881dbbb089..d9a675204b7a 100644 --- a/src/Uno.UI/UI/Xaml/UIElement.EventRegistration.wasm.cs +++ b/src/Uno.UI/UI/Xaml/UIElement.EventRegistration.wasm.cs @@ -203,9 +203,9 @@ internal void RegisterEventHandler( this.Log().Debug($"Registering {eventName} on {this}."); } - if (!_eventHandlers.TryGetValue(eventName, out var registration)) + if (!_eventHandlers.TryGetValue((eventName, onCapturePhase), out var registration)) { - _eventHandlers[eventName] = registration = new EventRegistration( + _eventHandlers[(eventName, onCapturePhase)] = registration = new EventRegistration( this, eventName, onCapturePhase, @@ -216,9 +216,9 @@ internal void RegisterEventHandler( registration.Add(handler, invoker); } - internal void UnregisterEventHandler(string eventName, Delegate handler, GenericEventHandler invoker) + internal void UnregisterEventHandler(string eventName, Delegate handler, GenericEventHandler invoker, bool onCapturePhase = false) { - if (_eventHandlers.TryGetValue(eventName, out var registration)) + if (_eventHandlers.TryGetValue((eventName, onCapturePhase), out var registration)) { registration.Remove(handler, invoker); } @@ -228,12 +228,12 @@ internal void UnregisterEventHandler(string eventName, Delegate handler, Generic } } - internal HtmlEventDispatchResult InternalDispatchEvent(string eventName, EventArgs eventArgs = null, string nativeEventPayload = null) + internal HtmlEventDispatchResult InternalDispatchEvent(string eventName, EventArgs eventArgs = null, string nativeEventPayload = null, bool onCapturePhase = false) { var n = eventName; try { - return InternalInnerDispatchEvent(eventArgs, nativeEventPayload, n); + return InternalInnerDispatchEvent(eventArgs, nativeEventPayload, n, onCapturePhase); } catch (Exception e) { @@ -250,9 +250,9 @@ internal HtmlEventDispatchResult InternalDispatchEvent(string eventName, EventAr /// See https://github.com/dotnet/runtime/issues/56309 /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - private HtmlEventDispatchResult InternalInnerDispatchEvent(EventArgs eventArgs, string nativeEventPayload, string n) + private HtmlEventDispatchResult InternalInnerDispatchEvent(EventArgs eventArgs, string nativeEventPayload, string n, bool onCapturePhase = false) { - if (_eventHandlers.TryGetValue(n, out var registration)) + if (_eventHandlers.TryGetValue((n, onCapturePhase), out var registration)) { return registration.Dispatch(eventArgs, nativeEventPayload); } @@ -278,7 +278,7 @@ private HtmlEventDispatchResult InternalInnerDispatchEvent(EventArgs eventArgs, #endif [Preserve] [EditorBrowsable(EditorBrowsableState.Never)] - public static int DispatchEvent(int handle, string eventName, string eventArgs) + public static int DispatchEvent(int handle, string eventName, string eventArgs, bool onCapturePhase) { #if DEBUG try @@ -287,7 +287,7 @@ public static int DispatchEvent(int handle, string eventName, string eventArgs) // Dispatch to right object, if we can find it if (GetElementFromHandle(handle) is UIElement element) { - return (int)element.InternalDispatchEvent(eventName, nativeEventPayload: eventArgs); + return (int)element.InternalDispatchEvent(eventName, nativeEventPayload: eventArgs, onCapturePhase: onCapturePhase); } else { @@ -305,7 +305,20 @@ public static int DispatchEvent(int handle, string eventName, string eventArgs) #endif } - private readonly Dictionary _eventHandlers = new Dictionary(StringComparer.OrdinalIgnoreCase); + public class TupleComparer : IEqualityComparer<(string, bool)> + { + public bool Equals((string, bool) x, (string, bool) y) + { + return StringComparer.OrdinalIgnoreCase.Equals(x.Item1, y.Item1) && x.Item2 == y.Item2; + } + + public int GetHashCode((string, bool) obj) + { + return StringComparer.OrdinalIgnoreCase.GetHashCode(obj.Item1) ^ obj.Item2.GetHashCode(); + } + } + + private readonly Dictionary<(string, bool), EventRegistration> _eventHandlers = new Dictionary<(string, bool), EventRegistration>(new TupleComparer()); internal delegate EventArgs EventArgsParser(object sender, string payload); diff --git a/src/Uno.UI/UI/Xaml/UIElement.RoutedEvents.cs b/src/Uno.UI/UI/Xaml/UIElement.RoutedEvents.cs index 08e8a8647b2c..9475e3fba07d 100644 --- a/src/Uno.UI/UI/Xaml/UIElement.RoutedEvents.cs +++ b/src/Uno.UI/UI/Xaml/UIElement.RoutedEvents.cs @@ -146,7 +146,11 @@ partial class UIElement /* ** */ internal /* ** */ static RoutedEvent DropCompletedEvent { get; } = new RoutedEvent(RoutedEventFlag.DropCompleted); +#if __WASM__ + public static RoutedEvent PreviewKeyDownEvent { get; } = new RoutedEvent(RoutedEventFlag.PreviewKeyDown); + public static RoutedEvent PreviewKeyUpEvent { get; } = new RoutedEvent(RoutedEventFlag.PreviewKeyUp); +#endif public static RoutedEvent KeyDownEvent { get; } = new RoutedEvent(RoutedEventFlag.KeyDown); public static RoutedEvent KeyUpEvent { get; } = new RoutedEvent(RoutedEventFlag.KeyUp); @@ -434,6 +438,20 @@ public event TypedEventHandler DropCompleted remove => RemoveHandler(DropCompletedEvent, value); } +#if __WASM__ + public event KeyEventHandler PreviewKeyDown + { + add => AddHandler(PreviewKeyDownEvent, value, false); + remove => RemoveHandler(PreviewKeyDownEvent, value); + } + + public event KeyEventHandler PreviewKeyUp + { + add => AddHandler(PreviewKeyUpEvent, value, false); + remove => RemoveHandler(PreviewKeyUpEvent, value); + } +#endif + #if __MACOS__ public new event KeyEventHandler KeyDown #else @@ -748,6 +766,16 @@ private static void TrackKeyState(RoutedEvent routedEvent, RoutedEventArgs args) { KeyboardStateTracker.OnKeyUp(keyArgs.OriginalKey); } +#if __WASM__ + else if (routedEvent == PreviewKeyDownEvent) + { + KeyboardStateTracker.OnKeyDown(keyArgs.OriginalKey); + } + else if (routedEvent == PreviewKeyUpEvent) + { + KeyboardStateTracker.OnKeyUp(keyArgs.OriginalKey); + } +#endif } } diff --git a/src/Uno.UI/UI/Xaml/UIElement.wasm.cs b/src/Uno.UI/UI/Xaml/UIElement.wasm.cs index cb1899931cf3..c12846accba3 100644 --- a/src/Uno.UI/UI/Xaml/UIElement.wasm.cs +++ b/src/Uno.UI/UI/Xaml/UIElement.wasm.cs @@ -616,22 +616,32 @@ partial void AddKeyHandler(RoutedEvent routedEvent, int handlersCount, object ha _registeredRoutedEvents |= routedEvent.Flag; string domEventName; - if (routedEvent.Flag == RoutedEventFlag.KeyDown) - { - domEventName = "keydown"; - } - else - { - domEventName = routedEvent.Flag == RoutedEventFlag.KeyUp - ? "keyup" - : throw new ArgumentOutOfRangeException(nameof(routedEvent), "Not a keyboard event"); + bool onCapturePhase = false; + switch (routedEvent.Flag) + { + case RoutedEventFlag.PreviewKeyDown: + domEventName = "keydown"; + onCapturePhase = true; + break; + case RoutedEventFlag.KeyDown: + domEventName = "keydown"; + break; + case RoutedEventFlag.PreviewKeyUp: + domEventName = "keyup"; + onCapturePhase = true; + break; + case RoutedEventFlag.KeyUp: + domEventName = "keyup"; + break; + default: + throw new ArgumentOutOfRangeException(nameof(routedEvent), "Not a keyboard event"); } RegisterEventHandler( domEventName, handler: new RoutedEventHandlerWithHandled((snd, args) => RaiseEvent(routedEvent, args)), invoker: GenericEventHandlers.RaiseRoutedEventHandlerWithHandled, - onCapturePhase: false, + onCapturePhase, eventExtractor: HtmlEventExtractor.KeyboardEventExtractor, payloadConverter: PayloadToKeyArgs ); diff --git a/src/Uno.UI/ts/WindowManager.ts b/src/Uno.UI/ts/WindowManager.ts index 7a4769b5f631..7b590855a80c 100644 --- a/src/Uno.UI/ts/WindowManager.ts +++ b/src/Uno.UI/ts/WindowManager.ts @@ -918,7 +918,7 @@ namespace Uno.UI { ? `${eventExtractor(event)}` : ""; - const result = this.dispatchEvent(element, eventName, eventPayload); + const result = this.dispatchEvent(element, eventName, eventPayload, onCapturePhase); if (result & HtmlEventDispatchResult.StopPropagation) { event.stopPropagation(); } @@ -1695,7 +1695,7 @@ namespace Uno.UI { WindowManager.focusInMethod(-1); } - private dispatchEvent(element: HTMLElement | SVGElement, eventName: string, eventPayload: string = null): HtmlEventDispatchResult { + private dispatchEvent(element: HTMLElement | SVGElement, eventName: string, eventPayload: string = null, onCapturePhase: boolean = false): HtmlEventDispatchResult { const htmlId = Number(element.getAttribute("XamlHandle")); // console.debug(`${element.getAttribute("id")}: Raising event ${eventName}.`); @@ -1704,7 +1704,7 @@ namespace Uno.UI { throw `No attribute XamlHandle on element ${element}. Can't raise event.`; } - return WindowManager.dispatchEventMethod(htmlId, eventName, eventPayload || ""); + return WindowManager.dispatchEventMethod(htmlId, eventName, eventPayload || "", onCapturePhase); } private getIsConnectedToRootElement(element: HTMLElement | SVGElement): boolean {