diff --git a/src/MaterialDesignThemes.Wpf/DialogHost.cs b/src/MaterialDesignThemes.Wpf/DialogHost.cs index d947bd10e9..f0b39e45b9 100644 --- a/src/MaterialDesignThemes.Wpf/DialogHost.cs +++ b/src/MaterialDesignThemes.Wpf/DialogHost.cs @@ -431,17 +431,28 @@ private static void IsOpenPropertyChangedCallback(DependencyObject dependencyObj dialogHost.DialogOpenedCallback?.Invoke(dialogHost, dialogOpenedEventArgs); dialogHost._asyncShowOpenedEventHandler?.Invoke(dialogHost, dialogOpenedEventArgs); - //https://github.com/MaterialDesignInXAML/MaterialDesignInXamlToolkit/issues/187 - //totally not happy about this, but on immediate validation we can get some weird looking stuff...give WPF a kick to refresh... - Task.Delay(300).ContinueWith(t => dialogHost.Dispatcher.BeginInvoke(DispatcherPriority.Background, new Action(() => + if (TransitionAssist.GetDisableTransitions(dialogHost)) { - CommandManager.InvalidateRequerySuggested(); + //Without a transition there is nothing to wait for: the dialog is complete on the next frame, + //and waiting would send the first keys typed into it to the controls behind the dialog + dialogHost.Dispatcher.BeginInvoke(DispatcherPriority.Background, new Action(OnDialogShown)); + } + else + { + //https://github.com/MaterialDesignInXAML/MaterialDesignInXamlToolkit/issues/187 + //totally not happy about this, but on immediate validation we can get some weird looking stuff...give WPF a kick to refresh... //Delay focusing the popup until after the animation has some time, Issue #2912 - UIElement? child = dialogHost.FocusPopup(); + Task.Delay(300).ContinueWith(t => dialogHost.Dispatcher.BeginInvoke(DispatcherPriority.Background, new Action(OnDialogShown))); + } - child?.InvalidateVisual(); + void OnDialogShown() + { + CommandManager.InvalidateRequerySuggested(); + //If the content has already moved the focus into the dialog (for example when it loaded), leave it there + UIElement? child = dialogHost.FocusPopup(keepFocusWithin: true); - }))); + child?.InvalidateVisual(); + } } /// @@ -891,13 +902,16 @@ internal void InternalClose(object? parameter) /// /// Attempts to focus the content of a popup. /// + /// When true, the focus is left alone if it is already inside the popup content. /// The popup content. - internal UIElement? FocusPopup() + internal UIElement? FocusPopup(bool keepFocusWithin = false) { var child = _popup?.Child ?? _popupContentControl; if (child is null) return null; CommandManager.InvalidateRequerySuggested(); + if (keepFocusWithin && child.IsKeyboardFocusWithin) return child; + var focusable = child.VisualDepthFirstTraversal().OfType().FirstOrDefault(ui => ui.Focusable); if (focusable is null) return null; diff --git a/tests/MaterialDesignThemes.UITests/WPF/DialogHosts/DialogHostTests.cs b/tests/MaterialDesignThemes.UITests/WPF/DialogHosts/DialogHostTests.cs index c6f7630817..1bd5c0a276 100644 --- a/tests/MaterialDesignThemes.UITests/WPF/DialogHosts/DialogHostTests.cs +++ b/tests/MaterialDesignThemes.UITests/WPF/DialogHosts/DialogHostTests.cs @@ -574,12 +574,99 @@ public async Task DialogHost_TrapsFocusInsidePopup_WhenTabbing(string dialogHost await Wait.For(async () => await Assert.That(await textBoxOne.GetIsFocused()).IsTrue()); recorder.Success(); + } + + [Test] + [Description("Issue 4097")] + [Arguments("")] + [Arguments("MaterialDesignEmbeddedDialogHost")] + public async Task DialogHost_WithTransitionsDisabled_FocusesDialogWithoutWaitingForTransition(string dialogHostStyle) + { + await using var recorder = new TestRecorder(App); + + var dialogHost = await LoadXaml(""" + + + + + + """); + + if (!string.IsNullOrEmpty(dialogHostStyle)) + { + await dialogHost.RemoteExecute(SetDialogHostStyle, dialogHostStyle); + } + + string? focusedElementName = await dialogHost.RemoteExecute(OpenAndGetFocusedElementName); + + await Assert.That(focusedElementName).IsEqualTo("DialogTextBox"); + + recorder.Success(); + + static async Task OpenAndGetFocusedElementName(DialogHost dialogHost) + { + // Opened and closed once first, so the check below does not include loading the templates + dialogHost.IsOpen = true; + await Task.Delay(500); + dialogHost.IsOpen = false; + await Task.Delay(500); + + dialogHost.IsOpen = true; + // Well within the 300 ms the dialog host waits for when there is a transition + await Task.Delay(100); + return (Keyboard.FocusedElement as FrameworkElement)?.Name; + } + } + + [Test] + [Description("Issue 4097")] + [Arguments("")] + [Arguments("MaterialDesignEmbeddedDialogHost")] + public async Task DialogHost_FocusMovedInsideDialogWhileOpening_IsKept(string dialogHostStyle) + { + await using var recorder = new TestRecorder(App); - static object SetDialogHostStyle(DialogHost dialogHost, string styleName) + var dialogHost = await LoadXaml(""" + + + + + + + + + """); + + if (!string.IsNullOrEmpty(dialogHostStyle)) { - var style = (Style)dialogHost.FindResource(styleName); - dialogHost.Style = style; - return null!; + await dialogHost.RemoteExecute(SetDialogHostStyle, dialogHostStyle); + } + + string? focusedElementName = await dialogHost.RemoteExecute(OpenFocusSecondTextBoxAndWait); + + await Assert.That(focusedElementName).IsEqualTo("TextBoxTwo"); + + recorder.Success(); + + static async Task OpenFocusSecondTextBoxAndWait(DialogHost dialogHost) + { + dialogHost.IsOpen = true; + await Task.Delay(50); + + // As when the user clicks the second TextBox right after the dialog appears + var content = (Panel)dialogHost.DialogContent!; + Keyboard.Focus(content.Children[1]); + + // Past the delay after which the dialog host moves the focus into the dialog + await Task.Delay(800); + return (Keyboard.FocusedElement as FrameworkElement)?.Name; } } + + private static object SetDialogHostStyle(DialogHost dialogHost, string styleName) + { + var style = (Style)dialogHost.FindResource(styleName); + dialogHost.Style = style; + return null!; + } }