@@ -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