diff --git a/src/CONST.ts b/src/CONST.ts
index 93a17ac3c70d..dd676a9465e9 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -2643,6 +2643,9 @@ const CONST = {
HTTPS: 'https',
PUSHER: 'pusher',
},
+ EVENTS: {
+ SCROLLING: 'scrolling',
+ },
} as const;
export default CONST;
diff --git a/src/components/Hoverable/hoverablePropTypes.js b/src/components/Hoverable/hoverablePropTypes.js
index 9fb2e3bc7306..d483a06d6aaf 100644
--- a/src/components/Hoverable/hoverablePropTypes.js
+++ b/src/components/Hoverable/hoverablePropTypes.js
@@ -12,12 +12,16 @@ const propTypes = {
/** Function that executes when the mouse leaves the children. */
onHoverOut: PropTypes.func,
+
+ /** Decides whether to handle the scroll behaviour to show hover once the scroll ends */
+ shouldHandleScroll: PropTypes.bool,
};
const defaultProps = {
disabled: false,
onHoverIn: () => {},
onHoverOut: () => {},
+ shouldHandleScroll: false,
};
export {propTypes, defaultProps};
diff --git a/src/components/Hoverable/index.js b/src/components/Hoverable/index.js
index 0b560703a069..5da41f1388fb 100644
--- a/src/components/Hoverable/index.js
+++ b/src/components/Hoverable/index.js
@@ -1,7 +1,9 @@
import _ from 'underscore';
import React, {Component} from 'react';
+import {DeviceEventEmitter} from 'react-native';
import {propTypes, defaultProps} from './hoverablePropTypes';
import * as DeviceCapabilities from '../../libs/DeviceCapabilities';
+import CONST from '../../CONST';
/**
* 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 {
isHovered: false,
};
+ this.isHoveredRef = false;
+ this.isScrollingRef = false;
this.wrapperView = null;
}
componentDidMount() {
document.addEventListener('visibilitychange', this.handleVisibilityChange);
document.addEventListener('mouseover', this.checkHover);
+
+ /**
+ * Only add the scrolling listener if the shouldHandleScroll prop is true
+ * and the scrollingListener is not already set.
+ */
+ if (!this.scrollingListener && this.props.shouldHandleScroll) {
+ this.scrollingListener = DeviceEventEmitter.addListener(CONST.EVENTS.SCROLLING, (scrolling) => {
+ /**
+ * If user has stopped scrolling and the isHoveredRef is true, then we should update the hover state.
+ */
+ if (!scrolling && this.isHoveredRef) {
+ this.setState({isHovered: this.isHoveredRef}, this.props.onHoverIn);
+ } else if (scrolling && this.isHoveredRef) {
+ /**
+ * If the user has started scrolling and the isHoveredRef is true, then we should set the hover state to false.
+ * This is to hide the existing hover and reaction bar.
+ */
+ this.isHoveredRef = false;
+ this.setState({isHovered: false}, this.props.onHoverOut);
+ }
+ this.isScrollingRef = scrolling;
+ });
+ }
}
componentDidUpdate(prevProps) {
@@ -40,6 +67,9 @@ class Hoverable extends Component {
componentWillUnmount() {
document.removeEventListener('visibilitychange', this.handleVisibilityChange);
document.removeEventListener('mouseover', this.checkHover);
+ if (this.scrollingListener) {
+ this.scrollingListener.remove();
+ }
}
/**
@@ -52,6 +82,17 @@ class Hoverable extends Component {
return;
}
+ /**
+ * Capture whther or not the user is hovering over the component.
+ * We will use this to determine if we should update the hover state when the user has stopped scrolling.
+ */
+ this.isHoveredRef = isHovered;
+
+ /**
+ * If the isScrollingRef is true, then the user is scrolling and we should not update the hover state.
+ */
+ if (this.isScrollingRef && this.props.shouldHandleScroll && !this.state.isHovered) return;
+
if (isHovered !== this.state.isHovered) {
this.setState({isHovered}, isHovered ? this.props.onHoverIn : this.props.onHoverOut);
}
diff --git a/src/components/InvertedFlatList/index.js b/src/components/InvertedFlatList/index.js
index e8e546385207..74409e9a0fe0 100644
--- a/src/components/InvertedFlatList/index.js
+++ b/src/components/InvertedFlatList/index.js
@@ -1,9 +1,10 @@
-import React, {forwardRef, useEffect} from 'react';
+import React, {forwardRef, useEffect, useRef} from 'react';
import PropTypes from 'prop-types';
-import {FlatList, StyleSheet} from 'react-native';
+import {DeviceEventEmitter, FlatList, StyleSheet} from 'react-native';
import _ from 'underscore';
import BaseInvertedFlatList from './BaseInvertedFlatList';
import styles from '../../styles/styles';
+import CONST from '../../CONST';
const propTypes = {
/** Passed via forwardRef so we can access the FlatList ref */
@@ -14,6 +15,9 @@ const propTypes = {
/** Any additional styles to apply */
// eslint-disable-next-line react/forbid-prop-types
contentContainerStyle: PropTypes.any,
+
+ /** Same as for FlatList */
+ onScroll: PropTypes.func,
};
// This is adapted from https://codesandbox.io/s/react-native-dsyse
@@ -22,6 +26,11 @@ function InvertedFlatList(props) {
const {innerRef, contentContainerStyle} = props;
const listRef = React.createRef();
+ const lastScrollEvent = useRef(null);
+ const scrollEndTimeout = useRef(null);
+ const updateInProgress = useRef(false);
+ const eventHandler = useRef(null);
+
useEffect(() => {
if (!_.isFunction(innerRef)) {
// eslint-disable-next-line no-param-reassign
@@ -29,8 +38,78 @@ function InvertedFlatList(props) {
} else {
innerRef(listRef);
}
+
+ return () => {
+ if (scrollEndTimeout.current) {
+ clearTimeout(scrollEndTimeout.current);
+ }
+
+ if (eventHandler.current) {
+ eventHandler.current.remove();
+ }
+ };
}, [innerRef, listRef]);
+ /**
+ * Emits when the scrolling is in progress. Also,
+ * invokes the onScroll callback function from props.
+ *
+ * @param {Event} event - The onScroll event from the FlatList
+ */
+ const onScroll = (event) => {
+ props.onScroll(event);
+
+ if (!updateInProgress.current) {
+ updateInProgress.current = true;
+ eventHandler.current = DeviceEventEmitter.emit(CONST.EVENTS.SCROLLING, true);
+ }
+ };
+
+ /**
+ * Emits when the scrolling has ended.
+ */
+ const onScrollEnd = () => {
+ eventHandler.current = DeviceEventEmitter.emit(CONST.EVENTS.SCROLLING, false);
+ updateInProgress.current = false;
+ };
+
+ /**
+ * Decides whether the scrolling has ended or not. If it has ended,
+ * then it calls the onScrollEnd function. Otherwise, it calls the
+ * onScroll function and pass the event to it.
+ *
+ * This is a temporary work around, since react-native-web doesn't
+ * support onScrollBeginDrag and onScrollEndDrag props for FlatList.
+ * More info:
+ * https://github.com/necolas/react-native-web/pull/1305
+ *
+ * This workaround is taken from below and refactored to fit our needs:
+ * https://github.com/necolas/react-native-web/issues/1021#issuecomment-984151185
+ *
+ * @param {Event} event - The onScroll event from the FlatList
+ */
+ const handleScroll = (event) => {
+ onScroll(event);
+ const timestamp = Date.now();
+
+ if (scrollEndTimeout.current) {
+ clearTimeout(scrollEndTimeout.current);
+ }
+
+ if (lastScrollEvent.current) {
+ scrollEndTimeout.current = setTimeout(() => {
+ if (lastScrollEvent.current !== timestamp) {
+ return;
+ }
+ // Scroll has ended
+ lastScrollEvent.current = null;
+ onScrollEnd();
+ }, 250);
+ }
+
+ lastScrollEvent.current = timestamp;
+ };
+
return (
);
}
@@ -46,6 +126,7 @@ function InvertedFlatList(props) {
InvertedFlatList.propTypes = propTypes;
InvertedFlatList.defaultProps = {
contentContainerStyle: {},
+ onScroll: () => {},
};
export default forwardRef((props, ref) => (
diff --git a/src/components/Tooltip/index.js b/src/components/Tooltip/index.js
index 398df07649cf..f60982f52dd4 100644
--- a/src/components/Tooltip/index.js
+++ b/src/components/Tooltip/index.js
@@ -154,6 +154,7 @@ function Tooltip(props) {
{children}
diff --git a/src/components/Tooltip/tooltipPropTypes.js b/src/components/Tooltip/tooltipPropTypes.js
index af18c4cfa412..2ddf8120d58c 100644
--- a/src/components/Tooltip/tooltipPropTypes.js
+++ b/src/components/Tooltip/tooltipPropTypes.js
@@ -28,6 +28,9 @@ const propTypes = {
/** Unique key of renderTooltipContent to rerender the tooltip when one of the key changes */
renderTooltipContentKey: PropTypes.arrayOf(PropTypes.string),
+
+ /** passes this down to Hoverable component to decide whether to handle the scroll behaviour to show hover once the scroll ends */
+ shouldHandleScroll: PropTypes.bool,
};
const defaultProps = {
@@ -38,6 +41,7 @@ const defaultProps = {
numberOfLines: CONST.TOOLTIP_MAX_LINES,
renderTooltipContent: undefined,
renderTooltipContentKey: [],
+ shouldHandleScroll: false,
};
export {propTypes, defaultProps};
diff --git a/src/components/UserDetailsTooltip/index.web.js b/src/components/UserDetailsTooltip/index.web.js
index 5fdae15184ac..1a78459d30a6 100644
--- a/src/components/UserDetailsTooltip/index.web.js
+++ b/src/components/UserDetailsTooltip/index.web.js
@@ -66,6 +66,7 @@ function UserDetailsTooltip(props) {
shiftHorizontal={props.shiftHorizontal}
renderTooltipContent={renderTooltipContent}
renderTooltipContentKey={[userDisplayName, userLogin]}
+ shouldHandleScroll
>
{props.children}
diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js
index 7440b28a8b3b..8cf8fd78371d 100644
--- a/src/pages/home/report/ReportActionItem.js
+++ b/src/pages/home/report/ReportActionItem.js
@@ -593,7 +593,10 @@ function ReportActionItem(props) {
withoutFocusOnSecondaryInteraction
accessibilityLabel={props.translate('accessibilityHints.chatMessage')}
>
-
+
{(hovered) => (
{props.shouldDisplayNewMarker && }