Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,10 @@ - (instancetype)initWithFrame:(CGRect)frame
#if !TARGET_OS_OSX // [macOS]
[_scrollView addSubview:_containerView];
#else // [macOS
_containerView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
// 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];
#endif // macOS]

Expand All @@ -184,6 +187,63 @@ - (void)dealloc
#endif // [macOS]
}

#if TARGET_OS_OSX // [macOS
+ (void)initialize
{
if (self == [RCTScrollViewComponentView class]) {
// Pre-warm the cached scrollbar width at class load time, before any
// layout occurs. This ensures the first Yoga layout pass already knows
// the correct scrollbar dimensions — no state round-trip required.
[self _updateCachedScrollbarWidth];

// Observe system scrollbar style changes so we can update the cached
// value and trigger re-layout when the preference changes.
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(_systemScrollerStyleDidChange:)
name:NSPreferredScrollerStyleDidChangeNotification
object:nil];
}
}

+ (void)_updateCachedScrollbarWidth
{
CGFloat width = 0;
if ([NSScroller preferredScrollerStyle] == NSScrollerStyleLegacy) {
width = [NSScroller scrollerWidthForControlSize:NSControlSizeRegular
scrollerStyle:NSScrollerStyleLegacy];
}
ScrollViewShadowNode::setSystemScrollbarWidth(static_cast<Float>(width));
}

+ (void)_systemScrollerStyleDidChange:(NSNotification *)notification
{
[self _updateCachedScrollbarWidth];
}

- (void)_preferredScrollerStyleDidChange:(NSNotification *)notification
{
// Update the native scroll view's scroller style and re-tile so scrollers
// are properly created/removed.
_scrollView.scrollerStyle = [NSScroller preferredScrollerStyle];
[_scrollView tile];

// Force a state update to trigger shadow tree re-clone. The cloned
// ScrollViewShadowNode will read the updated cached scrollbar width
// in applyScrollbarPadding() and re-layout with correct padding.
if (_state) {
_state->updateState(
[](const ScrollViewShadowNode::ConcreteState::Data &oldData)
-> ScrollViewShadowNode::ConcreteState::SharedData {
auto newData = oldData;
// Reset contentBoundingRect to force a state difference
newData.contentBoundingRect = {};
return std::make_shared<const ScrollViewShadowNode::ConcreteState::Data>(newData);
});
}
}
#endif // macOS]

#if TARGET_OS_IOS
- (void)_registerKeyboardListener
{
Expand Down Expand Up @@ -534,6 +594,11 @@ - (void)updateState:(const State::Shared &)state oldState:(const State::Shared &
[self _preserveContentOffsetIfNeededWithBlock:^{
self->_scrollView.contentSize = contentSize;
}];

#if TARGET_OS_OSX // [macOS
// Force the scroll view to re-evaluate which scrollers should be visible.
[_scrollView tile];
#endif // macOS]
}

- (RCTPlatformView *)betterHitTest:(CGPoint)point withEvent:(UIEvent *)event // [macOS]
Expand Down Expand Up @@ -561,6 +626,21 @@ - (RCTPlatformView *)betterHitTest:(CGPoint)point withEvent:(UIEvent *)event //
return nil;
}

#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 —
// for both overlay and legacy (always-visible) scrollbar styles.
if (isPointInside) {
NSPoint scrollViewPoint = [_scrollView convertPoint:point fromView:self];
NSView *scrollViewHit = [_scrollView hitTest:scrollViewPoint];
if ([scrollViewHit isKindOfClass:[NSScroller class]]) {
return (RCTPlatformView *)scrollViewHit;
}
}
#endif // macOS]

