Merge pull request #481 from irihitech/issue-479

Allow user to set Modal Dialog initial focus element
This commit is contained in:
Dong Bin
2024-11-13 16:53:27 +08:00
committed by GitHub
12 changed files with 221 additions and 10 deletions

View File

@@ -17,7 +17,10 @@
<GradientStop Offset="0.9" Color="{DynamicResource SemiLightBlue1Color}" />
</LinearGradientBrush>
</UserControl.Background>
<Grid Margin="24" RowDefinitions="Auto, *, Auto" MinWidth="400">
<Grid
MinWidth="400"
Margin="24"
RowDefinitions="Auto, *, Auto">
<TextBlock
Grid.Row="0"
Margin="8"
@@ -45,7 +48,10 @@
Grid.Column="1"
Margin="32,8,0,8"
Label="Owner">
<TextBox u:FormItem.Label="Owner" Text="{Binding Owner}" />
<TextBox
u:FocusHelper.DialogFocusHint="True"
u:FormItem.Label="Owner"
Text="{Binding Owner}" />
</u:FormItem>
<u:FormItem
Grid.Row="1"
@@ -69,9 +75,20 @@
HorizontalAlignment="Right"
Orientation="Horizontal"
Spacing="8">
<Button Command="{Binding DialogCommand}" Content="Dialog" Theme="{DynamicResource SolidButton}" />
<Button Command="{Binding OKCommand}" Content="OK" Theme="{DynamicResource SolidButton}" Classes="Tertiary"/>
<Button Command="{Binding CancelCommand}" Content="Cancel" Theme="{DynamicResource SolidButton}" Classes="Tertiary"/>
<Button
Command="{Binding DialogCommand}"
Content="Dialog"
Theme="{DynamicResource SolidButton}" />
<Button
Classes="Tertiary"
Command="{Binding OKCommand}"
Content="OK"
Theme="{DynamicResource SolidButton}" />
<Button
Classes="Tertiary"
Command="{Binding CancelCommand}"
Content="Cancel"
Theme="{DynamicResource SolidButton}" />
<ComboBox>
<ComboBoxItem>A</ComboBoxItem>
<ComboBoxItem>B</ComboBoxItem>
@@ -79,5 +96,5 @@
</ComboBox>
</StackPanel>
</Grid>
</UserControl>

View File

@@ -3,4 +3,5 @@ using Avalonia.Metadata;
[assembly:XmlnsPrefix("https://irihi.tech/ursa", "u")]
[assembly:XmlnsDefinition("https://irihi.tech/ursa", "Ursa")]
[assembly:XmlnsDefinition("https://irihi.tech/ursa", "Ursa.Controls")]
[assembly:XmlnsDefinition("https://irihi.tech/ursa", "Ursa.Controls.Shapes")]
[assembly:XmlnsDefinition("https://irihi.tech/ursa", "Ursa.Controls.Shapes")]
[assembly:XmlnsDefinition("https://irihi.tech/ursa", "Ursa.Helpers")]

View File

@@ -4,6 +4,7 @@ using Avalonia.VisualTree;
using Irihi.Avalonia.Shared.Helpers;
using Irihi.Avalonia.Shared.Shapes;
using Ursa.Controls.OverlayShared;
using Ursa.Helpers;
namespace Ursa.Controls;
@@ -111,14 +112,20 @@ public partial class OverlayDialogHost
SetToPosition(control);
control.AddHandler(OverlayFeedbackElement.ClosedEvent, OnDialogControlClosing);
control.AddHandler(DialogControlBase.LayerChangedEvent, OnDialogLayerChanged);
// Notice: mask animation here is not really awaited, because currently dialogs appears immediately.
if (!IsAnimationDisabled) MaskAppearAnimation.RunAsync(mask);
var element = control.GetVisualDescendants().OfType<InputElement>().FirstOrDefault(a => a.Focusable);
var element = control.GetVisualDescendants().OfType<InputElement>()
.FirstOrDefault(FocusHelper.GetDialogFocusHint);
if (element is null)
{
element = control.GetVisualDescendants().OfType<InputElement>().FirstOrDefault(a => a.Focusable);
}
element?.Focus();
_modalCount++;
IsInModalStatus = _modalCount > 0;
control.IsClosed = false;
control.Focus();
// control.Focus();
}
// Handle dialog layer change event

