Skip to content

Commit 7e66254

Browse files
M-i-k-e-lethanshar
andauthored
withScrollReached (new HOC) - notifies when start\end of scroll (ScrollView\FlatList) has been reached (#828)
* withScrollReached - new HOC * Add TODO for padding (user input) * Remove unused import * Fix proptypes * Another prop fix * Add threshold option * Add documentation * Simplify WithScrollReachedScreen * Move horizontal from props to options * Remove props.scrollEnabled * Simplify WithScrollReachedScreen even more Co-authored-by: Ethan Sharabi <[email protected]>
1 parent 8d498bc commit 7e66254

File tree

15 files changed

+216
-3
lines changed

15 files changed

+216
-3
lines changed

demo/src/assets/images/FadeOut.png

4.43 KB
Loading
8.82 KB
Loading
15.2 KB
Loading
32.1 KB
Loading
51.9 KB
Loading

demo/src/screens/MenuStructure.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,11 @@ export const navigationData = {
9595
tags: 'scroll enabled withScrollEnabler',
9696
screen: 'unicorn.components.WithScrollEnablerScreen'
9797
},
98+
{
99+
title: 'withScrollReached',
100+
tags: 'scroll reach start end',
101+
screen: 'unicorn.components.WithScrollReachedScreen'
102+
},
98103
{title: 'Wizard', tags: 'wizard', screen: 'unicorn.components.WizardScreen'}
99104
]
100105
},
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import _ from 'lodash';
2+
import React, {Component} from 'react';
3+
import {StyleSheet, ScrollView} from 'react-native';
4+
import {
5+
Colors,
6+
Text,
7+
View,
8+
Image,
9+
withScrollReached,
10+
// eslint-disable-next-line no-unused-vars
11+
WithScrollReachedProps
12+
} from 'react-native-ui-lib';
13+
// @ts-ignore
14+
import {renderHeader} from '../ExampleScreenPresenter';
15+
16+
const FADE_OUT_HEIGHT = 100;
17+
const fadeImage = require('../../assets/images/FadeOut.png');
18+
class WithScrollReachedScreen extends Component<WithScrollReachedProps> {
19+
renderItem = (index: number) => {
20+
return (
21+
<View key={index} style={styles.item}>
22+
<Text>{index + 1}</Text>
23+
</View>
24+
);
25+
};
26+
27+
render() {
28+
return (
29+
<View margin-10>
30+
{renderHeader('withScrollReached', {'marginB-10': true})}
31+
<View>
32+
<ScrollView
33+
style={styles.scrollView}
34+
contentContainerStyle={styles.scrollViewContainer}
35+
showsVerticalScrollIndicator={false}
36+
scrollEventThrottle={16}
37+
onScroll={this.props.scrollReachedProps.onScroll}
38+
>
39+
{_.times(3, this.renderItem)}
40+
</ScrollView>
41+
{!this.props.scrollReachedProps.isScrollAtEnd && (
42+
<Image style={styles.fadeOutImage} source={fadeImage} />
43+
)}
44+
</View>
45+
</View>
46+
);
47+
}
48+
}
49+
50+
export default withScrollReached(WithScrollReachedScreen);
51+
52+
const styles = StyleSheet.create({
53+
scrollView: {
54+
height: 240
55+
},
56+
scrollViewContainer: {
57+
alignItems: 'center'
58+
},
59+
item: {
60+
width: 100,
61+
height: 100,
62+
margin: 9,
63+
backgroundColor: Colors.grey40,
64+
alignItems: 'center',
65+
justifyContent: 'center'
66+
},
67+
fadeOutImage: {
68+
position: 'absolute',
69+
bottom: 0,
70+
height: FADE_OUT_HEIGHT,
71+
width: '100%'
72+
}
73+
});