for (RCTPlatformView *subview in [_containerView.subviews reverseObjectEnumerator]) { // [macOS]
RCTPlatformView *hitView = RCTUIViewHitTestWithEvent(subview, point, self, event); // [macOS]
if (hitView) {
Expand Down Expand Up @@ -865,12 +945,20 @@ - (void)viewDidMoveToWindow // [macOS]
[defaultCenter removeObserver:self
name:NSViewBoundsDidChangeNotification
object:_scrollView.contentView];
[defaultCenter removeObserver:self
name:NSPreferredScrollerStyleDidChangeNotification
object:nil];
} else {
// Register for scrollview's clipview bounds change notifications so we can track scrolling
[defaultCenter addObserver:self
selector:@selector(scrollViewDocumentViewBoundsDidChange:)
name:NSViewBoundsDidChangeNotification
object:_scrollView.contentView]; // NSClipView
// Observe system scrollbar style changes so we can update scrollbar insets for Yoga layout
[defaultCenter addObserver:self
selector:@selector(_preferredScrollerStyleDidChange:)
name:NSPreferredScrollerStyleDidChangeNotification
object:nil];
}
#endif // macOS]

Expand Down

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -9,28 +9,10 @@

#import <yoga/Yoga.h>

#if TARGET_OS_OSX // [macOS
#import "RCTScrollContentLocalData.h"
#endif // macOS]

#import "RCTUtils.h"

@implementation RCTScrollContentShadowView

#if TARGET_OS_OSX // [macOS
- (void)setLocalData:(RCTScrollContentLocalData *)localData
{
RCTAssert(
[localData isKindOfClass:[RCTScrollContentLocalData class]],
@"Local data object for `RCTScrollContentView` must be `RCTScrollContentLocalData` instance.");

super.marginEnd = (YGValue){localData.verticalScrollerWidth, YGUnitPoint};
super.marginBottom = (YGValue){localData.horizontalScrollerHeight, YGUnitPoint};

[self didSetProps:@[@"marginEnd", @"marginBottom"]];
}
#endif // macOS]

- (void)layoutWithMetrics:(RCTLayoutMetrics)layoutMetrics layoutContext:(RCTLayoutContext)layoutContext
{
if (layoutMetrics.layoutDirection == UIUserInterfaceLayoutDirectionRightToLeft) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,6 @@
#import <React/RCTAssert.h>
#import <React/UIView+React.h>

#if TARGET_OS_OSX // [macOS
#import <React/RCTUIManager.h>
#import "RCTScrollContentLocalData.h"
#endif // macOS]

#import "RCTScrollView.h"

@implementation RCTScrollContentView
Expand Down Expand Up @@ -44,22 +39,7 @@ - (void)reactSetFrame:(CGRect)frame

[scrollView updateContentSizeIfNeeded];
#if TARGET_OS_OSX // [macOS
// On macOS scroll indicators may float over the content view like they do in iOS
// or depending on system preferences they may be outside of the content view
// which means the clip view will be smaller than the scroll view itself.
// In such cases the content view layout must shrink accordingly otherwise
// the contents will overflow causing the scroll indicators to appear unnecessarily.
NSScrollView *platformScrollView = [scrollView scrollView];
if ([platformScrollView scrollerStyle] == NSScrollerStyleLegacy) {
BOOL contentHasHeight = platformScrollView.contentSize.height > 0;
CGFloat horizontalScrollerHeight = ([platformScrollView hasHorizontalScroller] && contentHasHeight) ? NSHeight([[platformScrollView horizontalScroller] frame]) : 0;
CGFloat verticalScrollerWidth = [platformScrollView hasVerticalScroller] ? NSWidth([[platformScrollView verticalScroller] frame]) : 0;

RCTScrollContentLocalData *localData = [[RCTScrollContentLocalData alloc] initWithVerticalScrollerWidth:verticalScrollerWidth horizontalScrollerHeight:horizontalScrollerHeight];

[[[scrollView bridge] uiManager] setLocalData:localData forView:self];
}

if ([platformScrollView accessibilityRole] == NSAccessibilityTableRole) {
NSMutableArray *subViews = [[NSMutableArray alloc] initWithCapacity:[[self subviews] count]];
for (NSView *view in [self subviews]) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1431,6 +1431,14 @@ - (void)keyUp:(NSEvent *)event {

- (void)preferredScrollerStyleDidChange:(__unused NSNotification *)notification {
RCT_SEND_SCROLL_EVENT(onPreferredScrollerStyleDidChange, (@{ @"preferredScrollerStyle": RCTStringForScrollerStyle([NSScroller preferredScrollerStyle])}));

// When the system scrollbar style changes, force the scroll view to adopt the
// new style, re-tile, and trigger a content size update. The ScrollView's
// shadow view (RCTScrollViewShadowView) will detect the new scroller style on
// the next layout pass and update its padding accordingly.
_scrollView.scrollerStyle = [NSScroller preferredScrollerStyle];
[_scrollView tile];
[self updateContentSizeIfNeeded];
}
#endif // macOS]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,54 @@
#import "RCTShadowView.h"
#import "RCTUIManager.h"

#if TARGET_OS_OSX // [macOS
// Custom shadow view for ScrollView on macOS. Applies paddingEnd to account for
// legacy (always-visible) scrollbar width so Yoga lays out children within the
// actual visible area (clip view), not the full ScrollView frame.
@interface RCTScrollViewShadowView : RCTShadowView
@end

@implementation RCTScrollViewShadowView

- (instancetype)init
{
if (self = [super init]) {
// Set scrollbar padding immediately so it's in place before the first Yoga
// layout pass. Without this, the first pass uses full width, then a second
// pass corrects it — causing a visible flicker.
[self applyScrollbarPadding];
}
return self;
}

- (void)applyScrollbarPadding
{
CGFloat verticalScrollerWidth = 0;
if ([NSScroller preferredScrollerStyle] == NSScrollerStyleLegacy) {
verticalScrollerWidth = [NSScroller scrollerWidthForControlSize:NSControlSizeRegular
scrollerStyle:NSScrollerStyleLegacy];
}

YGValue currentPaddingEnd = super.paddingEnd;
BOOL needsUpdate =
(currentPaddingEnd.unit != YGUnitPoint || currentPaddingEnd.value != verticalScrollerWidth);

if (needsUpdate) {
super.paddingEnd = (YGValue){verticalScrollerWidth, YGUnitPoint};
[self didSetProps:@[@"paddingEnd"]];
}
}

- (void)layoutWithMetrics:(RCTLayoutMetrics)layoutMetrics layoutContext:(RCTLayoutContext)layoutContext
{
// Re-check on every layout pass in case the system scroller style changed.
[self applyScrollbarPadding];
[super layoutWithMetrics:layoutMetrics layoutContext:layoutContext];
}

@end
#endif // macOS]

#if !TARGET_OS_OSX // [macOS]
@implementation RCTConvert (UIScrollView)

Expand Down Expand Up @@ -62,6 +110,13 @@ - (RCTPlatformView *)view // [macOS]
return [[RCTScrollView alloc] initWithEventDispatcher:self.bridge.eventDispatcher];
}

#if TARGET_OS_OSX // [macOS
- (RCTShadowView *)shadowView
{
return [RCTScrollViewShadowView new];
}
#endif // macOS]

RCT_EXPORT_VIEW_PROPERTY(alwaysBounceHorizontal, BOOL)
RCT_EXPORT_VIEW_PROPERTY(alwaysBounceVertical, BOOL)
RCT_EXPORT_NOT_OSX_VIEW_PROPERTY(bounces, BOOL) // [macOS]
Expand Down
Loading