Skip to content

Commit a054786

Browse files
Saadnajmiclaude
andauthored
fix(fabric): force overlay scrollbar style in ScrollView (#2907)
## Summary - Force `NSScrollerStyleOverlay` on Fabric ScrollViews to fix layout overflow on first render - Remove `autoresizingMask` from documentView to prevent AppKit frame corruption - Fix scrollbar click hit testing (check `NSScroller` before content subviews) - Re-force overlay style when the system "Show scroll bars" preference changes at runtime ## Why force overlay instead of supporting legacy scrollbars? 1. **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. 2. **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 `ScrollViewShadowNode` C++ layer — a significant cross-platform change to support a single-platform edge case. 3. **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 - [x] Open RNTester on macOS with "Show scroll bars: Always" in System Settings - [x] Verify ScrollView content does not overflow on first render - [x] Verify scrollbars appear as overlay style (thin, semi-transparent) - [x] Verify switching system preference at runtime doesn't bring back legacy scrollbars - [x] Verify scrollbar clicks work (drag the scrollbar thumb) - [x] Verify window resize doesn't cause content overflow - [x] Test with "Show scroll bars: When scrolling" (default) — unchanged behavior Fixes #2857 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 4d114df commit a054786

File tree

1 file changed

+27
-21
lines changed

1 file changed

+27
-21
lines changed

packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,9 @@ - (instancetype)initWithFrame:(CGRect)frame
157157
#if !TARGET_OS_OSX // [macOS]
158158
[_scrollView addSubview:_containerView];
159159
#else // [macOS
160-
_containerView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
160+
// Force overlay scrollbar style to avoid layout issues with legacy scrollbars.
161+
_scrollView.scrollerStyle = NSScrollerStyleOverlay;
162+
// Don't set autoresizingMask — AppKit corrupts the documentView frame during tile/resize.
161163
[_scrollView setDocumentView:_containerView];
162164
#endif // macOS]
163165

@@ -185,26 +187,11 @@ - (void)dealloc
185187
}
186188

187189
#if TARGET_OS_OSX // [macOS
188-
- (void)layoutSubviews
189-
{
190-
[super layoutSubviews];
191-
192-
// On macOS, the _containerView is the NSScrollView's documentView and has autoresizingMask set so
193-
// it fills the visible area before React's first layout pass. However, AppKit's autoresizing can
194-
// corrupt the documentView's frame by adding the NSClipView's size delta to the container's
195-
// dimensions (e.g., during initial tile or window resize), inflating it well beyond the correct
196-
// content size. This produces massive horizontal and vertical overflow on first render.
197-
//
198-
// After React has set the content size via updateState:, we reset the documentView frame here to
199-
// undo any autoresizing corruption. This runs after AppKit's layout (which triggers autoresizing),
200-
// so it reliably corrects the frame.
201-
if (!CGSizeEqualToSize(_contentSize, CGSizeZero)) {
202-
CGRect containerFrame = _containerView.frame;
203-
if (!CGSizeEqualToSize(containerFrame.size, _contentSize)) {
204-
containerFrame.size = _contentSize;
205-
_containerView.frame = containerFrame;
206-
}
207-
}
190+
- (void)_preferredScrollerStyleDidChange:(NSNotification *)notification
191+
{
192+
// Re-force overlay style when system preference changes.
193+
_scrollView.scrollerStyle = NSScrollerStyleOverlay;
194+
[_scrollView tile];
208195
}
209196
#endif // macOS]
210197

@@ -585,6 +572,18 @@ - (RCTPlatformView *)betterHitTest:(CGPoint)point withEvent:(UIEvent *)event //
585572
return nil;
586573
}
587574

575+
#if TARGET_OS_OSX // [macOS
576+
// Check scrollbars before content subviews — scrollers are NSScrollView children,
577+
// not documentView children, so full-width content would swallow their clicks.
578+
if (isPointInside) {
579+
NSPoint scrollViewPoint = [_scrollView convertPoint:point fromView:self];
580+
NSView *scrollViewHit = [_scrollView hitTest:scrollViewPoint];
581+
if ([scrollViewHit isKindOfClass:[NSScroller class]]) {
582+
return (RCTPlatformView *)scrollViewHit;
583+
}
584+
}
585+
#endif // macOS]
586+
588587
for (RCTPlatformView *subview in [_containerView.subviews reverseObjectEnumerator]) { // [macOS]
589588
RCTPlatformView *hitView = RCTUIViewHitTestWithEvent(subview, point, self, event); // [macOS]
590589
if (hitView) {
@@ -889,12 +888,19 @@ - (void)viewDidMoveToWindow // [macOS]
889888
[defaultCenter removeObserver:self
890889
name:NSViewBoundsDidChangeNotification
891890
object:_scrollView.contentView];
891+
[defaultCenter removeObserver:self
892+
name:NSPreferredScrollerStyleDidChangeNotification
893+
object:nil];
892894
} else {
893895
// Register for scrollview's clipview bounds change notifications so we can track scrolling
894896
[defaultCenter addObserver:self
895897
selector:@selector(scrollViewDocumentViewBoundsDidChange:)
896898
name:NSViewBoundsDidChangeNotification
897899
object:_scrollView.contentView]; // NSClipView
900+
[defaultCenter addObserver:self
901+
selector:@selector(_preferredScrollerStyleDidChange:)
902+
name:NSPreferredScrollerStyleDidChangeNotification
903+
object:nil];
898904
}
899905
#endif // macOS]
900906

0 commit comments

Comments
 (0)