demo/src/screens/componentScreens/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,5 +59,6 @@ export function registerScreens(registrar) {
5959
registrar('unicorn.screens.LoadingScreen', () => require('./LoadingScreen').default);
6060
registrar('unicorn.screens.ModalScreen', () => require('./ModalScreen').default);
6161
registrar('unicorn.components.WithScrollEnablerScreen', () => require('./WithScrollEnablerScreen').default);
62+
registrar('unicorn.components.WithScrollReachedScreen', () => require('./WithScrollReachedScreen').default);
6263
}
6364

generatedTypes/commons/new.d.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export { default as UIComponent } from './UIComponent';
22
export { default as asBaseComponent, BaseComponentInjectedProps } from './asBaseComponent';
33
export { default as forwardRef, ForwardRefInjectedProps } from './forwardRef';
4-
export { default as withScrollEnabler } from './withScrollEnabler';
4+
export { default as withScrollEnabler, WithScrollEnablerProps } from './withScrollEnabler';
5+
export { default as withScrollReached, WithScrollReachedProps } from './withScrollReached';
56
export { ContainerModifiers, MarginModifiers, TypographyModifiers, ColorsModifiers, BackgroundColorModifier } from './modifiers';
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import React from 'react';
2+
import { NativeSyntheticEvent, NativeScrollEvent } from 'react-native';
3+
export declare type ScrollReachedProps = {
4+
onScroll: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
5+
/**
6+
* Is the scroll at the start (or equal\smaller than the threshold if one was given)
7+
*/
8+
isScrollAtStart?: boolean;
9+
/**
10+
* Is the scroll at the end (or equal\greater than the threshold if one was given)
11+
*/
12+
isScrollAtEnd?: boolean;
13+
};
14+
export declare type WithScrollReachedOptionsProps = {
15+
/**
16+
* Whether the scroll is horizontal.
17+
*/
18+
horizontal?: boolean;
19+
/**
20+
* Allows to b notified prior to actually reaching the start \ end of the scroll (by the threshold).
21+
* Should be a positive value.
22+
*/
23+
threshold?: number;
24+
};
25+
export declare type WithScrollReachedProps = {
26+
scrollReachedProps: ScrollReachedProps;
27+
ref?: any;
28+
};
29+
declare function withScrollReached<PROPS>(WrappedComponent: React.ComponentType<PROPS & WithScrollReachedProps>, options?: WithScrollReachedOptionsProps): React.ComponentType<PROPS>;
30+
export default withScrollReached;

generatedTypes/index.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* Please use this file for declaring all the exports, so they could be picked up by typescript's complier
55
*/
66
export * from './style';
7-
export {asBaseComponent, withScrollEnabler} from './commons/new';
7+
export {asBaseComponent, withScrollEnabler, withScrollReached, WithScrollEnablerProps, WithScrollReachedProps} from './commons/new';
88
export {default as Card, CardPropTypes, CardSectionProps} from './components/card';
99
export {default as View, ViewPropTypes} from './components/view';
1010
export {default as Text, TextPropTypes} from './components/text';

src/commons/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,8 @@ module.exports = {
2424
},
2525
get withScrollEnabler() {
2626
return require('./withScrollEnabler').default;
27+
},
28+
get withScrollReached() {
29+
return require('./withScrollReached').default;
2730
}
2831
};

