Skip to content

withScrollReached (new HOC) - notifies when start\end of scroll (ScrollView\FlatList) has been reached #828

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 12 commits into from
Jul 6, 2020
Binary file added demo/src/assets/images/FadeOut.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added demo/src/assets/images/[email protected]
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added demo/src/assets/images/[email protected]
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added demo/src/assets/images/[email protected]
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added demo/src/assets/images/[email protected]
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions demo/src/screens/MenuStructure.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,11 @@ export const navigationData = {
tags: 'scroll enabled withScrollEnabler',
screen: 'unicorn.components.WithScrollEnablerScreen'
},
{
title: 'withScrollReached',
tags: 'scroll reach start end',
screen: 'unicorn.components.WithScrollReachedScreen'
},
{title: 'Wizard', tags: 'wizard', screen: 'unicorn.components.WizardScreen'}
]
},
Expand Down
73 changes: 73 additions & 0 deletions demo/src/screens/componentScreens/WithScrollReachedScreen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import _ from 'lodash';
import React, {Component} from 'react';
import {StyleSheet, ScrollView} from 'react-native';
import {
Colors,
Text,
View,
Image,
withScrollReached,
// eslint-disable-next-line no-unused-vars
WithScrollReachedProps
} from 'react-native-ui-lib';
// @ts-ignore
import {renderHeader} from '../ExampleScreenPresenter';

const FADE_OUT_HEIGHT = 100;
const fadeImage = require('../../assets/images/FadeOut.png');
class WithScrollReachedScreen extends Component<WithScrollReachedProps> {
renderItem = (index: number) => {
return (
<View key={index} style={styles.item}>
<Text>{index + 1}</Text>
</View>
);
};

render() {
return (
<View margin-10>
{renderHeader('withScrollReached', {'marginB-10': true})}
<View>
<ScrollView
style={styles.scrollView}
contentContainerStyle={styles.scrollViewContainer}
showsVerticalScrollIndicator={false}
scrollEventThrottle={16}
onScroll={this.props.scrollReachedProps.onScroll}
>
{_.times(3, this.renderItem)}
</ScrollView>
{!this.props.scrollReachedProps.isScrollAtEnd && (
<Image style={styles.fadeOutImage} source={fadeImage} />
)}
</View>
</View>
);
}
}

export default withScrollReached(WithScrollReachedScreen);

const styles = StyleSheet.create({
scrollView: {
height: 240
},
scrollViewContainer: {
alignItems: 'center'
},
item: {
width: 100,
height: 100,
margin: 9,
backgroundColor: Colors.grey40,
alignItems: 'center',
justifyContent: 'center'
},
fadeOutImage: {
position: 'absolute',
bottom: 0,
height: FADE_OUT_HEIGHT,
width: '100%'
}
});
1 change: 1 addition & 0 deletions demo/src/screens/componentScreens/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,5 +59,6 @@ export function registerScreens(registrar) {
registrar('unicorn.screens.LoadingScreen', () => require('./LoadingScreen').default);
registrar('unicorn.screens.ModalScreen', () => require('./ModalScreen').default);
registrar('unicorn.components.WithScrollEnablerScreen', () => require('./WithScrollEnablerScreen').default);
registrar('unicorn.components.WithScrollReachedScreen', () => require('./WithScrollReachedScreen').default);
}

