Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -221,23 +221,8 @@ internal void SetHeaderContentInset()
return;
}

var headerBehavior = _context.Shell.FlyoutHeaderBehavior;
if (headerBehavior == FlyoutHeaderBehavior.Default || headerBehavior == FlyoutHeaderBehavior.Fixed)
{
// For Default/Fixed, the scroll view frame is positioned below the header by LayoutContent,
// so no top content inset is needed and no content offset compensation should be applied.
// Applying the compensation (offset = oldInset - 0) would incorrectly scroll the content
// down by the old inset amount, hiding the first flyout item behind the header.
ScrollView.ContentInset = new UIEdgeInsets(0, 0, 0, 0);
UpdateVerticalScrollMode();
return;
}
else
{
// For Scroll/CollapseOnScroll, the scroll view overlaps the header so the header
// can scroll away or shrink. We use content inset to push items below it initially.
ScrollView.ContentInset = new UIEdgeInsets((nfloat)Math.Max(HeaderMinimumHeight, MeasuredHeaderViewHeightWithNoMargin), 0, 0, 0);
}
// We take the measured header height without margin, since the margin is already accounted for in the positioning of the scroll view itself.
ScrollView.ContentInset = new UIEdgeInsets((nfloat)Math.Max(HeaderMinimumHeight, MeasuredHeaderViewHeightWithNoMargin), 0, 0, 0);
}
else
{
Expand Down Expand Up @@ -343,19 +328,9 @@ void LayoutContent(CGRect parentBounds, nfloat footerHeight)
}
else
{
var headerBehavior = _context.Shell.FlyoutHeaderBehavior;
if (headerBehavior == FlyoutHeaderBehavior.Default || headerBehavior == FlyoutHeaderBehavior.Fixed)
{
// For Default/Fixed, position the scroll view below the header so items
// cannot scroll behind it. No content inset is needed in this case.
contentYOffset += HeaderView.Frame.Height;
}
else
{
// For Scroll/CollapseOnScroll, the scroll view overlaps the header so the header
// can scroll away or shrink. The content inset is managed by SetHeaderContentInset.
contentYOffset += HeaderView.View.Margin.VerticalThickness;
}
// For ScrollView, we need to consider the margin, but we should not consider the header height, since it should overlap with the scroll view.
// The content inset is already managed by SetHeaderContentInset.
contentYOffset += HeaderView.View.Margin.VerticalThickness;
}
}

Expand Down
65 changes: 7 additions & 58 deletions src/Controls/tests/DeviceTests/Elements/Shell/ShellFlyoutTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -233,19 +233,18 @@ await RunShellTest(shell =>
var expectedContentY = headerMargin.Top + headerMargin.Bottom + contentMargin.Top;

#if IOS
if (contentType == "ScrollView" &&
(behavior == FlyoutHeaderBehavior.Scroll || behavior == FlyoutHeaderBehavior.CollapseOnScroll))
if (contentType != "ScrollView")
#endif
{
// For Scroll/CollapseOnScroll, the scroll view overlaps the header so the header
// can scroll away or shrink. Content is offset via ContentInset, not frame position.
var scrollViewContentInsetTop = ((UIScrollView)((IView)shell.FlyoutContent).Handler.PlatformView).ContentInset.Top;
AssertionExtensions.CloseEnough(headerFrame.Height, scrollViewContentInsetTop, message: "Content ScrollView Inset Y");
expectedContentY += headerFrame.Height;
}
#if IOS
else
#endif
{
expectedContentY += headerFrame.Height;
var scrollViewContentInsetTop = ((UIScrollView)((IView)shell.FlyoutContent).Handler.PlatformView).ContentInset.Top;
AssertionExtensions.CloseEnough(headerFrame.Height, scrollViewContentInsetTop, message: "Content ScrollView Inset Y");
}
#endif
Comment on lines 235 to +247

AssertionExtensions.CloseEnough(0, contentFrame.X, message: "Content X");
AssertionExtensions.CloseEnough(expectedContentY, contentFrame.Y, epsilon: 0.5, message: "Content Y");
Expand All @@ -261,56 +260,6 @@ await RunShellTest(shell =>
AssertionExtensions.CloseEnough(expectedFooterY + footerFrame.Height, flyoutFrame.Height, epsilon: 0.5, message: "Total Height");
});
}
// Regression test for https://github.com/dotnet/maui/issues/34925
// For Default/Fixed header behavior, the scroll view should be positioned below the header
// (ContentInset.Top = 0) so items cannot scroll behind the header.
[Theory]
[InlineData(FlyoutHeaderBehavior.Default)]
[InlineData(FlyoutHeaderBehavior.Fixed)]
public async Task FlyoutScrollViewDoesNotOverlapHeaderForDefaultAndFixed(FlyoutHeaderBehavior behavior)
{
var headerHeight = 150;

await RunShellTest(shell =>
{
shell.FlyoutHeaderBehavior = behavior;
shell.FlyoutHeader = new VerticalStackLayout
{
HeightRequest = headerHeight,
BackgroundColor = Colors.Blue,
Children = { new Label { Text = "Header" } }
};

// Add enough items to require scrolling
for (int i = 0; i < 20; i++)
{
shell.Items.Add(new FlyoutItem
{
Title = $"Page {i}",
Items = { new ShellContent { ContentTemplate = new DataTemplate(() => new ContentPage()) } }
});
}
},
async (shell, handler) =>
{
await OpenFlyout(handler);

var flyoutView = GetFlyoutPlatformView(handler);
var scrollView = flyoutView.FindDescendantView<UIScrollView>();
Assert.NotNull(scrollView);

var headerFrame = GetFrameRelativeToFlyout(handler, (IView)shell.FlyoutHeader);
var headerBottom = headerFrame.Y + headerFrame.Height;

// For Default/Fixed, the scroll view should be positioned below the header,
// not overlapping it with a content inset. When the scroll view overlaps the header,
// items become visible behind a semi-transparent header when scrolling.
Assert.True(
scrollView.Frame.Y >= headerBottom - 0.5,
$"ScrollView frame (Y={scrollView.Frame.Y}) should start at or below header bottom ({headerBottom}). " +
$"ContentInset.Top={scrollView.ContentInset.Top}. Items can scroll behind the header (issue #34925).");
});
}
#endif
#endif

Expand Down
Loading