Skip to content

Commit ef37f10

Browse files
Saadnajmiclaude
andcommitted
fix(fabric,macos): force overlay scrollbar style in ScrollView
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>
1 parent 429f532 commit ef37f10

File tree

1 file changed

+43
-21
lines changed

1 file changed

+43
-21
lines changed

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

Lines changed: 43 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,16 @@ - (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. Overlay scrollers float above content and
161+
// don't reduce the clip view's visible area, so no layout compensation is
162+
// needed. Legacy (always-visible) scrollbars sit inside the frame and would
163+
// require padding adjustments in the Yoga shadow tree — avoiding that
164+
// complexity is the main motivation for forcing overlay style.
165+
_scrollView.scrollerStyle = NSScrollerStyleOverlay;
166+
// Do NOT set autoresizingMask on the documentView. AppKit's autoresizing
167+
// corrupts the documentView frame during tile/resize (adding the clip view's
168+
// size delta to the container, inflating it beyond the actual content size).
169+
// React manages the documentView frame directly via updateState:.
161170
[_scrollView setDocumentView:_containerView];
162171
#endif // macOS]
163172

@@ -185,26 +194,12 @@ - (void)dealloc
185194
}
186195

187196
#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-
}
197+
- (void)_preferredScrollerStyleDidChange:(NSNotification *)notification
198+
{
199+
// The user changed the system "Show scroll bars" preference. Re-force
200+
// overlay style so legacy scrollbars don't appear and cause layout issues.
201+
_scrollView.scrollerStyle = NSScrollerStyleOverlay;
202+
[_scrollView tile];
208203
}
209204
#endif // macOS]
210205

@@ -558,6 +553,11 @@ - (void)updateState:(const State::Shared &)state oldState:(const State::Shared &
558553
[self _preserveContentOffsetIfNeededWithBlock:^{
559554
self->_scrollView.contentSize = contentSize;
560555
}];
556+
557+
#if TARGET_OS_OSX // [macOS
558+
// Force the scroll view to re-evaluate which scrollers should be visible.
559+
[_scrollView tile];
560+
#endif // macOS]
561561
}
562562

563563
- (RCTPlatformView *)betterHitTest:(CGPoint)point withEvent:(UIEvent *)event // [macOS]
@@ -585,6 +585,20 @@ - (RCTPlatformView *)betterHitTest:(CGPoint)point withEvent:(UIEvent *)event //
585585
return nil;
586586
}
587587

588+
#if TARGET_OS_OSX // [macOS
589+
// Check if the hit lands on a scrollbar (NSScroller) BEFORE checking content
590+
// subviews. Scrollers are subviews of the NSScrollView, not the documentView
591+
// (_containerView). They must be checked first because content views typically
592+
// fill the entire visible area and would otherwise swallow scroller clicks.
593+
if (isPointInside) {
594+
NSPoint scrollViewPoint = [_scrollView convertPoint:point fromView:self];
595+
NSView *scrollViewHit = [_scrollView hitTest:scrollViewPoint];
596+
if ([scrollViewHit isKindOfClass:[NSScroller class]]) {
597+
return (RCTPlatformView *)scrollViewHit;
598+
}
599+
}
600+
#endif // macOS]
601+
588602
for (RCTPlatformView *subview in [_containerView.subviews reverseObjectEnumerator]) { // [macOS]
589603
RCTPlatformView *hitView = RCTUIViewHitTestWithEvent(subview, point, self, event); // [macOS]
590604
if (hitView) {
@@ -889,12 +903,20 @@ - (void)viewDidMoveToWindow // [macOS]
889903
[defaultCenter removeObserver:self
890904
name:NSViewBoundsDidChangeNotification
891905
object:_scrollView.contentView];
906+
[defaultCenter removeObserver:self
907+
name:NSPreferredScrollerStyleDidChangeNotification
908+
object:nil];
892909
} else {
893910
// Register for scrollview's clipview bounds change notifications so we can track scrolling
894911
[defaultCenter addObserver:self
895912
selector:@selector(scrollViewDocumentViewBoundsDidChange:)
896913
name:NSViewBoundsDidChangeNotification
897914
object:_scrollView.contentView]; // NSClipView
915+
// Re-force overlay style if the user changes the system "Show scroll bars" preference
916+
[defaultCenter addObserver:self
917+
selector:@selector(_preferredScrollerStyleDidChange:)
918+
name:NSPreferredScrollerStyleDidChangeNotification
919+
object:nil];
898920
}
899921
#endif // macOS]
900922

0 commit comments

Comments
 (0)