Skip to content

Commit 6b8c9ea

Browse files
committed
Add (and use) useScrollEnabler and useScrollReached hooks
1 parent 462647b commit 6b8c9ea

File tree

11 files changed

+275
-93
lines changed

11 files changed

+275
-93
lines changed
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { LayoutChangeEvent } from 'react-native';
2+
export declare type Props = {
3+
/**
4+
* Whether the scroll is horizontal (default is false).
5+
*/
6+
horizontal?: boolean;
7+
};
8+
export declare type ResultProps = {
9+
/**
10+
* onContentSizeChange callback (should be set to your onContentSizeChange).
11+
*/
12+
onContentSizeChange: (contentWidth: number, contentHeight: number) => void;
13+
/**
14+
* onLayout callback (should be set to your onLayout).
15+
*/
16+
onLayout: (event: LayoutChangeEvent) => void;
17+
/**
18+
* Whether the scroll should be enabled (should be set to your scrollEnabled).
19+
*/
20+
scrollEnabled: boolean;
21+
};
22+
declare const useScrollEnabler: (props?: Props) => ResultProps;
23+
export default useScrollEnabler;
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { NativeSyntheticEvent, NativeScrollEvent } from 'react-native';
2+
export declare type Props = {
3+
/**
4+
* Whether the scroll is horizontal (default is false).
5+
*/
6+
horizontal?: boolean;
7+
/**
8+
* Allows to be notified prior to actually reaching the start \ end of the scroll (by the threshold).
9+
* Should be a positive value.
10+
*/
11+
threshold?: number;
12+
};
13+
export declare type ResultProps = {
14+
/**
15+
* onScroll callback (should be set to your onScroll).
16+
*/
17+
onScroll: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
18+
/**
19+
* Is the scroll at the start (or equal\smaller than the threshold if one was given)
20+
*/
21+
isScrollAtStart?: boolean;
22+
/**
23+
* Is the scroll at the end (or equal\greater than the threshold if one was given)
24+
*/
25+
isScrollAtEnd?: boolean;
26+
};
27+
declare const useScrollReached: (props?: Props) => ResultProps;
28+
export default useScrollReached;
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import React from 'react';
22
import { ViewProps, ScrollViewProps } from 'react-native';
3+
import { ForwardRefInjectedProps } from '../../commons/forwardRef';
34
export declare type FadedScrollViewProps = ViewProps & ScrollViewProps & {
45
children?: React.ReactNode | React.ReactNode[];
56
};
6-
declare const _default: React.ComponentClass<FadedScrollViewProps, any> | React.FunctionComponent<FadedScrollViewProps>;
7+
declare type Props = FadedScrollViewProps & ForwardRefInjectedProps;
8+
declare const _default: React.ComponentType<Props>;
79
export default _default;

generatedTypes/helpers/FocusItemHelper.d.ts renamed to generatedTypes/helpers/useFocusItemHelper.d.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,5 +52,5 @@ export declare type ResultProps = {
5252
*/
5353
focusIndex: (index: number, animated?: boolean) => void;
5454
};
55-
declare const focusItemsHelper: (props: Props) => ResultProps;
56-
export default focusItemsHelper;
55+
declare const useFocusItemsHelper: (props: Props) => ResultProps;
56+
export default useFocusItemsHelper;

