Skip to content

Commit

Permalink
feat(focus): Raise Control.GotFocus asynchronously
Browse files Browse the repository at this point in the history
Bring focus handling closer into line with what UWP does, including ensuring that Control.GotFocus and Control.LostFocus are raised asynchronously, and that calling Focus() on a Control with IsTabStop=false recursively checks children to find one that can take the focus.
  • Loading branch information
davidjohnoliver committed Mar 31, 2020
1 parent 423538c commit 21d7758
Show file tree
Hide file tree
Showing 11 changed files with 302 additions and 175 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Private.Infrastructure;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Input;

namespace Uno.UI.RuntimeTests.Tests.Windows_UI_Xaml_Input
{
[TestClass]
public class Given_FocusManager
{
[TestMethod]
[RunsOnUIThread]
public async Task GotLostFocus()
{
try
{
var panel = new StackPanel();
var wasEventRaised = false;

var receivedGotFocus = new bool[4];
var receivedLostFocus = new bool[4];

var buttons = new Button[4];
for (int i = 0; i < 4; i++)
{
var button = new Button
{
Content = $"Button {i}",
Name = $"Button{i}"
};

panel.Children.Add(button);
buttons[i] = button;
}

TestServices.WindowHelper.WindowContent = panel;

await TestServices.WindowHelper.WaitForIdle();

const int tries = 10;
var initialSuccess = false;
for (int i = 0; i < tries; i++)
{
initialSuccess = buttons[0].Focus(FocusState.Programmatic);
if (initialSuccess)
{
break;
}
//await TestServices.WindowHelper.WaitForIdle(); //
await Task.Delay(50); //
}

Assert.IsTrue(initialSuccess);
AssertHasFocus(buttons[0]);
await TestServices.WindowHelper.WaitForIdle();

AssertHasFocus(buttons[0]);

for (int i = 0; i < 4; i++)
{
var inner = i;
buttons[i].GotFocus += (o, e) =>
{
receivedGotFocus[inner] = true;
wasEventRaised = true;
Assert.IsNotNull(FocusManager.GetFocusedElement());
};

buttons[i].LostFocus += (o, e) =>
{
receivedLostFocus[inner] = true;
wasEventRaised = true;
Assert.IsNotNull(FocusManager.GetFocusedElement());
};
}

FocusManager.GotFocus += (o, e) =>
{
Assert.IsNotNull(FocusManager.GetFocusedElement());
wasEventRaised = true;
};
FocusManager.LostFocus += (o, e) =>
{
Assert.IsNotNull(FocusManager.GetFocusedElement());
wasEventRaised = true;
};

buttons[1].Focus(FocusState.Programmatic);
buttons[3].Focus(FocusState.Programmatic);
buttons[2].Focus(FocusState.Programmatic);

AssertHasFocus(buttons[2]);
Assert.IsFalse(wasEventRaised);
await TestServices.WindowHelper.WaitForIdle();
AssertHasFocus(buttons[2]);
Assert.IsTrue(wasEventRaised);

Assert.IsFalse(receivedGotFocus[0]);
Assert.IsTrue(receivedGotFocus[1]);
Assert.IsTrue(receivedGotFocus[2]);
Assert.IsTrue(receivedGotFocus[3]);

Assert.IsTrue(receivedLostFocus[0]);
Assert.IsTrue(receivedLostFocus[1]);
Assert.IsFalse(receivedLostFocus[2]);
Assert.IsTrue(receivedLostFocus[3]);
;
}
finally
{
TestServices.WindowHelper.WindowContent = null;
}
}

[TestMethod]
[RunsOnUIThread]
public async Task IsTabStop_False_Check_Inner()
{
var outerControl = new ContentControl() { IsTabStop = false };
var innerControl = new Button { Content = "Inner" };
var contentRoot = new Border
{
Child = new Grid
{
Children =
{
new TextBlock {Text = "Spinach"},
innerControl
}
}
};
outerControl.Content = contentRoot;

TestServices.WindowHelper.WindowContent = outerControl;
await TestServices.WindowHelper.WaitForIdle();

outerControl.Focus(FocusState.Programmatic);
AssertHasFocus(innerControl);
Assert.AreEqual(FocusState.Unfocused, outerControl.FocusState);

TestServices.WindowHelper.WindowContent = null;
}

private void AssertHasFocus(Control control)
{
Assert.IsNotNull(control);
var focused = FocusManager.GetFocusedElement();
Assert.IsNotNull(focused);
Assert.AreEqual(focused, control);
Assert.AreNotEqual(FocusState.Unfocused, control.FocusState);
}
}
}
16 changes: 0 additions & 16 deletions src/Uno.UI/UI/Xaml/Controls/Control/Control.Android.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,21 +50,5 @@ public IFrameworkElement GetTemplateRoot()
{
return this.GetChildren()?.FirstOrDefault() as IFrameworkElement;
}

