Skip to content

Commit f746c46

Browse files
authored
Merge pull request #27236 from hurali97/perf/disable-hover-when-scrolling
perf: disable hover when scrolling on web and desktop
2 parents b10e0bc + a88f7b7 commit f746c46

File tree

8 files changed

+141
-3
lines changed

8 files changed

+141
-3
lines changed

src/CONST.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2647,6 +2647,9 @@ const CONST = {
26472647
HTTPS: 'https',
26482648
PUSHER: 'pusher',
26492649
},
2650+
EVENTS: {
2651+
SCROLLING: 'scrolling',
2652+
},
26502653
} as const;
26512654

26522655
export default CONST;

src/components/Hoverable/hoverablePropTypes.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,16 @@ const propTypes = {
1212

1313
/** Function that executes when the mouse leaves the children. */
1414
onHoverOut: PropTypes.func,
15+
16+
/** Decides whether to handle the scroll behaviour to show hover once the scroll ends */
17+
shouldHandleScroll: PropTypes.bool,
1518
};
1619

1720
const defaultProps = {
1821
disabled: false,
1922
onHoverIn: () => {},
2023
onHoverOut: () => {},
24+
shouldHandleScroll: false,
2125
};
2226

2327
export {propTypes, defaultProps};

src/components/Hoverable/index.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import _ from 'underscore';
22
import React, {Component} from 'react';
3+
import {DeviceEventEmitter} from 'react-native';
34
import {propTypes, defaultProps} from './hoverablePropTypes';
45
import * as DeviceCapabilities from '../../libs/DeviceCapabilities';
6+
import CONST from '../../CONST';
57

68
/**
79
* It is necessary to create a Hoverable component instead of relying solely on Pressable support for hover state,
@@ -19,12 +21,37 @@ class Hoverable extends Component {
1921
isHovered: false,
2022
};
2123

24+
this.isHoveredRef = false;
25+
this.isScrollingRef = false;
2226
this.wrapperView = null;
2327
}
2428

2529
componentDidMount() {
2630
document.addEventListener('visibilitychange', this.handleVisibilityChange);
2731
document.addEventListener('mouseover', this.checkHover);
32+
33+
/**
34+
* Only add the scrolling listener if the shouldHandleScroll prop is true
35+
* and the scrollingListener is not already set.
36+
*/
37+
if (!this.scrollingListener && this.props.shouldHandleScroll) {
38+
this.scrollingListener = DeviceEventEmitter.addListener(CONST.EVENTS.SCROLLING, (scrolling) => {
39+
/**
40+
* If user has stopped scrolling and the isHoveredRef is true, then we should update the hover state.
41+
*/
42+
if (!scrolling && this.isHoveredRef) {
43+
this.setState({isHovered: this.isHoveredRef}, this.props.onHoverIn);
44+
} else if (scrolling && this.isHoveredRef) {
45+
/**
46+
* If the user has started scrolling and the isHoveredRef is true, then we should set the hover state to false.
47+
* This is to hide the existing hover and reaction bar.
48+
*/
49+
this.isHoveredRef = false;
50+
this.setState({isHovered: false}, this.props.onHoverOut);
51+
}
52+
this.isScrollingRef = scrolling;
53+
});
54+
}
2855
}
2956