src/commons/index.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,15 @@ module.exports = {
2525
get withScrollEnabler() {
2626
return require('./withScrollEnabler').default;
2727
},
28+
get useScrollEnabler() {
29+
return require('./useScrollEnabler').default;
30+
},
2831
get withScrollReached() {
2932
return require('./withScrollReached').default;
3033
},
34+
get useScrollReached() {
35+
return require('./useScrollReached').default;
36+
},
3137
get modifiers() {
3238
return require('./modifiers');
3339
}

src/commons/useScrollEnabler.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import {useState, useCallback, useRef} from 'react';
2+
import {LayoutChangeEvent} from 'react-native';
3+
4+
export type Props = {
5+
/**
6+
* Whether the scroll is horizontal (default is false).
7+
*/
8+
horizontal?: boolean;
9+
};
10+
11+
export type ResultProps = {
12+
/**
13+
* onContentSizeChange callback (should be set to your onContentSizeChange).
14+
*/
15+
onContentSizeChange: (contentWidth: number, contentHeight: number) => void;
16+
/**
17+
* onLayout callback (should be set to your onLayout).
18+
*/
19+
onLayout: (event: LayoutChangeEvent) => void;
20+
/**
21+
* Whether the scroll should be enabled (should be set to your scrollEnabled).
22+
*/
23+
scrollEnabled: boolean;
24+
};
25+
26+
const useScrollEnabler = (props: Props = {}): ResultProps => {
27+
const {horizontal = false} = props;
28+
const [scrollEnabled, setScrollEnabled] = useState(true);
29+
const contentSize = useRef<number>(0);
30+
const layoutSize = useRef<number>(0);
31+
32+
const checkScroll = useCallback(() => {
33+
const isScrollEnabled = contentSize.current > layoutSize.current;
34+
if (isScrollEnabled !== scrollEnabled) {
35+
setScrollEnabled(isScrollEnabled);
36+
}
37+
}, [scrollEnabled]);
38+
39+
const onContentSizeChange = useCallback((contentWidth: number, contentHeight: number) => {
40+
const size = horizontal ? contentWidth : contentHeight;
41+
if (size !== contentSize.current) {
42+
contentSize.current = size;
43+
if (layoutSize.current > 0) {
44+
checkScroll();
45+
}
46+
}
47+
},
48+
[horizontal, checkScroll]);
49+
50+
const onLayout = useCallback((event: LayoutChangeEvent) => {
51+
const {
52+
nativeEvent: {
53+
layout: {width, height}
54+
}
55+
} = event;
56+
const size = horizontal ? width : height;
57+
if (size !== layoutSize.current) {
58+
layoutSize.current = size;
59+
if (contentSize.current > 0) {
60+
checkScroll();
61+
}
62+
}
63+
},
64+
[horizontal, checkScroll]);
65+
66+
return {
67+
onContentSizeChange,
68+
onLayout,
69+
scrollEnabled
70+
};
71+
};
72+
73+
export default useScrollEnabler;

src/commons/useScrollReached.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import {useState, useCallback} from 'react';
2+
import {NativeSyntheticEvent, NativeScrollEvent} from 'react-native';
3+
import {Constants} from '../helpers';
4+
5+
export type Props = {
6+
/**
7+
* Whether the scroll is horizontal (default is false).
8+
*/
9+
horizontal?: boolean;
10+
/**
11+
* Allows to be notified prior to actually reaching the start \ end of the scroll (by the threshold).
12+
* Should be a positive value.
13+
*/
14+
threshold?: number;
15+
};
16+
17+
export type ResultProps = {
18+
/**
19+
* onScroll callback (should be set to your onScroll).
20+
*/
21+
onScroll: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
22+
/**
23+
* Is the scroll at the start (or equal\smaller than the threshold if one was given)
24+
*/
25+
isScrollAtStart?: boolean;
26+
/**
27+
* Is the scroll at the end (or equal\greater than the threshold if one was given)
28+
*/
29+
isScrollAtEnd?: boolean;
30+
};
31+
32+
const DEFAULT_THRESHOLD = Constants.isAndroid ? 1 : 0;
33+
34+
const useScrollReached = (props: Props = {}): ResultProps => {
35+
const {horizontal = false, threshold = DEFAULT_THRESHOLD} = props;
36+
const [isScrollAtStart, setScrollAtStart] = useState(true);
37+
const [isScrollAtEnd, setScrollAtEnd] = useState(false);
38+
39+
const onScroll = useCallback((event: NativeSyntheticEvent<NativeScrollEvent>) => {
40+
const {
41+
nativeEvent: {
42+
layoutMeasurement: {width: layoutWidth, height: layoutHeight},
43+
contentOffset: {x: offsetX, y: offsetY},
44+
contentSize: {width: contentWidth, height: contentHeight}
45+
}
46+
} = event;
47+
48+
const layoutSize = horizontal ? layoutWidth : layoutHeight;
49+
const offset = horizontal ? offsetX : offsetY;
50+
const contentSize = horizontal ? contentWidth : contentHeight;
51+
const closeToStart = offset <= threshold;
52+
if (closeToStart !== isScrollAtStart) {
53+
setScrollAtStart(closeToStart);
54+
}
55+
56+
const closeToEnd = layoutSize + offset >= contentSize - threshold;
57+
if (closeToEnd !== isScrollAtEnd) {
58+
setScrollAtEnd(closeToEnd);
59+
}
60+
},
61+
[horizontal, threshold, isScrollAtStart, isScrollAtEnd]);
62+
63+
return {onScroll, isScrollAtStart, isScrollAtEnd};
64+
};
65+
66+
export default useScrollReached;

src/commons/withScrollEnabler.tsx

Lines changed: 24 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import React, {useState, useCallback, useRef} from 'react';
2-
// eslint-disable-next-line no-unused-vars
32
import {FlatListProps, ScrollViewProps, LayoutChangeEvent} from 'react-native';
4-
// eslint-disable-next-line no-unused-vars
53
import forwardRef, {ForwardRefInjectedProps} from './forwardRef';
64
//@ts-ignore
75
import hoistStatics from 'hoist-non-react-statics';
@@ -21,9 +19,7 @@ export type WithScrollEnablerProps = {
2119

2220
type PropTypes = ForwardRefInjectedProps & SupportedViewsProps;
2321

24-
function withScrollEnabler<PROPS, STATICS = {}>(
25-
WrappedComponent: React.ComponentType<PROPS & WithScrollEnablerProps>
26-
): React.ComponentType<PROPS> & STATICS {
22+
function withScrollEnabler<PROPS, STATICS = {}>(WrappedComponent: React.ComponentType<PROPS & WithScrollEnablerProps>): React.ComponentType<PROPS> & STATICS {
2723
const ScrollEnabler: React.FunctionComponent<PROPS & PropTypes> = (props: PROPS & PropTypes) => {
2824
const [scrollEnabled, setScrollEnabled] = useState(true);
2925
const contentSize = useRef(0);
@@ -36,36 +32,32 @@ function withScrollEnabler<PROPS, STATICS = {}>(
3632
}
3733
}, [scrollEnabled]);
3834

39-
const onContentSizeChange = useCallback(
40-
(contentWidth: number, contentHeight: number) => {
41-
const size = props.horizontal ? contentWidth : contentHeight;
42-
if (size !== contentSize.current) {
43-
contentSize.current = size;
44-
if (layoutSize.current > 0) {
45-
checkScroll();
46-
}
35+
const onContentSizeChange = useCallback((contentWidth: number, contentHeight: number) => {
36+
const size = props.horizontal ? contentWidth : contentHeight;
37+
if (size !== contentSize.current) {
38+
contentSize.current = size;
39+
if (layoutSize.current > 0) {
40+
checkScroll();
4741
}
48-
},
49-
[props.horizontal, checkScroll]
50-
);
42+
}
43+
},
44+
[props.horizontal, checkScroll]);
5145

52-
const onLayout = useCallback(
53-
(event: LayoutChangeEvent) => {
54-
const {
55-
nativeEvent: {
56-
layout: {width, height}
57-
}
58-
} = event;
59-
const size = props.horizontal ? width : height;
60-
if (size !== layoutSize.current) {
61-
layoutSize.current = size;
62-
if (contentSize.current > 0) {
63-
checkScroll();
64-
}
46+
const onLayout = useCallback((event: LayoutChangeEvent) => {
47+
const {
48+
nativeEvent: {
49+
layout: {width, height}
6550
}
66-
},
67-
[props.horizontal, checkScroll]
68-
);
51+
} = event;
52+
const size = props.horizontal ? width : height;
53+
if (size !== layoutSize.current) {
54+
layoutSize.current = size;
55+
if (contentSize.current > 0) {
56+
checkScroll();
57+
}
58+
}
59+
},
60+
[props.horizontal, checkScroll]);
6961

7062
return (
7163
<WrappedComponent

src/commons/withScrollReached.tsx

Lines changed: 26 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,5 @@
11
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
2+
import {FlatListProps, ScrollViewProps, NativeSyntheticEvent, NativeScrollEvent} from 'react-native';
133
import forwardRef, {ForwardRefInjectedProps} from './forwardRef';
144
//@ts-ignore
155
import hoistStatics from 'hoist-non-react-statics';
@@ -55,41 +45,37 @@ const DEFAULT_THRESHOLD = Constants.isAndroid ? 1 : 0;
5545
* @example: https://github.com/wix/react-native-ui-lib/blob/master/demo/src/screens/componentScreens/WithScrollReachedScreen.tsx
5646
* @notes: Send `props.scrollReachedProps.onScroll` to your onScroll and receive via props.scrollReachedProps.isScrollAtStart props.scrollReachedProps.isScrollAtEnd
5747
*/
58-
function withScrollReached<PROPS, STATICS = {}>(
59-
WrappedComponent: React.ComponentType<PROPS & WithScrollReachedProps>,
60-
options: WithScrollReachedOptionsProps = {}
61-
): React.ComponentType<PROPS> & STATICS {
48+
function withScrollReached<PROPS, STATICS = {}>(WrappedComponent: React.ComponentType<PROPS & WithScrollReachedProps>,
49+
options: WithScrollReachedOptionsProps = {}): React.ComponentType<PROPS> & STATICS {
6250
const ScrollReachedDetector: React.FunctionComponent<PROPS & PropTypes> = (props: PROPS & PropTypes) => {
6351
// The scroll starts at the start, from what I've tested this works fine
6452
const [isScrollAtStart, setScrollAtStart] = useState(true);
6553
const [isScrollAtEnd, setScrollAtEnd] = useState(false);
66-
const onScroll = useCallback(
67-
(event: NativeSyntheticEvent<NativeScrollEvent>) => {
68-
const {
69-
nativeEvent: {
70-
layoutMeasurement: {width: layoutWidth, height: layoutHeight},
71-
contentOffset: {x: offsetX, y: offsetY},
72-
contentSize: {width: contentWidth, height: contentHeight}
73-
}
74-
} = event;
75-
76-
const horizontal = options.horizontal;
77-
const threshold = options.threshold || DEFAULT_THRESHOLD;
78-
const layoutSize = horizontal ? layoutWidth : layoutHeight;
79-
const offset = horizontal ? offsetX : offsetY;
80-
const contentSize = horizontal ? contentWidth : contentHeight;
81-
const closeToStart = offset <= threshold;
82-
if (closeToStart !== isScrollAtStart) {
83-
setScrollAtStart(closeToStart);
54+
const onScroll = useCallback((event: NativeSyntheticEvent<NativeScrollEvent>) => {
55+
const {
56+
nativeEvent: {
57+
layoutMeasurement: {width: layoutWidth, height: layoutHeight},
58+
contentOffset: {x: offsetX, y: offsetY},
59+
contentSize: {width: contentWidth, height: contentHeight}
8460
}
61+
} = event;
8562

86-
const closeToEnd = layoutSize + offset >= contentSize - threshold;
87-
if (closeToEnd !== isScrollAtEnd) {
88-
setScrollAtEnd(closeToEnd);
89-
}
90-
},
91-
[isScrollAtStart, isScrollAtEnd]
92-
);
63+
const horizontal = options.horizontal;
64+
const threshold = options.threshold || DEFAULT_THRESHOLD;
65+
const layoutSize = horizontal ? layoutWidth : layoutHeight;
66+
const offset = horizontal ? offsetX : offsetY;
67+
const contentSize = horizontal ? contentWidth : contentHeight;
68+
const closeToStart = offset <= threshold;
69+
if (closeToStart !== isScrollAtStart) {
70+
setScrollAtStart(closeToStart);
71+
}
72+
73+
const closeToEnd = layoutSize + offset >= contentSize - threshold;
74+
if (closeToEnd !== isScrollAtEnd) {
75+
setScrollAtEnd(closeToEnd);
76+
}
77+
},
78+
[isScrollAtStart, isScrollAtEnd]);
9379

9480
return (
9581
<WrappedComponent

0 commit comments

Comments
 (0)