diff --git a/src/components/EmojiPicker/EmojiPicker.js b/src/components/EmojiPicker/EmojiPicker.js index f87d08f3c02b..35a96eb38477 100644 --- a/src/components/EmojiPicker/EmojiPicker.js +++ b/src/components/EmojiPicker/EmojiPicker.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, {useState, useEffect, useRef, forwardRef, useImperativeHandle} from 'react'; import {Dimensions, Keyboard} from 'react-native'; import _ from 'underscore'; import EmojiPickerMenu from './EmojiPickerMenu'; @@ -9,6 +9,7 @@ import withWindowDimensions, {windowDimensionsPropTypes} from '../withWindowDime import withViewportOffsetTop, {viewportOffsetTopPropTypes} from '../withViewportOffsetTop'; import compose from '../../libs/compose'; import * as StyleUtils from '../../styles/StyleUtils'; +import calculateAnchorPosition from '../../libs/calculateAnchorPosition'; const DEFAULT_ANCHOR_ORIGIN = { horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.RIGHT, @@ -20,52 +21,85 @@ const propTypes = { ...viewportOffsetTopPropTypes, }; -class EmojiPicker extends React.Component { - constructor(props) { - super(props); - - this.hideEmojiPicker = this.hideEmojiPicker.bind(this); - this.showEmojiPicker = this.showEmojiPicker.bind(this); - this.selectEmoji = this.selectEmoji.bind(this); - this.measureEmojiPopoverAnchorPosition = this.measureEmojiPopoverAnchorPosition.bind(this); - this.measureEmojiPopoverAnchorPositionAndUpdateState = this.measureEmojiPopoverAnchorPositionAndUpdateState.bind(this); - this.focusEmojiSearchInput = this.focusEmojiSearchInput.bind(this); - this.onModalHide = () => {}; - this.onEmojiSelected = () => {}; - - this.state = { - reportAction: {}, - isEmojiPickerVisible: false, - - // The horizontal and vertical position (relative to the window) where the emoji popover will display. - emojiPopoverAnchorPosition: { - horizontal: 0, - vertical: 0, - }, - - emojiPopoverAnchorOrigin: DEFAULT_ANCHOR_ORIGIN, +const EmojiPicker = forwardRef((props, ref) => { + const [isEmojiPickerVisible, setIsEmojiPickerVisible] = useState(false); + const [emojiPopoverAnchorPosition, setEmojiPopoverAnchorPosition] = useState({ + horizontal: 0, + vertical: 0, + }); + const [reportAction, setReportAction] = useState({}); + const emojiPopoverAnchorOrigin = useRef(DEFAULT_ANCHOR_ORIGIN); + const emojiPopoverAnchor = useRef(null); + const onModalHide = useRef(() => {}); + const onEmojiSelected = useRef(() => {}); + const emojiSearchInput = useRef(); + + useEffect(() => { + if (isEmojiPickerVisible) { + Keyboard.dismiss(); + } + + const emojiPopoverDimensionListener = Dimensions.addEventListener('change', () => { + calculateAnchorPosition(emojiPopoverAnchor.current).then((value) => { + setEmojiPopoverAnchorPosition(value); + }); + }); + return () => { + emojiPopoverDimensionListener.remove(); }; - } + }, [isEmojiPickerVisible]); - componentDidMount() { - this.emojiPopoverDimensionListener = Dimensions.addEventListener('change', this.measureEmojiPopoverAnchorPositionAndUpdateState); - } + /** + * Show the emoji picker menu. + * + * @param {Function} [onModalHideValue=() => {}] - Run a callback when Modal hides. + * @param {Function} [onEmojiSelectedValue=() => {}] - Run a callback when Emoji selected. + * @param {Element} emojiPopoverAnchorValue - Element to which Popover is anchored + * @param {Object} [anchorOrigin=DEFAULT_ANCHOR_ORIGIN] - Anchor origin for Popover + * @param {Function} [onWillShow=() => {}] - Run a callback when Popover will show + * @param {Object} reportActionValue - ReportAction for EmojiPicker + */ + const showEmojiPicker = (onModalHideValue, onEmojiSelectedValue, emojiPopoverAnchorValue, anchorOrigin, onWillShow = () => {}, reportActionValue) => { + onModalHide.current = onModalHideValue; + onEmojiSelected.current = onEmojiSelectedValue; + emojiPopoverAnchor.current = emojiPopoverAnchorValue; - componentDidUpdate(prevProps, prevState) { - if (prevState.isEmojiPickerVisible === this.state.isEmojiPickerVisible || !this.state.isEmojiPickerVisible) { - return; + if (emojiPopoverAnchor.current) { + // Drop focus to avoid blue focus ring. + emojiPopoverAnchor.current.blur(); } - // Dismiss the keyboard to provide a focus for the emoji picker to avoid selection issues. - Keyboard.dismiss(); - } + calculateAnchorPosition(emojiPopoverAnchor.current).then((value) => { + onWillShow(); + setIsEmojiPickerVisible(true); + setEmojiPopoverAnchorPosition(value); + emojiPopoverAnchorOrigin.current = anchorOrigin || DEFAULT_ANCHOR_ORIGIN; + setReportAction(reportActionValue); + }); + }; + + /** + * Hide the emoji picker menu. + * + * @param {Boolean} isNavigating + */ + const hideEmojiPicker = (isNavigating) => { + if (isNavigating) { + onModalHide.current = () => {}; + } + emojiPopoverAnchor.current = null; + setIsEmojiPickerVisible(false); + }; - componentWillUnmount() { - if (!this.emojiPopoverDimensionListener) { + /** + * Focus the search input in the emoji picker. + */ + const focusEmojiSearchInput = () => { + if (!emojiSearchInput.current) { return; } - this.emojiPopoverDimensionListener.remove(); - } + emojiSearchInput.current.focus(); + }; /** * Callback for the emoji picker to add whatever emoji is chosen into the main input @@ -73,33 +107,20 @@ class EmojiPicker extends React.Component { * @param {String} emoji * @param {Object} emojiObject */ - selectEmoji(emoji, emojiObject) { + const selectEmoji = (emoji, emojiObject) => { // Prevent fast click / multiple emoji selection; // The first click will hide the emoji picker by calling the hideEmojiPicker() function - // and in that function the emojiPopoverAnchor prop to will be set to null (synchronously) + // and in that function the emojiPopoverAnchor ref to will be set to null (synchronously) // thus we rely on that prop to prevent fast click / multiple emoji selection - if (!this.emojiPopoverAnchor) { + if (!emojiPopoverAnchor.current) { return; } - this.hideEmojiPicker(); - if (_.isFunction(this.onEmojiSelected)) { - this.onEmojiSelected(emoji, emojiObject); + hideEmojiPicker(false); + if (_.isFunction(onEmojiSelected.current)) { + onEmojiSelected.current(emoji, emojiObject); } - } - - /** - * Hide the emoji picker menu. - * - * @param {Boolean} isNavigating - */ - hideEmojiPicker(isNavigating) { - if (isNavigating) { - this.onModalHide = () => {}; - } - this.emojiPopoverAnchor = null; - this.setState({isEmojiPickerVisible: false}); - } + }; /** * Whether Context Menu is active for the Report Action. @@ -107,97 +128,43 @@ class EmojiPicker extends React.Component { * @param {Number|String} actionID * @return {Boolean} */ - isActiveReportAction(actionID) { - return Boolean(actionID) && this.state.reportAction.reportActionID === actionID; - } - - /** - * Show the emoji picker menu. - * - * @param {Function} [onModalHide=() => {}] - Run a callback when Modal hides. - * @param {Function} [onEmojiSelected=() => {}] - Run a callback when Emoji selected. - * @param {Element} emojiPopoverAnchor - Element to which Popover is anchored - * @param {Object} [anchorOrigin=DEFAULT_ANCHOR_ORIGIN] - Anchor origin for Popover - * @param {Function} [onWillShow=() => {}] - Run a callback when Popover will show - * @param {Object} reportAction - ReportAction for EmojiPicker - */ - showEmojiPicker(onModalHide, onEmojiSelected, emojiPopoverAnchor, anchorOrigin, onWillShow = () => {}, reportAction) { - this.onModalHide = onModalHide; - this.onEmojiSelected = onEmojiSelected; - this.emojiPopoverAnchor = emojiPopoverAnchor; - - if (this.emojiPopoverAnchor) { - // Drop focus to avoid blue focus ring. - emojiPopoverAnchor.blur(); - } - - this.measureEmojiPopoverAnchorPosition().then((emojiPopoverAnchorPosition) => { - onWillShow(); - this.setState({reportAction, isEmojiPickerVisible: true, emojiPopoverAnchorPosition, emojiPopoverAnchorOrigin: anchorOrigin || DEFAULT_ANCHOR_ORIGIN}); - }); - } - - measureEmojiPopoverAnchorPosition() { - return new Promise((resolve) => { - if (!this.emojiPopoverAnchor) { - return resolve({horizontal: 0, vertical: 0}); - } - this.emojiPopoverAnchor.measureInWindow((x, y, width) => resolve({horizontal: x + width, vertical: y})); - }); - } - - measureEmojiPopoverAnchorPositionAndUpdateState() { - this.measureEmojiPopoverAnchorPosition().then((emojiPopoverAnchorPosition) => { - this.setState({emojiPopoverAnchorPosition}); - }); - } - - /** - * Focus the search input in the emoji picker. - */ - focusEmojiSearchInput() { - // we won't focus the input if it's mobile device - if (!this.emojiSearchInput || this.props.isSmallScreenWidth) { - return; - } - this.emojiSearchInput.focus(); - } - - render() { - // There is no way to disable animations and they are really laggy, because there are so many - // emojis. The best alternative is to set it to 1ms so it just "pops" in and out - return ( - - (this.emojiSearchInput = el)} - /> - - ); - } -} + const isActiveReportAction = (actionID) => Boolean(actionID) && reportAction.reportActionID === actionID; + + useImperativeHandle(ref, () => ({showEmojiPicker, isActiveReportAction, hideEmojiPicker})); + + // There is no way to disable animations, and they are really laggy, because there are so many + // emojis. The best alternative is to set it to 1ms so it just "pops" in and out + return ( + + (emojiSearchInput.current = el)} + /> + + ); +}); EmojiPicker.propTypes = propTypes; - +EmojiPicker.displayName = 'EmojiPicker'; export default compose(withViewportOffsetTop, withWindowDimensions)(EmojiPicker); diff --git a/src/components/withViewportOffsetTop.js b/src/components/withViewportOffsetTop.js index e25e5db9c5fa..538071a948fa 100644 --- a/src/components/withViewportOffsetTop.js +++ b/src/components/withViewportOffsetTop.js @@ -53,7 +53,8 @@ export default function (WrappedComponent) { WithViewportOffsetTop.displayName = `WithViewportOffsetTop(${getComponentDisplayName(WrappedComponent)})`; WithViewportOffsetTop.propTypes = { - forwardedRef: PropTypes.oneOfType([PropTypes.func, PropTypes.shape({current: PropTypes.instanceOf(React.Component)})]), + // eslint-disable-next-line react/forbid-prop-types + forwardedRef: PropTypes.oneOfType([PropTypes.func, PropTypes.shape({current: PropTypes.any})]), }; WithViewportOffsetTop.defaultProps = { forwardedRef: undefined, diff --git a/src/libs/calculateAnchorPosition.js b/src/libs/calculateAnchorPosition.js new file mode 100644 index 000000000000..512f77612f52 --- /dev/null +++ b/src/libs/calculateAnchorPosition.js @@ -0,0 +1,14 @@ +/** + * Gets the x,y position of the passed in component for the purpose of anchoring another component to it. + * + * @param {Element} anchorComponent + * @return {Promise} + */ +export default function calculateAnchorPosition(anchorComponent) { + return new Promise((resolve) => { + if (!anchorComponent) { + return resolve({horizontal: 0, vertical: 0}); + } + anchorComponent.measureInWindow((x, y, width) => resolve({horizontal: x + width, vertical: y})); + }); +}