3057
componentDidUpdate(prevProps) {
@@ -40,6 +67,9 @@ class Hoverable extends Component {
4067
componentWillUnmount() {
4168
document.removeEventListener('visibilitychange', this.handleVisibilityChange);
4269
document.removeEventListener('mouseover', this.checkHover);
70+
if (this.scrollingListener) {
71+
this.scrollingListener.remove();
72+
}
4373
}
4474

4575
/**
@@ -52,6 +82,17 @@ class Hoverable extends Component {
5282
return;
5383
}
5484

85+
/**
86+
* Capture whther or not the user is hovering over the component.
87+
* We will use this to determine if we should update the hover state when the user has stopped scrolling.
88+
*/
89+
this.isHoveredRef = isHovered;
90+
91+
/**
92+
* If the isScrollingRef is true, then the user is scrolling and we should not update the hover state.
93+
*/
94+
if (this.isScrollingRef && this.props.shouldHandleScroll && !this.state.isHovered) return;
95+
5596
if (isHovered !== this.state.isHovered) {
5697
this.setState({isHovered}, isHovered ? this.props.onHoverIn : this.props.onHoverOut);
5798
}

src/components/InvertedFlatList/index.js

Lines changed: 83 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import React, {forwardRef, useEffect} from 'react';
1+
import React, {forwardRef, useEffect, useRef} from 'react';
22
import PropTypes from 'prop-types';
3-
import {FlatList, StyleSheet} from 'react-native';
3+
import {DeviceEventEmitter, FlatList, StyleSheet} from 'react-native';
44
import _ from 'underscore';
55
import BaseInvertedFlatList from './BaseInvertedFlatList';
66
import styles from '../../styles/styles';
7+
import CONST from '../../CONST';
78

89
const propTypes = {
910
/** Passed via forwardRef so we can access the FlatList ref */
@@ -14,6 +15,9 @@ const propTypes = {
1415
/** Any additional styles to apply */
1516
// eslint-disable-next-line react/forbid-prop-types
1617
contentContainerStyle: PropTypes.any,
18+
19+
/** Same as for FlatList */
20+
onScroll: PropTypes.func,
1721
};
1822

1923
// This is adapted from https://codesandbox.io/s/react-native-dsyse
@@ -22,15 +26,90 @@ function InvertedFlatList(props) {
2226
const {innerRef, contentContainerStyle} = props;
2327
const listRef = React.createRef();
2428

29+
const lastScrollEvent = useRef(null);
30+
const scrollEndTimeout = useRef(null);
31+
const updateInProgress = useRef(false);
32+
const eventHandler = useRef(null);
33+
2534
useEffect(() => {
2635
if (!_.isFunction(innerRef)) {
2736
// eslint-disable-next-line no-param-reassign
2837
innerRef.current = listRef.current;
2938
} else {
3039
innerRef(listRef);
3140
}
41+
42+
return () => {
43+
if (scrollEndTimeout.current) {
44+
clearTimeout(scrollEndTimeout.current);
45+
}
46+
47+
if (eventHandler.current) {
48+
eventHandler.current.remove();
49+
}
50+
};
3251
}, [innerRef, listRef]);
3352

53+
/**
54+
* Emits when the scrolling is in progress. Also,
55+
* invokes the onScroll callback function from props.
56+
*
57+
* @param {Event} event - The onScroll event from the FlatList
58+
*/
59+
const onScroll = (event) => {
60+
props.onScroll(event);
61+
62+
if (!updateInProgress.current) {
63+
updateInProgress.current = true;
64+
eventHandler.current = DeviceEventEmitter.emit(CONST.EVENTS.SCROLLING, true);
65+
}
66+
};
67+
68+
/**
69+
* Emits when the scrolling has ended.
70+
*/
71+
const onScrollEnd = () => {
72+
eventHandler.current = DeviceEventEmitter.emit(CONST.EVENTS.SCROLLING, false);
73+
updateInProgress.current = false;
74+
};
75+
76+
/**
77+
* Decides whether the scrolling has ended or not. If it has ended,
78+
* then it calls the onScrollEnd function. Otherwise, it calls the
79+
* onScroll function and pass the event to it.
80+
*
81+
* This is a temporary work around, since react-native-web doesn't
82+
* support onScrollBeginDrag and onScrollEndDrag props for FlatList.
83+
* More info:
84+
* https://github.com/necolas/react-native-web/pull/1305
85+
*
86+
* This workaround is taken from below and refactored to fit our needs:
87+
* https://github.com/necolas/react-native-web/issues/1021#issuecomment-984151185
88+
*
89+
* @param {Event} event - The onScroll event from the FlatList
90+
*/
91+
const handleScroll = (event) => {
92+
onScroll(event);
93+
const timestamp = Date.now();
94+
95+
if (scrollEndTimeout.current) {
96+
clearTimeout(scrollEndTimeout.current);
97+
}
98+
99+
if (lastScrollEvent.current) {
100+
scrollEndTimeout.current = setTimeout(() => {
101+
if (lastScrollEvent.current !== timestamp) {
102+
return;
103+
}
104+
// Scroll has ended
105+
lastScrollEvent.current = null;
106+
onScrollEnd();
107+
}, 250);
108+
}
109+
110+
lastScrollEvent.current = timestamp;
111+
};
112+
34113
return (
35114
<BaseInvertedFlatList
36115
// eslint-disable-next-line react/jsx-props-no-spreading
@@ -39,13 +118,15 @@ function InvertedFlatList(props) {
39118
ref={listRef}
40119
shouldMeasureItems
41120
contentContainerStyle={StyleSheet.compose(contentContainerStyle, styles.justifyContentEnd)}
121+
onScroll={handleScroll}
42122
/>
43123
);
44124
}
45125

46126
InvertedFlatList.propTypes = propTypes;
47127
InvertedFlatList.defaultProps = {
48128
contentContainerStyle: {},
129+
onScroll: () => {},
49130
};
50131

51132
export default forwardRef((props, ref) => (

src/components/Tooltip/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ function Tooltip(props) {
154154
<Hoverable
155155
onHoverIn={showTooltip}
156156
onHoverOut={hideTooltip}
157+
shouldHandleScroll={props.shouldHandleScroll}
157158
>
158159
{children}
159160
</Hoverable>

src/components/Tooltip/tooltipPropTypes.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ const propTypes = {
2828

2929
/** Unique key of renderTooltipContent to rerender the tooltip when one of the key changes */
3030
renderTooltipContentKey: PropTypes.arrayOf(PropTypes.string),
31+
32+
/** passes this down to Hoverable component to decide whether to handle the scroll behaviour to show hover once the scroll ends */
33+
shouldHandleScroll: PropTypes.bool,
3134
};
3235

3336
const defaultProps = {
@@ -38,6 +41,7 @@ const defaultProps = {
3841
numberOfLines: CONST.TOOLTIP_MAX_LINES,
3942
renderTooltipContent: undefined,
4043
renderTooltipContentKey: [],
44+
shouldHandleScroll: false,
4145
};
4246

4347
export {propTypes, defaultProps};

src/components/UserDetailsTooltip/index.web.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ function UserDetailsTooltip(props) {
6666
shiftHorizontal={props.shiftHorizontal}
6767
renderTooltipContent={renderTooltipContent}
6868
renderTooltipContentKey={[userDisplayName, userLogin]}
69+
shouldHandleScroll
6970
>
7071
{props.children}
7172
</Tooltip>

src/pages/home/report/ReportActionItem.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -593,7 +593,10 @@ function ReportActionItem(props) {
593593
withoutFocusOnSecondaryInteraction
594594
accessibilityLabel={props.translate('accessibilityHints.chatMessage')}
595595
>
596-
<Hoverable disabled={Boolean(props.draftMessage)}>
596+
<Hoverable
597+
shouldHandleScroll
598+
disabled={Boolean(props.draftMessage)}
599+
>
597600
{(hovered) => (
598601
<View>
599602
{props.shouldDisplayNewMarker && <UnreadActionIndicator reportActionID={props.action.reportActionID} />}

0 commit comments

Comments
 (0)