Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
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
4 changes: 4 additions & 0 deletions src/Controls/src/Core/Handlers/Items/ItemsViewHandler.iOS.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,10 @@ protected virtual void UpdateLayout()
internal static void MapIsEnabled(ItemsViewHandler<TItemsView> handler, ItemsView itemsView)
{
(handler.Controller as SelectableItemsViewController<ReorderableItemsView>)?.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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,10 @@ public static void MapIsVisible(ItemsViewHandler2<TItemsView> handler, ItemsView
internal static void MapIsEnabled(ItemsViewHandler2<TItemsView> handler, ItemsView itemsView)
{
(handler.Controller as SelectableItemsViewController2<ReorderableItemsView>)?.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<TItemsView> handler, ItemsView itemsView)
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -20,7 +20,7 @@ public void CollectionViewScrollsWhenRefreshViewDisabled()
App.WaitForElement("Baboon");
App.ScrollDown("CollectionView");
App.ScrollDown("CollectionView");
App.WaitForElement("Gelada");
App.WaitForElement("Baboon");
}
}
#endif
8 changes: 7 additions & 1 deletion src/Core/src/Handlers/RefreshView/RefreshViewHandler.iOS.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down
5 changes: 5 additions & 0 deletions src/Core/src/Handlers/ScrollView/ScrollViewHandler.iOS.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
20 changes: 17 additions & 3 deletions src/Core/src/Handlers/View/ViewHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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

/// <summary>
/// Maps the abstract <see cref="IView.Unfocus"/> method to the platform-specific implementations.
/// </summary>
Expand Down
17 changes: 14 additions & 3 deletions src/Core/src/Platform/iOS/ViewExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down Expand Up @@ -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)
Expand Down
81 changes: 78 additions & 3 deletions src/Core/tests/DeviceTests/Handlers/View/ViewHandlerTests.iOS.cs
Original file line number Diff line number Diff line change
@@ -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<WrapperView>(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);
});
}
}
}
}
Loading