View File

@@ -9,6 +9,7 @@ using Irihi.Avalonia.Shared.Shapes;
using Ursa.Common;
using Ursa.Controls.OverlayShared;
using Ursa.EventArgs;
using Ursa.Helpers;
namespace Ursa.Controls;
@@ -69,7 +70,13 @@ public partial class OverlayDialogHost
{
await Task.WhenAll(animation.RunAsync(control), MaskAppearAnimation.RunAsync(mask));
}
var element = control.GetVisualDescendants().OfType<InputElement>().FirstOrDefault(a => a.Focusable);
var element = control.GetVisualDescendants().OfType<InputElement>()
.FirstOrDefault(FocusHelper.GetDialogFocusHint);
if (element is null)
{
element = control.GetVisualDescendants().OfType<InputElement>().FirstOrDefault(a => a.Focusable);
}
element?.Focus();
}

View File

@@ -0,0 +1,13 @@
using Avalonia;
using Avalonia.Input;
namespace Ursa.Helpers;
public class FocusHelper
{
public static readonly AttachedProperty<bool> DialogFocusHintProperty =
AvaloniaProperty.RegisterAttached<FocusHelper, InputElement, bool>("DialogFocusHint");
public static void SetDialogFocusHint(InputElement obj, bool value) => obj.SetValue(DialogFocusHintProperty, value);
public static bool GetDialogFocusHint(InputElement obj) => obj.GetValue(DialogFocusHintProperty);
}

View File

@@ -0,0 +1,10 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:u="https://irihi.tech/ursa"
mc:Ignorable="d" d:DesignWidth="800"
d:DesignHeight="450"
x:Class="HeadlessTest.Ursa.Controls.OverlayShared.Dialog_Primary_Focus.FocusDialog">
<TextBox Text="Hello" u:FocusHelper.DialogFocusHint="True"></TextBox>
</UserControl>

View File

@@ -0,0 +1,13 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace HeadlessTest.Ursa.Controls.OverlayShared.Dialog_Primary_Focus;
public partial class FocusDialog : UserControl
{
public FocusDialog()
{
InitializeComponent();
}
}

View File

@@ -0,0 +1,9 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800"
d:DesignHeight="450"
x:Class="HeadlessTest.Ursa.Controls.OverlayShared.Dialog_Primary_Focus.NormalDialog">
<TextBox Text="Hello"></TextBox>
</UserControl>

View File

@@ -0,0 +1,13 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace HeadlessTest.Ursa.Controls.OverlayShared.Dialog_Primary_Focus;
public partial class NormalDialog : UserControl
{
public NormalDialog()
{
InitializeComponent();
}
}

View File