partial void OnFocusStateChangedPartial(FocusState oldValue, FocusState newValue)
{
if (newValue == FocusState.Pointer && Focusable)
{
//Set native focus to this view
RequestFocus();
}
}

protected virtual bool RequestFocus(FocusState state)
{
FocusState = state;

return true;
}
}
}
44 changes: 14 additions & 30 deletions src/Uno.UI/UI/Xaml/Controls/Control/Control.cs
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ internal View TemplatedRoot

if (value != null)
{
if(_templatedRoot is IDependencyObjectStoreProvider provider)
if (_templatedRoot is IDependencyObjectStoreProvider provider)
{
provider.Store.SetValue(provider.Store.TemplatedParentProperty, this, DependencyPropertyValuePrecedences.Local);
}
Expand Down Expand Up @@ -310,7 +310,7 @@ private static object FindNameInScope(IFrameworkElement root, string name)
&& NameScope.GetNameScope(root) is INameScope nameScope
&& nameScope.FindName(name) is DependencyObject element
// Doesn't currently support ElementStub (fallbacks to other FindName implementation)
&& !(element is ElementStub)
&& !(element is ElementStub)
? element
: null;
}
Expand Down Expand Up @@ -363,7 +363,7 @@ private void UpdateTemplate()
}

if (
!FeatureConfiguration.FrameworkElement.UseLegacyApplyStylePhase &&
!FeatureConfiguration.FrameworkElement.UseLegacyApplyStylePhase &&
FeatureConfiguration.FrameworkElement.ClearPreviousOnStyleChange
)
{
Expand Down Expand Up @@ -610,8 +610,7 @@ public FocusState FocusState
typeof(FocusState),
typeof(Control),
new PropertyMetadata(
(FocusState)FocusState.Unfocused,
(s, e) => ((Control)s)?.OnFocusStateChanged((FocusState)e.OldValue, (FocusState)e.NewValue)
(FocusState)FocusState.Unfocused
)
);

Expand Down Expand Up @@ -668,11 +667,13 @@ public bool Focus(FocusState value)
throw new ArgumentException("Value does not fall within the expected range.", nameof(value));
}

#if __WASM__
return Visibility == Visibility.Visible && IsEnabled && RequestFocus(value);
#else
return IsFocusable && RequestFocus(value);
#endif
return RequestFocus(value);
}


protected virtual bool RequestFocus(FocusState state)
{
return FocusManager.SetFocusedElement(this, FocusNavigationDirection.None, state);
}

internal void Unfocus()
Expand Down Expand Up @@ -736,29 +737,12 @@ protected virtual void OnBorderBrushChanged(Brush oldValue, Brush newValue)

partial void OnBorderBrushChangedPartial(Brush oldValue, Brush newValue);

protected virtual void OnFocusStateChanged(FocusState oldValue, FocusState newValue)
internal virtual void UpdateFocusState(FocusState focusState)
{
if (newValue == FocusState.Unfocused && oldValue == newValue)
{
return;
}

OnFocusStateChangedPartial(oldValue, newValue);
#if XAMARIN || __WASM__
FocusManager.OnFocusChanged(this, newValue);
#endif

if (newValue == FocusState.Unfocused)
{
RaiseEvent(LostFocusEvent, new RoutedEventArgs(this));
}
else
{
RaiseEvent(GotFocusEvent, new RoutedEventArgs(this));
}
FocusState = focusState;
}

