Skip to content

Commit 536a46f

Browse files
authored
Merge pull request #22111 from ramezgerges/textbox_drops_keydown
fix(textbox): prevent keys from dropping when input is too quick
2 parents 169540c + 62a1cd5 commit 536a46f

File tree

6 files changed

+149
-38
lines changed

6 files changed

+149
-38
lines changed

src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Controls/Given_TextBox.skia.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,27 @@ public async Task When_Basic_Input_Event_Sequence()
117117
""".Replace("\r\n", "\n"), eventLog);
118118
}
119119

120+
[TestMethod]
121+
public async Task When_Public_KeyDown_Subscription_Changes_Text()
122+
{
123+
using var _ = new TextBoxFeatureConfigDisposable();
124+
125+
var SUT = new TextBox();
126+
SUT.KeyDown += (sender, args) =>
127+
{
128+
if (args.Key == VirtualKey.T)
129+
{
130+
SUT.Text = "Ramez";
131+
}
132+
};
133+
134+
await UITestHelper.Load(SUT);
135+
136+
await KeyboardHelper.PressKeySequence("t", SUT);
137+
138+
Assert.AreEqual("tRamez", SUT.Text);
139+
}
140+
120141
[TestMethod]
121142
public async Task When_Basic_Input_With_ArrowKeys()
122143
{

src/Uno.UI/UI/Xaml/Controls/AutoSuggestBox/AutoSuggestBox.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,7 @@ void RegisterEvents()
300300
{
301301
if (_textBox != null)
302302
{
303-
_textBox.KeyDown += OnTextBoxKeyDown;
303+
_textBox.PostKeyDown += OnTextBoxPostKeyDown;
304304
_queryButton = _textBox.GetTemplateChild<Button>("QueryButton");
305305
}
306306

@@ -332,7 +332,7 @@ void UnregisterEvents()
332332
_textBoxLoadedDisposable?.Dispose();
333333
if (_textBox != null)
334334
{
335-
_textBox.KeyDown -= OnTextBoxKeyDown;
335+
_textBox.PostKeyDown -= OnTextBoxPostKeyDown;
336336
}
337337

338338
if (_queryButton != null)
@@ -448,7 +448,7 @@ private void SubmitSearch(object item)
448448
IsSuggestionListOpen = false;
449449
}
450450

451-
private void OnTextBoxKeyDown(object sender, KeyRoutedEventArgs e)
451+
private void OnTextBoxPostKeyDown(object sender, KeyRoutedEventArgs e)
452452
{
453453
if (e.Key == VirtualKey.Enter)
454454
{

src/Uno.UI/UI/Xaml/Controls/Control/Control.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ public partial class Control : FrameworkElement
4040
private void InitializeControl()
4141
{
4242
SubscribeToOverridenRoutedEvents();
43+
SubscribeToPostKeyDown();
4344
OnIsFocusableChanged();
4445

4546
DefaultStyleKey = typeof(Control);
@@ -329,6 +330,15 @@ private protected override void OnPostLoading()
329330
#endif
330331

331332
#if !__NETSTD_REFERENCE__
333+
[System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2072", Justification = "Types manipulated here have been marked earlier")]
334+
private void SubscribeToPostKeyDown()
335+
{
336+
if (GetIsEventOverrideImplemented(GetType(), nameof(OnPostKeyDown), _keyArgsType))
337+
{
338+
PostKeyDown += OnPostKeyDownHandler;
339+
}
340+
}
341+
332342
[UnconditionalSuppressMessage("Trimming", "IL2072", Justification = "Types manipulated here have been marked earlier")]
333343
private void SubscribeToOverridenRoutedEvents()
334344
{
@@ -1031,6 +1041,7 @@ protected virtual void OnPreviewKeyDown(KeyRoutedEventArgs e) { }
10311041
protected virtual void OnPreviewKeyUp(KeyRoutedEventArgs e) { }
10321042
#endif
10331043
protected virtual void OnKeyDown(KeyRoutedEventArgs e) { }
1044+
private protected virtual void OnPostKeyDown(KeyRoutedEventArgs e) { }
10341045
protected virtual void OnKeyUp(KeyRoutedEventArgs e) { }
10351046
protected virtual void OnGotFocus(RoutedEventArgs e) { }
10361047
protected virtual void OnLostFocus(RoutedEventArgs e) { }
@@ -1117,6 +1128,9 @@ protected virtual void OnLostFocus(RoutedEventArgs e) { }
11171128
}
11181129
};
11191130

1131+
private static readonly KeyEventHandler OnPostKeyDownHandler =
1132+
(object sender, KeyRoutedEventArgs args) => ((Control)sender).OnPostKeyDown(args);
1133+
11201134
private static readonly KeyEventHandler OnKeyUpHandler =
11211135
(object sender, KeyRoutedEventArgs args) => ((Control)sender).OnKeyUp(args);
11221136

src/Uno.UI/UI/Xaml/Controls/TextBox/TextBox.cs

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1159,10 +1159,27 @@ protected override void OnTapped(TappedRoutedEventArgs e)
11591159

11601160
partial void OnTappedPartial();
11611161

1162-
/// <inheritdoc />
1162+
partial void OnKeyDownPartial(KeyRoutedEventArgs args);
1163+
11631164
protected override void OnKeyDown(KeyRoutedEventArgs args)
11641165
{
1166+
base.OnKeyDown(args);
1167+
11651168
OnKeyDownPartial(args);
1169+
}
1170+
1171+
private protected override void OnPostKeyDown(KeyRoutedEventArgs args)
1172+
{
1173+
#if __SKIA__
1174+
if (_isSkiaTextBox)
1175+
{
1176+
OnKeyDownSkia(args);
1177+
}
1178+
else
1179+
#endif
1180+
{
1181+
OnKeyDownNonSkia(args);
1182+
}
11661183

11671184
var modifiers = CoreImports.Input_GetKeyboardModifiers();
11681185
if (!args.Handled && KeyboardAcceleratorUtility.IsKeyValidForAccelerators(args.Key, KeyboardAcceleratorUtility.MapVirtualKeyModifiersToIntegersModifiers(modifiers)))
@@ -1175,17 +1192,8 @@ protected override void OnKeyDown(KeyRoutedEventArgs args)
11751192
}
11761193
}
11771194

1178-
partial void OnKeyDownPartial(KeyRoutedEventArgs args);
1179-
1180-
#if !__SKIA__
1181-
partial void OnKeyDownPartial(KeyRoutedEventArgs args) => OnKeyDownInternal(args);
1182-
#endif
1183-
1184-
private void OnKeyDownInternal(KeyRoutedEventArgs args)
1195+
private void OnKeyDownNonSkia(KeyRoutedEventArgs args)
11851196
{
1186-
base.OnKeyDown(args);
1187-
1188-
11891197
// On skia, sometimes SelectionStart is updated to a new value before KeyDown is fired, so
11901198
// we need to get selectionStart from another source on Skia.
11911199
#if __SKIA__

src/Uno.UI/UI/Xaml/Controls/TextBox/TextBox.skia.cs

Lines changed: 85 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -436,14 +436,80 @@ private void UpdateScrolling(bool putSelectionEndInVisibleViewport)
436436

437437
partial void OnKeyDownPartial(KeyRoutedEventArgs args)
438438
{
439+
// This is a minimal copy of OnkeyDownSkia that just sets args.Handled without doing any work.
440+
// This is to match WinUI behavior where Handled is set for certain keys before public
441+
// subscribers get the event, but before any actual text processing is done.
439442
if (!_isSkiaTextBox)
440443
{
441-
OnKeyDownInternal(args);
442444
return;
443445
}
444446

445-
base.OnKeyDown(args);
447+
var (selectionStart, selectionLength) = _selection.selectionEndsAtTheStart ? (_selection.start + _selection.length, -_selection.length) : (_selection.start, _selection.length);
448+
var text = Text;
449+
var shift = args.KeyboardModifiers.HasFlag(VirtualKeyModifiers.Shift);
450+
var ctrl = args.KeyboardModifiers.HasFlag(_platformCtrlKey);
451+
switch (args.Key)
452+
{
453+
case VirtualKey.Escape:
454+
if (HasPointerCapture)
455+
{
456+
args.Handled = true;
457+
}
458+
return;
459+
case VirtualKey.Z when ctrl:
460+
case VirtualKey.Y when ctrl:
461+
case VirtualKey.Delete when !IsReadOnly:
462+
case VirtualKey.A when ctrl:
463+
if (!HasPointerCapture)
464+
{
465+
args.Handled = true;
466+
}
467+
return;
468+
case VirtualKey.Up:
469+
// on macOS start of document is `Command` and `Up`
470+
if (ctrl && OperatingSystem.IsMacOS())
471+
{
472+
KeyDownHome(args, text, ctrl, shift, ref selectionStart, ref selectionLength);
473+
}
474+
else
475+
{
476+
KeyDownUpArrow(args, text, ctrl, shift, ref selectionStart, ref selectionLength);
477+
}
478+
break;
479+
case VirtualKey.Down:
480+
// on macOS end of document is `Command` and `Down`
481+
if (ctrl && OperatingSystem.IsMacOS())
482+
{
483+
KeyDownEnd(args, text, ctrl, shift, ref selectionStart, ref selectionLength);
484+
}
485+
else
486+
{
487+
KeyDownDownArrow(args, text, ctrl, shift, ref selectionStart, ref selectionLength);
488+
}
489+
break;
490+
case VirtualKey.Left when !TextBoxView.DisplayBlock.ParsedText.IsBaseDirectionRightToLeft:
491+
case VirtualKey.Right when TextBoxView.DisplayBlock.ParsedText.IsBaseDirectionRightToLeft:
492+
KeyDownLeftArrow(args, text, shift, ctrl, ref selectionStart, ref selectionLength);
493+
break;
494+
case VirtualKey.Left when TextBoxView.DisplayBlock.ParsedText.IsBaseDirectionRightToLeft:
495+
case VirtualKey.Right when !TextBoxView.DisplayBlock.ParsedText.IsBaseDirectionRightToLeft:
496+
KeyDownRightArrow(args, text, ctrl, shift, ref selectionStart, ref selectionLength);
497+
break;
498+
case VirtualKey.Home:
499+
KeyDownHome(args, text, ctrl, shift, ref selectionStart, ref selectionLength);
500+
break;
501+
case VirtualKey.End:
502+
KeyDownEnd(args, text, ctrl, shift, ref selectionStart, ref selectionLength);
503+
break;
504+
// TODO: PageUp/Down
505+
case VirtualKey.Back when !IsReadOnly:
506+
KeyDownBack(args, ref text, ctrl, shift, ref selectionStart, ref selectionLength);
507+
break;
508+
}
509+
}
446510

511+
private void OnKeyDownSkia(KeyRoutedEventArgs args)
512+
{
447513
if (_selection.length != 0 &&
448514
args.Key is not (VirtualKey.Up or VirtualKey.Down or VirtualKey.Left or VirtualKey.Right))
449515
{
@@ -588,31 +654,26 @@ args.Key is not (VirtualKey.Up or VirtualKey.Down or VirtualKey.Left or VirtualK
588654
selectionStart = Math.Max(0, Math.Min(text.Length, selectionStart));
589655
selectionLength = Math.Max(-selectionStart, Math.Min(text.Length - selectionStart, selectionLength));
590656

591-
// This is queued in order to run after public KeyDown callbacks are fired and is enqueued on High to run
592-
// before the next TextChanged+KeyUp sequence.
593-
DispatcherQueue.TryEnqueue(DispatcherQueuePriority.High, () =>
594-
{
595-
var caretXOffset = _caretXOffset;
657+
var caretXOffset = _caretXOffset;
596658

597-
_suppressCurrentlyTyping = true;
598-
_clearHistoryOnTextChanged = false;
599-
if (!HasPointerCapture)
600-
{
601-
_pendingSelection = (selectionStart, selectionLength);
602-
}
659+
_suppressCurrentlyTyping = true;
660+
_clearHistoryOnTextChanged = false;
661+
if (!HasPointerCapture)
662+
{
663+
_pendingSelection = (selectionStart, selectionLength);
664+
}
603665

604-
ProcessTextInput(text);
605-
_clearHistoryOnTextChanged = true;
606-
_suppressCurrentlyTyping = false;
666+
ProcessTextInput(text);
667+
_clearHistoryOnTextChanged = true;
668+
_suppressCurrentlyTyping = false;
607669

608-
// don't change the caret offset when moving up and down
609-
if (args.Key is VirtualKey.Up or VirtualKey.Down)
610-
{
611-
// this condition is accurate in the case of hitting Down on the last line
612-
// or up on the first line. On WinUI, the caret offset won't change.
613-
_caretXOffset = caretXOffset;
614-
}
615-
});
670+
// don't change the caret offset when moving up and down
671+
if (args.Key is VirtualKey.Up or VirtualKey.Down)
672+
{
673+
// this condition is accurate in the case of hitting Down on the last line
674+
// or up on the first line. On WinUI, the caret offset won't change.
675+
_caretXOffset = caretXOffset;
676+
}
616677
}
617678

618679
internal void SetPendingSelection(int selectionStart, int selectionLength)

src/Uno.UI/UI/Xaml/UIElement.RoutedEvents.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,8 @@ public event KeyEventHandler KeyDown
437437
remove => RemoveHandler(KeyDownEvent, value);
438438
}
439439

440+
internal event KeyEventHandler PostKeyDown;
441+
440442
public event KeyEventHandler KeyUp
441443
{
442444
add => AddHandler(KeyUpEvent, value, false);
@@ -647,6 +649,11 @@ internal bool RaiseEvent(RoutedEvent routedEvent, RoutedEventArgs args, Bubbling
647649
}
648650
}
649651

652+
if (routedEvent == KeyDownEvent)
653+
{
654+
PostKeyDown?.Invoke(this, (KeyRoutedEventArgs)args);
655+
}
656+
650657
if (routedEvent.IsTunnelingEvent || ctx.ModeHasFlag(BubblingMode.IgnoreParents) || ctx.Root == this)
651658
{
652659
return isHandled;

0 commit comments

Comments
 (0)