3 changes: 2 additions & 1 deletion generatedTypes/commons/new.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export { default as UIComponent } from './UIComponent';
export { default as asBaseComponent, BaseComponentInjectedProps } from './asBaseComponent';
export { default as forwardRef, ForwardRefInjectedProps } from './forwardRef';
export { default as withScrollEnabler } from './withScrollEnabler';
export { default as withScrollEnabler, WithScrollEnablerProps } from './withScrollEnabler';
export { default as withScrollReached, WithScrollReachedProps } from './withScrollReached';
export { ContainerModifiers, MarginModifiers, TypographyModifiers, ColorsModifiers, BackgroundColorModifier } from './modifiers';
30 changes: 30 additions & 0 deletions generatedTypes/commons/withScrollReached.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React from 'react';
import { NativeSyntheticEvent, NativeScrollEvent } from 'react-native';
export declare type ScrollReachedProps = {
onScroll: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
/**
* Is the scroll at the start (or equal\smaller than the threshold if one was given)
*/
isScrollAtStart?: boolean;
/**
* Is the scroll at the end (or equal\greater than the threshold if one was given)
*/
isScrollAtEnd?: boolean;
};
export declare type WithScrollReachedOptionsProps = {
/**
* Whether the scroll is horizontal.
*/
horizontal?: boolean;
/**
* Allows to b notified prior to actually reaching the start \ end of the scroll (by the threshold).
* Should be a positive value.
*/
threshold?: number;
};
export declare type WithScrollReachedProps = {
scrollReachedProps: ScrollReachedProps;
ref?: any;
};
declare function withScrollReached<PROPS>(WrappedComponent: React.ComponentType<PROPS & WithScrollReachedProps>, options?: WithScrollReachedOptionsProps): React.ComponentType<PROPS>;
export default withScrollReached;
2 changes: 1 addition & 1 deletion generatedTypes/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* Please use this file for declaring all the exports, so they could be picked up by typescript's complier
*/
export * from './style';
export {asBaseComponent, withScrollEnabler} from './commons/new';
export {asBaseComponent, withScrollEnabler, withScrollReached, WithScrollEnablerProps, WithScrollReachedProps} from './commons/new';
export {default as Card, CardPropTypes, CardSectionProps} from './components/card';
export {default as View, ViewPropTypes} from './components/view';
export {default as Text, TextPropTypes} from './components/text';
Expand Down
3 changes: 3 additions & 0 deletions src/commons/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,8 @@ module.exports = {
},
get withScrollEnabler() {
return require('./withScrollEnabler').default;
},
get withScrollReached() {
return require('./withScrollReached').default;
}
};
3 changes: 2 additions & 1 deletion src/commons/new.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
export {default as UIComponent} from './UIComponent';
export {default as asBaseComponent, BaseComponentInjectedProps} from './asBaseComponent';
export {default as forwardRef, ForwardRefInjectedProps} from './forwardRef';
export {default as withScrollEnabler} from './withScrollEnabler';
export {default as withScrollEnabler, WithScrollEnablerProps} from './withScrollEnabler';
export {default as withScrollReached, WithScrollReachedProps} from './withScrollReached';
export {
ContainerModifiers,
MarginModifiers,
Expand Down
96 changes: 96 additions & 0 deletions src/commons/withScrollReached.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import React, {useState, useCallback} from 'react';
import {
// eslint-disable-next-line no-unused-vars
FlatListProps,
// eslint-disable-next-line no-unused-vars
ScrollViewProps,
// eslint-disable-next-line no-unused-vars
NativeSyntheticEvent,
// eslint-disable-next-line no-unused-vars
NativeScrollEvent
} from 'react-native';
// eslint-disable-next-line no-unused-vars
import forwardRef, {ForwardRefInjectedProps} from './forwardRef';

export type ScrollReachedProps = {
onScroll: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
/**
* Is the scroll at the start (or equal\smaller than the threshold if one was given)
*/
isScrollAtStart?: boolean;
/**
* Is the scroll at the end (or equal\greater than the threshold if one was given)
*/
isScrollAtEnd?: boolean;
};

declare type SupportedViewsProps = FlatListProps<any> | ScrollViewProps;

export type WithScrollReachedOptionsProps = {
/**
* Whether the scroll is horizontal.
*/
horizontal?: boolean;
/**
* Allows to b notified prior to actually reaching the start \ end of the scroll (by the threshold).
* Should be a positive value.
*/
threshold?: number;
};

export type WithScrollReachedProps = {
scrollReachedProps: ScrollReachedProps;
ref?: any;
};

type PropTypes = ForwardRefInjectedProps & SupportedViewsProps;

function withScrollReached<PROPS>(
WrappedComponent: React.ComponentType<PROPS & WithScrollReachedProps>,
options: WithScrollReachedOptionsProps = {}
): React.ComponentType<PROPS> {
const ScrollReachedDetector = (props: PROPS & PropTypes) => {
// The scroll starts at the start, from what I've tested this works fine
const [isScrollAtStart, setScrollAtStart] = useState(true);
const [isScrollAtEnd, setScrollAtEnd] = useState(false);
const onScroll = useCallback(
(event: NativeSyntheticEvent<NativeScrollEvent>) => {
const {
nativeEvent: {
layoutMeasurement: {width: layoutWidth, height: layoutHeight},
contentOffset: {x: offsetX, y: offsetY},
contentSize: {width: contentWidth, height: contentHeight}
}
} = event;

const horizontal = options.horizontal;
const threshold = options.threshold || 0;
const layoutSize = horizontal ? layoutWidth : layoutHeight;
const offset = horizontal ? offsetX : offsetY;
const contentSize = horizontal ? contentWidth : contentHeight;
const closeToStart = offset <= threshold;
if (closeToStart !== isScrollAtStart) {
setScrollAtStart(closeToStart);
}

const closeToEnd = layoutSize + offset >= contentSize - threshold;
if (closeToEnd !== isScrollAtEnd) {
setScrollAtEnd(closeToEnd);
}
},
[isScrollAtStart, isScrollAtEnd]
);

return (
<WrappedComponent
{...props}
scrollReachedProps={{onScroll, isScrollAtStart, isScrollAtEnd}}
ref={props.forwardedRef}
/>
);
};

return forwardRef(ScrollReachedDetector);
}

export default withScrollReached;
3 changes: 3 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,9 @@ export default {
get withScrollEnabler() {
return require('./commons').withScrollEnabler;
},
get withScrollReached() {
return require('./commons').withScrollReached;
},

// Helpers
get AvatarHelper() {
Expand Down