Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Handlers;
using Microsoft.Maui.Platform;
using Microsoft.UI.Xaml.Input;
Comment thread
jfversluis marked this conversation as resolved.
Outdated
using Xunit;
using WPanel = Microsoft.UI.Xaml.Controls.Panel;

Expand Down Expand Up @@ -151,5 +152,49 @@ await CreateHandlerAndAddToWindow(window,
Assert.True(windowRootView.NavigationViewControl.ButtonHolderGrid.Visibility == UI.Xaml.Visibility.Visible);
}));
}

[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task ModalPageDisablesHitTestOnUnderlyingPage(bool useColor)
{
SetupBuilder();

var navPage = new NavigationPage(new ContentPage() { Content = new Label() { Text = "Root Page" } });

await CreateHandlerAndAddToWindow<IWindowHandler>(new Window(navPage),
async (handler) =>
{
var windowRootViewContainer = (WPanel)handler.PlatformView.Content;
Comment thread
jfversluis marked this conversation as resolved.
Outdated
ContentPage modalPage = new ContentPage() { Content = new Label() { Text = "Modal Page" } };

if (useColor)
modalPage.BackgroundColor = Colors.Purple.WithAlpha(0.5f);
else
modalPage.Background = new SolidColorBrush(Colors.Purple.WithAlpha(0.5f));

var rootPageRootView = navPage.FindMauiContext().GetNavigationRootManager().RootView;

await navPage.CurrentPage.Navigation.PushModalAsync(modalPage);
await OnLoadedAsync(modalPage);

var modalRootView = modalPage.FindMauiContext().GetNavigationRootManager().RootView;

// The underlying page should have IsHitTestVisible disabled
Assert.False(rootPageRootView.IsHitTestVisible,
"Underlying page should have IsHitTestVisible=false when a modal is displayed");

// The modal page should have IsHitTestVisible enabled
Assert.True(modalRootView.IsHitTestVisible,
"Modal page should have IsHitTestVisible=true");

await navPage.CurrentPage.Navigation.PopModalAsync();
await OnUnloadedAsync(modalPage);

// After popping the modal, the underlying page should be interactive again
Assert.True(rootPageRootView.IsHitTestVisible,
"Underlying page should have IsHitTestVisible=true after modal is dismissed");
});
}
}
}
89 changes: 89 additions & 0 deletions src/Controls/tests/TestCases.HostApp/Issues/Issue22938.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
namespace Maui.Controls.Sample.Issues;