@@ -0,0 +1,74 @@
using Avalonia.Controls;
using Avalonia.Headless.XUnit;
using Avalonia.Threading;
using Avalonia.VisualTree;
using Ursa.Controls;
namespace HeadlessTest.Ursa.Controls.OverlayShared.Dialog_Primary_Focus;
public class Test
{
[AvaloniaFact]
public async Task Normal_Drawer_Focus_On_Border()
{
var ursaWindow = new TestWindow();
ursaWindow.Show();
ursaWindow.InvokeNormalDrawer();
Dispatcher.UIThread.RunJobs();
await Task.Delay(500);
var dialog = ursaWindow.GetVisualDescendants().OfType<DefaultDrawerControl>().FirstOrDefault();
Assert.NotNull(dialog);
var border = dialog.GetVisualDescendants().OfType<Border>().FirstOrDefault(a=>a.Name == "PART_Root");
var text = dialog.GetVisualDescendants().OfType<TextBox>().FirstOrDefault();
Assert.True(border?.IsFocused);
Assert.False(text?.IsFocused);
}
[AvaloniaFact]
public async Task Focus_Drawer_Focus_On_Primary()
{
var ursaWindow = new TestWindow();
ursaWindow.Show();
ursaWindow.InvokeFocusDrawer();
Dispatcher.UIThread.RunJobs();
await Task.Delay(500);
var dialog = ursaWindow.GetVisualDescendants().OfType<DefaultDrawerControl>().FirstOrDefault();
Assert.NotNull(dialog);
var border = dialog.GetVisualDescendants().OfType<Border>().FirstOrDefault(a=>a.Name == "PART_Root");
var text = dialog.GetVisualDescendants().OfType<TextBox>().FirstOrDefault();
Assert.False(border?.IsFocused);
Assert.True(text?.IsFocused);
}
[AvaloniaFact]
public async Task Normal_Dialog_Focus_On_Border()
{
var ursaWindow = new TestWindow();
ursaWindow.Show();
ursaWindow.InvokeNormalDialog();
Dispatcher.UIThread.RunJobs();
await Task.Delay(100);
var dialog = ursaWindow.GetVisualDescendants().OfType<DefaultDialogControl>().FirstOrDefault();
Assert.NotNull(dialog);
var border = dialog.GetVisualDescendants().OfType<Border>().FirstOrDefault(a=>a.Name == "PART_Border");
var text = dialog.GetVisualDescendants().OfType<TextBox>().FirstOrDefault();
Assert.True(border?.IsFocused);
Assert.False(text?.IsFocused);
}
[AvaloniaFact]
public async Task Focus_Dialog_Focus_On_Primary()
{
var ursaWindow = new TestWindow();
ursaWindow.Show();
ursaWindow.InvokeFocusDialog();
Dispatcher.UIThread.RunJobs();
await Task.Delay(100);
var dialog = ursaWindow.GetVisualDescendants().OfType<DefaultDialogControl>().FirstOrDefault();
Assert.NotNull(dialog);
var border = dialog.GetVisualDescendants().OfType<Border>().FirstOrDefault(a=>a.Name == "PART_Border");
var text = dialog.GetVisualDescendants().OfType<TextBox>().FirstOrDefault();
Assert.False(border?.IsFocused);
Assert.True(text?.IsFocused);
}
}

View File

@@ -0,0 +1,11 @@
<u:UrsaWindow xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:u="https://irihi.tech/ursa"
mc:Ignorable="d" d:DesignWidth="800"
d:DesignHeight="450"
x:Class="HeadlessTest.Ursa.Controls.OverlayShared.Dialog_Primary_Focus.TestWindow"
Title="TestWindow">
Welcome to Avalonia!
</u:UrsaWindow>

View File

@@ -0,0 +1,36 @@
using Ursa.Controls;
using Ursa.Controls.Options;
namespace HeadlessTest.Ursa.Controls.OverlayShared.Dialog_Primary_Focus;
public partial class TestWindow : UrsaWindow
{
public TestWindow()
{
InitializeComponent();
}
public void InvokeNormalDrawer()
{
Drawer.ShowModal<NormalDialog, object>("Hello World",
options: new DrawerOptions() { TopLevelHashCode = GetHashCode() });
}
public void InvokeFocusDrawer()
{
Drawer.ShowModal<FocusDialog, object>("Hello World",
options: new DrawerOptions() { TopLevelHashCode = GetHashCode() });
}
public void InvokeNormalDialog()
{
OverlayDialog.ShowModal<NormalDialog, object>("Hello World",
options: new OverlayDialogOptions() { TopLevelHashCode = GetHashCode() });
}
public void InvokeFocusDialog()
{
OverlayDialog.ShowModal<FocusDialog, object>("Hello World",
options: new OverlayDialogOptions() { TopLevelHashCode = GetHashCode() });
}
}