src/commons/new.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
export {default as UIComponent} from './UIComponent';
33
export {default as asBaseComponent, BaseComponentInjectedProps} from './asBaseComponent';
44
export {default as forwardRef, ForwardRefInjectedProps} from './forwardRef';
5-
export {default as withScrollEnabler} from './withScrollEnabler';
5+
export {default as withScrollEnabler, WithScrollEnablerProps} from './withScrollEnabler';
6+
export {default as withScrollReached, WithScrollReachedProps} from './withScrollReached';
67
export {
78
ContainerModifiers,
89
MarginModifiers,

src/commons/withScrollReached.tsx

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import React, {useState, useCallback} from 'react';
2+
import {
3+
// eslint-disable-next-line no-unused-vars
4+
FlatListProps,
5+
// eslint-disable-next-line no-unused-vars
6+
ScrollViewProps,
7+
// eslint-disable-next-line no-unused-vars
8+
NativeSyntheticEvent,
9+
// eslint-disable-next-line no-unused-vars
10+
NativeScrollEvent
11+
} from 'react-native';
12+
// eslint-disable-next-line no-unused-vars
13+
import forwardRef, {ForwardRefInjectedProps} from './forwardRef';
14+
15+
export type ScrollReachedProps = {
16+
onScroll: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
17+
/**
18+
* Is the scroll at the start (or equal\smaller than the threshold if one was given)
19+
*/
20+
isScrollAtStart?: boolean;
21+
/**
22+
* Is the scroll at the end (or equal\greater than the threshold if one was given)
23+
*/
24+
isScrollAtEnd?: boolean;
25+
};
26+
27+
declare type SupportedViewsProps = FlatListProps<any> | ScrollViewProps;
28+
29+
export type WithScrollReachedOptionsProps = {
30+
/**
31+
* Whether the scroll is horizontal.
32+
*/
33+
horizontal?: boolean;
34+
/**
35+
* Allows to b notified prior to actually reaching the start \ end of the scroll (by the threshold).
36+
* Should be a positive value.
37+
*/
38+
threshold?: number;
39+
};
40+
41+
export type WithScrollReachedProps = {
42+
scrollReachedProps: ScrollReachedProps;
43+
ref?: any;
44+
};
45+
46+
type PropTypes = ForwardRefInjectedProps & SupportedViewsProps;
47+
48+
function withScrollReached<PROPS>(
49+
WrappedComponent: React.ComponentType<PROPS & WithScrollReachedProps>,
50+
options: WithScrollReachedOptionsProps = {}
51+
): React.ComponentType<PROPS> {
52+
const ScrollReachedDetector = (props: PROPS & PropTypes) => {
53+
// The scroll starts at the start, from what I've tested this works fine
54+
const [isScrollAtStart, setScrollAtStart] = useState(true);
55+
const [isScrollAtEnd, setScrollAtEnd] = useState(false);
56+
const onScroll = useCallback(
57+
(event: NativeSyntheticEvent<NativeScrollEvent>) => {
58+
const {
59+
nativeEvent: {
60+
layoutMeasurement: {width: layoutWidth, height: layoutHeight},
61+
contentOffset: {x: offsetX, y: offsetY},
62+
contentSize: {width: contentWidth, height: contentHeight}
63+
}
64+
} = event;
65+
66+
const horizontal = options.horizontal;
67+
const threshold = options.threshold || 0;
68+
const layoutSize = horizontal ? layoutWidth : layoutHeight;
69+
const offset = horizontal ? offsetX : offsetY;
70+
const contentSize = horizontal ? contentWidth : contentHeight;
71+
const closeToStart = offset <= threshold;
72+
if (closeToStart !== isScrollAtStart) {
73+
setScrollAtStart(closeToStart);
74+
}
75+
76+
const closeToEnd = layoutSize + offset >= contentSize - threshold;
77+
if (closeToEnd !== isScrollAtEnd) {
78+
setScrollAtEnd(closeToEnd);
79+
}
80+
},
81+
[isScrollAtStart, isScrollAtEnd]
82+
);
83+
84+
return (
85+
<WrappedComponent
86+
{...props}
87+
scrollReachedProps={{onScroll, isScrollAtStart, isScrollAtEnd}}
88+
ref={props.forwardedRef}
89+
/>
90+
);
91+
};
92+
93+
return forwardRef(ScrollReachedDetector);
94+
}
95+
96+
export default withScrollReached;

src/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,9 @@ export default {
211211
get withScrollEnabler() {
212212
return require('./commons').withScrollEnabler;
213213
},
214+
get withScrollReached() {
215+
return require('./commons').withScrollReached;
216+
},
214217

215218
// Helpers
216219
get AvatarHelper() {

0 commit comments

Comments
 (0)