[Issue(IssueTracker.Github, 22938, "Keyboard focus does not shift to a newly opened modal page", PlatformAffected.All)]
Comment thread
jfversluis marked this conversation as resolved.
public class Issue22938 : ContentPage
{
public Issue22938()
{
var clickCountLabel = new Label
{
Text = "0",
AutomationId = "ClickCountLabel",
FontSize = 24
};

var mainPageButton = new Button
{
Text = "Click Me",
AutomationId = "MainPageButton",
Command = new Command(() =>
{
int count = int.Parse(clickCountLabel.Text);
clickCountLabel.Text = (count + 1).ToString();
})
};

var openModalButton = new Button
{
Text = "Open Modal",
AutomationId = "OpenModalButton",
Command = new Command(async () =>
{
var modalPage = new ContentPage
{
BackgroundColor = Colors.Beige,
Content = new VerticalStackLayout
{
Spacing = 20,
Padding = new Thickness(30),
VerticalOptions = LayoutOptions.Center,
Children =
{
new Label
{
Text = "Modal Page",
AutomationId = "ModalPageLabel",
FontSize = 24,
HorizontalOptions = LayoutOptions.Center
},
new Button
{
Text = "Close Modal",
AutomationId = "CloseModalButton",
Command = new Command(async () =>
{
await Navigation.PopModalAsync();
})
},
new Entry
{
Placeholder = "Focus target on modal",
AutomationId = "ModalEntry"
}
}
Comment thread
jfversluis marked this conversation as resolved.
}
};

await Navigation.PushModalAsync(modalPage);
})
};

Content = new VerticalStackLayout
{
Spacing = 20,
Padding = new Thickness(30),
VerticalOptions = LayoutOptions.Center,
Children =
{
new Label
{
Text = "Main Page - Issue 22938",
FontSize = 24
},
mainPageButton,
clickCountLabel,
openModalButton
}
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using NUnit.Framework;
using UITest.Appium;
using UITest.Core;

namespace Microsoft.Maui.TestCases.Tests.Issues;

public class Issue22938 : _IssuesUITest
{
public Issue22938(TestDevice device) : base(device)
{
}

public override string Issue => "Keyboard focus does not shift to a newly opened modal page";

[Test]
[Category(UITestCategories.Focus)]
public void ModalPageShouldReceiveKeyboardFocus()
{
App.WaitForElement("OpenModalButton");

// Open the modal page
App.Tap("OpenModalButton");

// Wait for modal to appear
App.WaitForElement("ModalPageLabel");

// Press Enter — if focus is correctly on the modal, this should NOT
// activate the MainPage button (click count should stay at 0)
App.PressEnter();

// Close the modal
App.Tap("CloseModalButton");

Comment thread
jfversluis marked this conversation as resolved.
// Wait for main page to reappear
App.WaitForElement("ClickCountLabel");

// Verify the main page button was NOT clicked by the Enter key
var clickCount = App.WaitForElement("ClickCountLabel").GetText();
Assert.That(clickCount, Is.EqualTo("0"),
"Enter key should not activate buttons on the page beneath a modal");
}
}
49 changes: 48 additions & 1 deletion src/Core/src/Platform/Windows/WindowRootViewContainer.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Diagnostics.CodeAnalysis;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Input;
using Windows.Foundation;

namespace Microsoft.Maui.Platform
Expand Down Expand Up @@ -56,12 +57,18 @@ internal void AddPage(FrameworkElement pageView)
{
if (!CachedChildren.Contains(pageView))
{
// Disable interaction on the page being covered by the modal
_topPage?.SetValue(IsHitTestVisibleProperty, false);

int indexOFTopPage = 0;
if (_topPage != null)
if (_topPage is not null)
indexOFTopPage = CachedChildren.IndexOf(_topPage) + 1;

CachedChildren.Insert(indexOFTopPage, pageView);
_topPage = pageView;

// Move keyboard focus to the new top page
TryMoveFocusToPage(_topPage);
}
}

Expand All @@ -74,9 +81,49 @@ internal void RemovePage(FrameworkElement pageView)
CachedChildren.Remove(pageView);

if (indexOFTopPage >= 0)
{
_topPage = (FrameworkElement)CachedChildren[indexOFTopPage];

// Re-enable interaction on the revealed page and restore focus
if (_topPage is not null)
{
_topPage.IsHitTestVisible = true;
TryMoveFocusToPage(_topPage);
}
}
else
{
_topPage = null;
}
}
Comment thread
jfversluis marked this conversation as resolved.

static void TryMoveFocusToPage(FrameworkElement page)
{
if (page.IsLoaded)
{
SetFocusToFirstElement(page);
}
else
{
Comment thread
jfversluis marked this conversation as resolved.
page.Loaded += OnPageLoadedForFocus;
}
}

static void OnPageLoadedForFocus(object sender, RoutedEventArgs e)
{
if (sender is FrameworkElement page)
{
page.Loaded -= OnPageLoadedForFocus;
SetFocusToFirstElement(page);
}
}

static void SetFocusToFirstElement(FrameworkElement page)
{
if (FocusManager.FindFirstFocusableElement(page) is Control focusable)
{
focusable.Focus(FocusState.Programmatic);
}
Comment thread
jfversluis marked this conversation as resolved.
Outdated
}

internal void AddOverlay(FrameworkElement overlayView)
Expand Down
Loading