fix(fabric): force overlay scrollbar style in ScrollView#2907
fix(fabric): force overlay scrollbar style in ScrollView#2907
Conversation
|
|
/backport 0.81-stable |
Backport results
|
Force NSScrollerStyleOverlay on Fabric ScrollViews to avoid layout issues with legacy (always-visible) scrollbars. Legacy scrollbars sit inside the NSScrollView frame and reduce the clip view's visible area, which would require compensating padding in the Yoga shadow tree. Overlay scrollers float above content, so no layout compensation is needed. Additional fixes: - Remove autoresizingMask from documentView to prevent AppKit frame corruption during tile/resize - Remove the layoutSubviews workaround (no longer needed without autoresizingMask) - Add [_scrollView tile] after content size updates to re-evaluate scroller visibility - Fix hit testing: check NSScroller before content subviews so scrollbar clicks aren't swallowed by full-width content views Fixes #2857 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
3f66ad7 to
ef37f10
Compare
| _containerView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; | ||
| // Force overlay scrollbar style. Overlay scrollers float above content and | ||
| // don't reduce the clip view's visible area, so no layout compensation is | ||
| // needed. Legacy (always-visible) scrollbars sit inside the frame and would | ||
| // require padding adjustments in the Yoga shadow tree — avoiding that | ||
| // complexity is the main motivation for forcing overlay style. | ||
| _scrollView.scrollerStyle = NSScrollerStyleOverlay; | ||
| // Do NOT set autoresizingMask on the documentView. AppKit's autoresizing | ||
| // corrupts the documentView frame during tile/resize (adding the clip view's | ||
| // size delta to the container, inflating it beyond the actual content size). | ||
| // React manages the documentView frame directly via updateState:. | ||
| [_scrollView setDocumentView:_containerView]; |
There was a problem hiding this comment.
We can probably minimize this comment
| // The user changed the system "Show scroll bars" preference. Re-force | ||
| // overlay style so legacy scrollbars don't appear and cause layout issues. | ||
| _scrollView.scrollerStyle = NSScrollerStyleOverlay; | ||
| [_scrollView tile]; |
There was a problem hiding this comment.
Likewise with this comment
| #if TARGET_OS_OSX // [macOS | ||
| // Force the scroll view to re-evaluate which scrollers should be visible. | ||
| [_scrollView tile]; | ||
| #endif // macOS] |
There was a problem hiding this comment.
Are we sure this isn't called as a side effect of setting content size or anything?
| #if TARGET_OS_OSX // [macOS | ||
| // Check if the hit lands on a scrollbar (NSScroller) BEFORE checking content | ||
| // subviews. Scrollers are subviews of the NSScrollView, not the documentView | ||
| // (_containerView). They must be checked first because content views typically | ||
| // fill the entire visible area and would otherwise swallow scroller clicks. | ||
| if (isPointInside) { | ||
| NSPoint scrollViewPoint = [_scrollView convertPoint:point fromView:self]; | ||
| NSView *scrollViewHit = [_scrollView hitTest:scrollViewPoint]; | ||
| if ([scrollViewHit isKindOfClass:[NSScroller class]]) { | ||
| return (RCTPlatformView *)scrollViewHit; | ||
| } | ||
| } | ||
| #endif // macOS] |
There was a problem hiding this comment.
Double check this is necessary
| selector:@selector(scrollViewDocumentViewBoundsDidChange:) | ||
| name:NSViewBoundsDidChangeNotification | ||
| object:_scrollView.contentView]; // NSClipView | ||
| // Re-force overlay style if the user changes the system "Show scroll bars" preference |
There was a problem hiding this comment.
Don't need this comment
Summary
NSScrollerStyleOverlayon Fabric ScrollViews to fix layout overflow on first renderautoresizingMaskfrom documentView to prevent AppKit frame corruptionNSScrollerbefore content subviews)Why force overlay instead of supporting legacy scrollbars?
Every other platform uses overlay scrollbars. iOS, Android, and web all render scrollbar indicators that float above content. macOS is the only platform where scrollbars can sit inside the frame and reduce the visible content area. Forcing overlay aligns macOS behavior with every other React Native platform.
Supporting legacy scrollbars required invasive changes to ReactCommon. The alternative approach required adding cached atomic values, custom shadow node constructors, and padding adjustments in the Yoga layout pass at the
ScrollViewShadowNodeC++ layer — a significant cross-platform change to support a single-platform edge case.Apple themselves call non-overlay scrollbars "legacy." The API is literally
NSScrollerStyleLegacy. Mac Catalyst doesn't even respect this system preference (it always uses overlay). SwiftUI does respect it, but SwiftUI also has the advantage of proposing clip view size to children — something React Native's layout system doesn't do.Test plan
Fixes #2857
🤖 Generated with Claude Code