partial void OnFocusStateChangedPartial(FocusState oldValue, FocusState newValue);
partial void UpdateFocusStatePartial(FocusState focusState);

protected virtual void OnPointerPressed(PointerRoutedEventArgs args) { }
protected virtual void OnPointerReleased(PointerRoutedEventArgs args) { }
Expand Down
7 changes: 0 additions & 7 deletions src/Uno.UI/UI/Xaml/Controls/Control/Control.iOSmacOS.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,6 @@ partial void RegisterSubView(View child)

AddSubview(child);
}

protected virtual bool RequestFocus(FocusState state)
{
FocusState = state;

return true;
}
}
}

46 changes: 1 addition & 45 deletions src/Uno.UI/UI/Xaml/Controls/Control/Control.wasm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public partial class Control
{
/// This binary compatibility workaround that can be removed
public new static DependencyProperty IsEnabledProperty => FrameworkElement.IsEnabledProperty;

public Control(string htmlTag = "div") : base(htmlTag)
{
InitializeControl();
Expand Down Expand Up @@ -46,15 +46,6 @@ public IFrameworkElement GetTemplateRoot()
return this.GetChildren()?.FirstOrDefault() as IFrameworkElement;
}

partial void OnFocusStateChangedPartial(FocusState oldValue, FocusState newValue)
{
//if (newValue == FocusState.Pointer && Focusable)
//{
// //Set native focus to this view
// RequestFocus();
//}
}

partial void OnIsFocusableChanged()
{
var isFocusable = IsFocusable && !IsDelegatingFocusToTemplateChild();
Expand All @@ -63,40 +54,5 @@ partial void OnIsFocusableChanged()
}

protected virtual bool IsDelegatingFocusToTemplateChild() => false;

private FocusState? _pendingFocusRequestState;
protected virtual bool RequestFocus(FocusState state)
{
try
{
_pendingFocusRequestState = state;
return FocusManager.Focus(this);
}
finally
{
_pendingFocusRequestState = null;
}
}

internal bool SetFocused(bool isFocused)
{
if (isFocused)
{
if (IsFocusable)
{
FocusState = _pendingFocusRequestState ?? FocusState.Pointer;
return true;
}
else
{
return false;
}
}
else
{
Unfocus();
return true;
}
}
}
}
18 changes: 14 additions & 4 deletions src/Uno.UI/UI/Xaml/Controls/TextBox/TextBox.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
using Windows.UI.Xaml.Media;
using Windows.Foundation;
using Windows.UI.Core;
using Microsoft.Extensions.Logging;

namespace Windows.UI.Xaml.Controls
{
Expand Down Expand Up @@ -153,7 +154,8 @@ protected override void OnApplyTemplate()
public string Text
{
get => (string)this.GetValue(TextProperty);
set {
set
{
if (value == null)
{
throw new ArgumentNullException();
Expand Down Expand Up @@ -581,12 +583,15 @@ public TextAlignment TextAlignment

#endregion

protected override void OnFocusStateChanged(FocusState oldValue, FocusState newValue)
=> OnFocusStateChanged(oldValue, newValue, initial: false);
internal override void UpdateFocusState(FocusState focusState)
{
var oldValue = FocusState;
base.UpdateFocusState(focusState);
OnFocusStateChanged(oldValue, focusState, initial: false);
}

private void OnFocusStateChanged(FocusState oldValue, FocusState newValue, bool initial)
{
base.OnFocusStateChanged(oldValue, newValue);
OnFocusStateChangedPartial(newValue);

if (!initial && newValue == FocusState.Unfocused && _hasTextChangedThisFocusSession)
Expand Down Expand Up @@ -646,6 +651,11 @@ protected override void OnKeyDown(KeyRoutedEventArgs args)

private void UpdateButtonStates()
{
if (this.Log().IsEnabled(LogLevel.Debug))
{
this.Log().LogDebug(nameof(UpdateButtonStates));
}

if (Text.HasValue()
&& FocusState != FocusState.Unfocused
&& !IsReadOnly
Expand Down
Loading

0 comments on commit 21d7758

Please sign in to comment.