Skip to content

Support inverted ScrollView on macOS #1264

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Sep 7, 2022
Merged
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
11 changes: 11 additions & 0 deletions Libraries/Components/ScrollView/ScrollView.js
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,10 @@ export type Props = $ReadOnly<{|
* ScrollView. This is usually used with inverted ScrollViews.
*/
invertStickyHeaders?: ?boolean,
/**
* Reverses the direction of scroll. Uses native inversion on macOS and scale transforms of -1 elsewhere
*/
inverted?: ?boolean, // TODO(macOS GH#774)
/**
* Determines whether the keyboard gets dismissed in response to a drag.
*
Expand Down Expand Up @@ -1192,6 +1196,11 @@ class ScrollView extends React.Component<Props, State> {
this.setState({contentKey: this.state.contentKey + 1});
}; // ]TODO(macOS GH#774)

// [TODO(macOS GH#774)
_handleInvertedDidChange = () => {
this.setState({contentKey: this.state.contentKey + 1});
}; // ]TODO(macOS GH#774)

_handleScroll = (e: ScrollEvent) => {
if (__DEV__) {
if (
Expand Down Expand Up @@ -1716,6 +1725,7 @@ class ScrollView extends React.Component<Props, State> {
: this.props.removeClippedSubviews
}
key={this.state.contentKey} // TODO(macOS GH#774)
inverted={this.props.inverted} // TODO(macOS GH#774)
collapsable={false}>
{children}
</NativeDirectionalScrollContentView>
Expand Down Expand Up @@ -1743,6 +1753,7 @@ class ScrollView extends React.Component<Props, State> {
// Override the onContentSizeChange from props, since this event can
// bubble up from TextInputs
onContentSizeChange: null,
onInvertedDidChange: this._handleInvertedDidChange, // TODO macOS GH#774
onPreferredScrollerStyleDidChange:
this._handlePreferredScrollerStyleDidChange, // TODO(macOS GH#774)
onLayout: this._handleLayout,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ exports[`<ScrollView /> should render as expected: should deep render when not m
<RCTScrollView
alwaysBounceVertical={true}
onContentSizeChange={null}
onInvertedDidChange={[Function]}
onLayout={[Function]}
onMomentumScrollBegin={[Function]}
onMomentumScrollEnd={[Function]}
Expand Down
10 changes: 10 additions & 0 deletions Libraries/Components/View/ViewPropTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,12 @@ type DirectEventProps = $ReadOnly<{|
*/
onDoubleClick?: ?(event: SyntheticEvent<{}>) => mixed, // TODO(macOS GH#774)

/**
* This event is fired when the scrollView's inverted property changes.
* @platform macos
*/
onInvertedDidChange?: ?() => mixed, // TODO(macOS GH#774)

/**
* This event is fired when the system's preferred scroller style changes.
* The `preferredScrollerStyle` key will be `legacy` or `overlay`.
Expand Down Expand Up @@ -414,6 +420,10 @@ type IOSViewProps = $ReadOnly<{|
* See https://reactnative.dev/docs/view#accessibilityElementsHidden
*/
accessibilityElementsHidden?: ?boolean,
/**
* Reverses the direction of scroll. Uses native inversion on macOS and scale transforms of -1 elsewhere
*/
inverted?: ?boolean, // TODO(macOS GH#774)

onDoubleClick?: ?(event: SyntheticEvent<{}>) => mixed, // TODO(macOS GH#774)

Expand Down
2 changes: 1 addition & 1 deletion Libraries/Lists/FlatList.js
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ type OptionalProps<ItemT> = {|
initialSelectedIndex?: ?number,
// ]TODO(macOS GH#774)
/**
* Reverses the direction of scroll. Uses scale transforms of -1.
* Reverses the direction of scroll. Uses native inversion on macOS and scale transforms of -1 elsewhere
*/
inverted?: ?boolean,
/**
Expand Down
15 changes: 10 additions & 5 deletions Libraries/Lists/VirtualizedList.js
Original file line number Diff line number Diff line change
Expand Up @@ -979,11 +979,13 @@ class VirtualizedList extends React.PureComponent<Props, State> {
this.props;
const {data, horizontal} = this.props;
const isVirtualizationDisabled = this._isVirtualizationDisabled();
const inversionStyle = this.props.inverted
? horizontalOrDefault(this.props.horizontal)
? styles.horizontallyInverted
: styles.verticallyInverted
: null;
// macOS natively supports inverted lists, thus not needing an inversion style
const inversionStyle =
this.props.inverted && Platform.OS !== 'macos' // TODO(macOS GH#774)
? horizontalOrDefault(this.props.horizontal)
? styles.horizontallyInverted
: styles.verticallyInverted
: null;
const cells = [];
const stickyIndicesFromProps = new Set(this.props.stickyHeaderIndices);
const stickyHeaderIndices = [];
Expand Down Expand Up @@ -1330,6 +1332,7 @@ class VirtualizedList extends React.PureComponent<Props, State> {
// [TODO(macOS GH#774)
const preferredScrollerStyleDidChangeHandler =
this.props.onPreferredScrollerStyleDidChange;
const invertedDidChange = this.props.onInvertedDidChange;

const keyboardNavigationProps = {
focusable: true,
Expand All @@ -1353,6 +1356,7 @@ class VirtualizedList extends React.PureComponent<Props, State> {
<ScrollView
// [TODO(macOS GH#774)
{...(props.enableSelectionOnKeyPress && keyboardNavigationProps)}
onInvertedDidChange={invertedDidChange}
onPreferredScrollerStyleDidChange={
preferredScrollerStyleDidChangeHandler
} // TODO(macOS GH#774)]
Expand All @@ -1376,6 +1380,7 @@ class VirtualizedList extends React.PureComponent<Props, State> {
<ScrollView
// [TODO(macOS GH#774)
{...(props.enableSelectionOnKeyPress && keyboardNavigationProps)}
onInvertedDidChange={invertedDidChange}
onPreferredScrollerStyleDidChange={
preferredScrollerStyleDidChangeHandler
} // TODO(macOS GH#774)]
Expand Down
4 changes: 4 additions & 0 deletions React/Views/ScrollView/RCTScrollContentView.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,8 @@

@interface RCTScrollContentView : RCTView

#if TARGET_OS_OSX // [TODO(macOS GH#774)
@property (nonatomic, assign, getter=isInverted) BOOL inverted;
#endif // ]TODO(macOS GH#774)

@end
6 changes: 6 additions & 0 deletions React/Views/ScrollView/RCTScrollContentView.m
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@
#import "RCTScrollView.h"

@implementation RCTScrollContentView
#if TARGET_OS_OSX // [TODO(macOS GH#774)
- (BOOL)isFlipped
{
return !self.inverted;
}
#endif // ]TODO(macOS GH#774)

- (void)reactSetFrame:(CGRect)frame
{
Expand Down
2 changes: 2 additions & 0 deletions React/Views/ScrollView/RCTScrollContentViewManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ @implementation RCTScrollContentViewManager

RCT_EXPORT_MODULE()

RCT_EXPORT_OSX_VIEW_PROPERTY(inverted, BOOL) // TODO(macOS GH#774)

- (RCTScrollContentView *)view
{
return [RCTScrollContentView new];
Expand Down
2 changes: 2 additions & 0 deletions React/Views/ScrollView/RCTScrollView.h
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@
@property (nonatomic, copy) RCTDirectEventBlock onMomentumScrollEnd;
@property (nonatomic, copy) RCTDirectEventBlock onPreferredScrollerStyleDidChange; // TODO(macOS GH#774)

@property (nonatomic, copy) RCTDirectEventBlock onInvertedDidChange; // TODO(macOS GH#774)

- (void)flashScrollIndicators; // TODO(macOS GH#774)

@end
Expand Down
15 changes: 15 additions & 0 deletions React/Views/ScrollView/RCTScrollView.m
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ @interface RCTCustomScrollView :
@property (nonatomic, assign) BOOL pinchGestureEnabled;
#else // [TODO(macOS GH#774)
+ (BOOL)isCompatibleWithResponsiveScrolling;
@property (nonatomic, assign, getter=isInverted) BOOL inverted;
@property (nonatomic, assign, getter=isScrollEnabled) BOOL scrollEnabled;
@property (nonatomic, strong) NSPanGestureRecognizer *panGestureRecognizer;
#endif // ]TODO(macOS GH#774)
Expand Down Expand Up @@ -108,6 +109,11 @@ + (BOOL)isCompatibleWithResponsiveScrolling
return YES;
}

- (BOOL)isFlipped
{
return !self.inverted;
}

- (void)scrollWheel:(NSEvent *)theEvent
{
if (!self.scrollEnabled) {
Expand Down Expand Up @@ -556,6 +562,15 @@ - (void)setAccessibilityRole:(NSAccessibilityRole)accessibilityRole
{
[_scrollView setAccessibilityRole:accessibilityRole];
}

- (void)setInverted:(BOOL)inverted
{
BOOL changed = _inverted != inverted;
_inverted = inverted;
if (changed && _onInvertedDidChange) {
_onInvertedDidChange(@{});
}
}
#endif // ]TODO(macOS GH#774)

RCT_NOT_IMPLEMENTED(-(instancetype)initWithFrame : (CGRect)frame)
Expand Down
1 change: 1 addition & 0 deletions React/Views/ScrollView/RCTScrollViewManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ - (RCTPlatformView *)view // TODO(macOS GH#774)
RCT_EXPORT_VIEW_PROPERTY(onScrollEndDrag, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onMomentumScrollBegin, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onMomentumScrollEnd, RCTDirectEventBlock)
RCT_EXPORT_OSX_VIEW_PROPERTY(onInvertedDidChange, RCTDirectEventBlock) // TODO(macOS GH#774)
RCT_EXPORT_OSX_VIEW_PROPERTY(onPreferredScrollerStyleDidChange, RCTDirectEventBlock) // TODO(macOS GH#774)
RCT_EXPORT_VIEW_PROPERTY(inverted, BOOL)
#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000 /* __IPHONE_13_0 */
Expand Down
42 changes: 42 additions & 0 deletions packages/rn-tester/js/examples/ScrollView/ScrollViewExample.js
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,48 @@ if (Platform.OS === 'ios') {
}
exports.examples = examples;

// TODO [(macOS GH#774)
if (Platform.OS === 'macos') {
examples.push({
title: '<ScrollView> (inverted = true/false)\n',
description:
"You can display <ScrollView>'s child components in inverted order",
render: function (): React.Node {
return <InvertedContentExample />;
},
});
}

const InvertedContentExample = () => {
const [inverted, setInverted] = useState(true);
const [items, setItems] = useState(ITEMS);
return (
<>
<ScrollView
style={[styles.scrollView, {height: 200}]}
inverted={inverted}>
{items.map(createItemRow)}
</ScrollView>
<Text style={{paddingTop: 10, paddingBottom: 10}}>
Same example as above, but with the opposite inverted option
</Text>
<ScrollView
style={[styles.scrollView, {height: 200}]}
inverted={!inverted}>
{items.map(createItemRow)}
</ScrollView>
<Button
label={'toggle inverted'}
onPress={() => {
setInverted(!inverted);
setItems([...Array(14)].map((_, i) => `Item ${i}`));
}}
/>
</>
);
};
// ]TODO(macOS GH#774)

const AndroidScrollBarOptions = () => {
const [persistentScrollBar, setPersistentScrollBar] = useState(false);
return (
Expand Down