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}));
+ });
+}