Skip to content

Commit e440434

Browse files
Saadnajmiclaude
andcommitted
[macOS] Fix Paper ScrollView scrollbar inset handling without localData
Replace the buggy RCTScrollContentLocalData mechanism with direct scrollbar detection in the shadow node. RCTScrollContentShadowView now calls +[NSScroller preferredScrollerStyle] and +scrollerWidthForControlSize: directly in layoutWithMetrics: to detect legacy scrollbar dimensions and apply them as trailing margin. These are class methods that read cached system state, so they are safe to call from the shadow thread. Key changes: - Use marginEnd (not paddingEnd) on the content shadow view so the documentView frame itself shrinks to match the clip view width. Padding only shrinks children inside the content view while leaving the documentView full-width, causing horizontal scrollbar overflow. - Eliminate the localData round-trip through the UIManager bridge - Observe NSPreferredScrollerStyleDidChangeNotification in RCTScrollView to re-layout when the system scrollbar preference changes Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 922c862 commit e440434

File tree

5 files changed

+44
-84
lines changed

5 files changed

+44
-84
lines changed

packages/react-native/React/Views/ScrollView/MacOS/RCTScrollContentLocalData.h

Lines changed: 0 additions & 27 deletions
This file was deleted.

packages/react-native/React/Views/ScrollView/MacOS/RCTScrollContentLocalData.m

Lines changed: 0 additions & 26 deletions
This file was deleted.

packages/react-native/React/Views/ScrollView/RCTScrollContentShadowView.m

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,25 +9,43 @@
99

1010
#import <yoga/Yoga.h>
1111

12-
#if TARGET_OS_OSX // [macOS
13-
#import "RCTScrollContentLocalData.h"
14-
#endif // macOS]
15-
1612
#import "RCTUtils.h"
1713

1814
@implementation RCTScrollContentShadowView
1915

2016
#if TARGET_OS_OSX // [macOS
21-
- (void)setLocalData:(RCTScrollContentLocalData *)localData
17+
- (void)applyScrollbarPadding
2218
{
23-
RCTAssert(
24-
[localData isKindOfClass:[RCTScrollContentLocalData class]],
25-
@"Local data object for `RCTScrollContentView` must be `RCTScrollContentLocalData` instance.");
19+
// On macOS, legacy (always-visible) scrollbars sit inside the NSScrollView
20+
// frame and reduce the NSClipView's visible area. Detect the scrollbar
21+
// dimensions and apply them as padding so Yoga lays out children within
22+
// the actual visible area.
23+
//
24+
// +preferredScrollerStyle and +scrollerWidthForControlSize: are class methods
25+
// that read cached system state, so they are safe to call from any thread.
26+
// Only account for the vertical scrollbar width. The horizontal scrollbar
27+
// should not need padding — once the vertical scrollbar width is correct,
28+
// content fits horizontally and no horizontal scrollbar appears.
29+
CGFloat verticalScrollerWidth = 0;
30+
31+
if ([NSScroller preferredScrollerStyle] == NSScrollerStyleLegacy) {
32+
verticalScrollerWidth = [NSScroller scrollerWidthForControlSize:NSControlSizeRegular
33+
scrollerStyle:NSScrollerStyleLegacy];
34+
}
2635

27-
super.marginEnd = (YGValue){localData.verticalScrollerWidth, YGUnitPoint};
28-
super.marginBottom = (YGValue){localData.horizontalScrollerHeight, YGUnitPoint};
36+
// Use marginEnd (not paddingEnd) so the content view's own frame shrinks to
37+
// match the clip view width. Padding only affects the content view's children,
38+
// while the content view (documentView) frame stays full-width — causing the
39+
// NSScrollView to show a horizontal scrollbar when legacy scrollbars are present.
40+
YGValue currentMarginEnd = super.marginEnd;
2941

30-
[self didSetProps:@[@"marginEnd", @"marginBottom"]];
42+
BOOL needsUpdate =
43+
(currentMarginEnd.unit != YGUnitPoint || currentMarginEnd.value != verticalScrollerWidth);
44+
45+
if (needsUpdate) {
46+
super.marginEnd = (YGValue){verticalScrollerWidth, YGUnitPoint};
47+
[self didSetProps:@[@"marginEnd"]];
48+
}
3149
}
3250
#endif // macOS]
3351

