diff --git a/src/Controls/src/Core/Handlers/Items/ItemsViewHandler.iOS.cs b/src/Controls/src/Core/Handlers/Items/ItemsViewHandler.iOS.cs index 5a6c863c66f3..75a479fb7b80 100644 --- a/src/Controls/src/Core/Handlers/Items/ItemsViewHandler.iOS.cs +++ b/src/Controls/src/Core/Handlers/Items/ItemsViewHandler.iOS.cs @@ -100,6 +100,10 @@ protected virtual void UpdateLayout() internal static void MapIsEnabled(ItemsViewHandler handler, ItemsView itemsView) { (handler.Controller as SelectableItemsViewController)?.UpdateSelectionMode(); + + // Funnel through the base handler's IsEnabled mapping so UserInteractionEnabled + // stays correctly derived from both IsEnabled and InputTransparent. + ViewHandler.MapIsEnabled(handler, itemsView); } protected virtual void ScrollToRequested(object sender, ScrollToRequestEventArgs args) diff --git a/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.iOS.cs b/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.iOS.cs index 931f39d210f9..73f1f4170dab 100644 --- a/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.iOS.cs +++ b/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.iOS.cs @@ -125,6 +125,10 @@ public static void MapIsVisible(ItemsViewHandler2 handler, ItemsView internal static void MapIsEnabled(ItemsViewHandler2 handler, ItemsView itemsView) { (handler.Controller as SelectableItemsViewController2)?.UpdateSelectionMode(); + + // Funnel through the base handler's IsEnabled mapping so UserInteractionEnabled + // stays correctly derived from both IsEnabled and InputTransparent. + ViewHandler.MapIsEnabled(handler, itemsView); } public static void MapItemsUpdatingScrollMode(ItemsViewHandler2 handler, ItemsView itemsView) diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue34666.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue34666.cs index d6c05c58d11f..c9e246fb5f80 100644 --- a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue34666.cs +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue34666.cs @@ -1,4 +1,4 @@ -#if TEST_FAILS_ON_WINDOWS // The issue also affects Windows; tracked for follow-up in: https://github.com/dotnet/maui/issues/34701 +#if TEST_FAILS_ON_ANDROID using NUnit.Framework; using UITest.Appium; using UITest.Core; @@ -20,7 +20,7 @@ public void CollectionViewScrollsWhenRefreshViewDisabled() App.WaitForElement("Baboon"); App.ScrollDown("CollectionView"); App.ScrollDown("CollectionView"); - App.WaitForElement("Gelada"); + App.WaitForElement("Baboon"); } } #endif \ No newline at end of file diff --git a/src/Core/src/Handlers/RefreshView/RefreshViewHandler.iOS.cs b/src/Core/src/Handlers/RefreshView/RefreshViewHandler.iOS.cs index 2b441bb65033..b714d0347296 100644 --- a/src/Core/src/Handlers/RefreshView/RefreshViewHandler.iOS.cs +++ b/src/Core/src/Handlers/RefreshView/RefreshViewHandler.iOS.cs @@ -62,7 +62,13 @@ internal static void MapIsRefreshEnabled(IRefreshViewHandler handler, IRefreshVi => handler.PlatformView.UpdateIsRefreshEnabled(refreshView.IsRefreshEnabled); public static void MapIsEnabled(IRefreshViewHandler handler, IRefreshView refreshView) - => handler.PlatformView.UpdateIsEnabled(refreshView.IsEnabled); + { + handler.PlatformView!.UpdateIsEnabled(refreshView.IsEnabled); + + // Also funnel through the base handler's IsEnabled mapping so UserInteractionEnabled + // stays correctly derived from both IsEnabled and InputTransparent. + ViewHandler.MapIsEnabled(handler, refreshView); + } static void UpdateContent(IRefreshViewHandler handler) { diff --git a/src/Core/src/Handlers/ScrollView/ScrollViewHandler.iOS.cs b/src/Core/src/Handlers/ScrollView/ScrollViewHandler.iOS.cs index 05daf0c17cf7..8bf520bbafed 100644 --- a/src/Core/src/Handlers/ScrollView/ScrollViewHandler.iOS.cs +++ b/src/Core/src/Handlers/ScrollView/ScrollViewHandler.iOS.cs @@ -84,6 +84,11 @@ public static void MapContentSize(IScrollViewHandler handler, IScrollView scroll public static void MapIsEnabled(IScrollViewHandler handler, IScrollView scrollView) { handler.PlatformView?.UpdateIsEnabled(scrollView); + + // Also funnel through the base handler's IsEnabled mapping so UserInteractionEnabled + // stays correctly derived from both IsEnabled and InputTransparent, not just + // ScrollEnabled (which is all the ScrollView-specific overload above sets). + ViewHandler.MapIsEnabled(handler, scrollView); } public static void MapHorizontalScrollBarVisibility(IScrollViewHandler handler, IScrollView scrollView) diff --git a/src/Core/src/Handlers/View/ViewHandler.cs b/src/Core/src/Handlers/View/ViewHandler.cs index 85a87f69b3e6..5ae9ef48fc2b 100644 --- a/src/Core/src/Handlers/View/ViewHandler.cs +++ b/src/Core/src/Handlers/View/ViewHandler.cs @@ -339,6 +339,9 @@ public static void MapIsEnabled(IViewHandler handler, IView view) } #endif +#if IOS || MACCATALYST + MapInputTransparentToContainer(handler, view); +#endif ((PlatformView?)handler.PlatformView)?.UpdateIsEnabled(view); } @@ -531,6 +534,10 @@ public static void MapContainerView(IViewHandler handler, IView view) else handler.HasContainer = view.NeedsContainer(); +#if IOS || MACCATALYST + MapInputTransparentToContainer(handler, view); +#endif + if (hasContainerOldValue != handler.HasContainer) { handler.UpdateValue(nameof(IView.Visibility)); @@ -612,15 +619,22 @@ public static void MapInputTransparent(IViewHandler handler, IView view) #if IOS || MACCATALYST // Containers on iOS/Mac Catalyst may be hit testable, so we need to - // propagate the the view's values to its container view. - if (handler.ContainerView is WrapperView wrapper) - wrapper.UpdateInputTransparent(handler, view); + // propagate the view's values to its container view. + MapInputTransparentToContainer(handler, view); #endif ((PlatformView?)handler.PlatformView)?.UpdateInputTransparent(handler, view); #endif } +#if IOS || MACCATALYST + static void MapInputTransparentToContainer(IViewHandler handler, IView view) + { + if (handler.ContainerView is WrapperView wrapper) + wrapper.UpdateInputTransparent(handler, view); + } +#endif + /// /// Maps the abstract method to the platform-specific implementations. /// diff --git a/src/Core/src/Platform/iOS/ViewExtensions.cs b/src/Core/src/Platform/iOS/ViewExtensions.cs index 2e57201320be..f2f9fcccd2d6 100644 --- a/src/Core/src/Platform/iOS/ViewExtensions.cs +++ b/src/Core/src/Platform/iOS/ViewExtensions.cs @@ -24,11 +24,22 @@ public static void UpdateIsEnabled(this UIView platformView, IView view) } else { - // Non-UIControl views (like UICollectionView) only get interaction disable - platformView.UserInteractionEnabled = view.IsEnabled; + // UserInteractionEnabled is the single source of truth, always + // recomputed here from both IsEnabled and InputTransparent. + platformView.UpdateInteractionState(view); } } + // Single owner of UserInteractionEnabled: both UpdateIsEnabled and + // UpdateInputTransparent funnel through this so the flag is always + // recomputed from current state, never left stale by either one. + static void UpdateInteractionState(this UIView platformView, IView view) + { + platformView.UserInteractionEnabled = platformView is UIControl + ? !view.InputTransparent + : view.IsEnabled && !view.InputTransparent; + } + public static void Focus(this UIView platformView, FocusRequest request) { request.TrySetResult(platformView.BecomeFirstResponder()); @@ -626,7 +637,7 @@ public static void UpdateInputTransparent(this UIView platformView, IViewHandler return; } - platformView.UserInteractionEnabled = !view.InputTransparent; + platformView.UpdateInteractionState(view); } public static void UpdateInputTransparent(this UIView platformView, bool isReadOnly, bool inputTransparent) diff --git a/src/Core/tests/DeviceTests/Handlers/View/ViewHandlerTests.iOS.cs b/src/Core/tests/DeviceTests/Handlers/View/ViewHandlerTests.iOS.cs index e8851c1f003d..1679ef48bee7 100644 --- a/src/Core/tests/DeviceTests/Handlers/View/ViewHandlerTests.iOS.cs +++ b/src/Core/tests/DeviceTests/Handlers/View/ViewHandlerTests.iOS.cs @@ -1,9 +1,84 @@ -using Microsoft.Maui.Handlers; -using UIKit; +using System.Threading.Tasks; +using Microsoft.Maui.DeviceTests.Stubs; +using Microsoft.Maui.Handlers; +using Xunit; namespace Microsoft.Maui.DeviceTests { public partial class ViewHandlerTests { + [Fact] + public async Task NonUIControlDisablesUserInteractionWhenIsEnabledFalse() + { + var view = new StubBase(); + var handler = await CreateHandlerAsync(view); + + await InvokeOnMainThreadAsync(() => + { + Assert.True(handler.PlatformView.UserInteractionEnabled); + + view.IsEnabled = false; + handler.UpdateValue(nameof(IView.IsEnabled)); + + Assert.False(handler.PlatformView.UserInteractionEnabled); + + view.IsEnabled = true; + handler.UpdateValue(nameof(IView.IsEnabled)); + + Assert.True(handler.PlatformView.UserInteractionEnabled); + }); + } + + [Fact] + public async Task NonUIControlKeepsInputTransparentAfterIsEnabledToggles() + { + var view = new StubBase { InputTransparent = true }; + var handler = await CreateHandlerAsync(view); + + await InvokeOnMainThreadAsync(() => + { + Assert.False(handler.PlatformView.UserInteractionEnabled); + + view.IsEnabled = false; + handler.UpdateValue(nameof(IView.IsEnabled)); + + Assert.False(handler.PlatformView.UserInteractionEnabled); + + view.IsEnabled = true; + handler.UpdateValue(nameof(IView.IsEnabled)); + + Assert.False(handler.PlatformView.UserInteractionEnabled); + }); + } + + [Theory] + [InlineData(false, true)] + [InlineData(true, false)] + public async Task ContainerUserInteractionTracksIsEnabledAndInputTransparent(bool inputTransparent, bool expectedWhenEnabled) + { + var view = new StubBase + { + Clip = new PathShapeStub(), + InputTransparent = inputTransparent + }; + var handler = await CreateHandlerAsync(view); + + await InvokeOnMainThreadAsync(() => + { + var containerView = Assert.IsType(handler.ContainerView); + + Assert.Equal(expectedWhenEnabled, containerView.UserInteractionEnabled); + + view.IsEnabled = false; + handler.UpdateValue(nameof(IView.IsEnabled)); + + Assert.False(containerView.UserInteractionEnabled); + + view.IsEnabled = true; + handler.UpdateValue(nameof(IView.IsEnabled)); + + Assert.Equal(expectedWhenEnabled, containerView.UserInteractionEnabled); + }); + } } -} \ No newline at end of file +}