@@ -43,6 +61,13 @@ - (void)layoutWithMetrics:(RCTLayoutMetrics)layoutMetrics layoutContext:(RCTLayo
4361
layoutMetrics.frame.origin.x = 0;
4462
}
4563

64+
#if TARGET_OS_OSX // [macOS
65+
// Check scrollbar padding on every layout pass. If the system scroller style
66+
// changed, this will mark the node dirty and Yoga will recalculate on the
67+
// next pass.
68+
[self applyScrollbarPadding];
69+
#endif // macOS]
70+
4671
[super layoutWithMetrics:layoutMetrics layoutContext:layoutContext];
4772
}
4873

packages/react-native/React/Views/ScrollView/RCTScrollContentView.m

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,6 @@
1010
#import <React/RCTAssert.h>
1111
#import <React/UIView+React.h>
1212

13-
#if TARGET_OS_OSX // [macOS
14-
#import <React/RCTUIManager.h>
15-
#import "RCTScrollContentLocalData.h"
16-
#endif // macOS]
17-
1813
#import "RCTScrollView.h"
1914

2015
@implementation RCTScrollContentView
@@ -44,22 +39,7 @@ - (void)reactSetFrame:(CGRect)frame
4439

4540
[scrollView updateContentSizeIfNeeded];
4641
#if TARGET_OS_OSX // [macOS
47-
// On macOS scroll indicators may float over the content view like they do in iOS
48-
// or depending on system preferences they may be outside of the content view
49-
// which means the clip view will be smaller than the scroll view itself.
50-
// In such cases the content view layout must shrink accordingly otherwise
51-
// the contents will overflow causing the scroll indicators to appear unnecessarily.
5242
NSScrollView *platformScrollView = [scrollView scrollView];
53-
if ([platformScrollView scrollerStyle] == NSScrollerStyleLegacy) {
54-
BOOL contentHasHeight = platformScrollView.contentSize.height > 0;
55-
CGFloat horizontalScrollerHeight = ([platformScrollView hasHorizontalScroller] && contentHasHeight) ? NSHeight([[platformScrollView horizontalScroller] frame]) : 0;
56-
CGFloat verticalScrollerWidth = [platformScrollView hasVerticalScroller] ? NSWidth([[platformScrollView verticalScroller] frame]) : 0;
57-
58-
RCTScrollContentLocalData *localData = [[RCTScrollContentLocalData alloc] initWithVerticalScrollerWidth:verticalScrollerWidth horizontalScrollerHeight:horizontalScrollerHeight];
59-
60-
[[[scrollView bridge] uiManager] setLocalData:localData forView:self];
61-
}
62-
6343
if ([platformScrollView accessibilityRole] == NSAccessibilityTableRole) {
6444
NSMutableArray *subViews = [[NSMutableArray alloc] initWithCapacity:[[self subviews] count]];
6545
for (NSView *view in [self subviews]) {

packages/react-native/React/Views/ScrollView/RCTScrollView.m

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1431,6 +1431,14 @@ - (void)keyUp:(NSEvent *)event {
14311431

14321432
- (void)preferredScrollerStyleDidChange:(__unused NSNotification *)notification {
14331433
RCT_SEND_SCROLL_EVENT(onPreferredScrollerStyleDidChange, (@{ @"preferredScrollerStyle": RCTStringForScrollerStyle([NSScroller preferredScrollerStyle])}));
1434+
1435+
// When the system scrollbar style changes, force the scroll view to adopt the
1436+
// new style, re-tile, and trigger a content size update. The shadow view
1437+
// (RCTScrollContentShadowView) will detect the new scroller style on the next
1438+
// layout pass and update its padding accordingly.
1439+
_scrollView.scrollerStyle = [NSScroller preferredScrollerStyle];
1440+
[_scrollView tile];
1441+
[self updateContentSizeIfNeeded];
14341442
}
14351443
#endif // macOS]
14361444

0 commit comments

Comments
 (0)