From 4bb09167982a50ab1a14d739b5feb59391b796f6 Mon Sep 17 00:00:00 2001 From: Ashutosh Khanduala Date: Fri, 6 Oct 2023 19:37:06 +0530 Subject: [PATCH 01/45] refactor: rename index*.js to EmojiPickerMenu*.tsx Signed-off-by: Ashutosh Khanduala --- .../{index.native.js => EmojiPickerMenu.native.tsx} | 0 .../EmojiPicker/EmojiPickerMenu/{index.js => EmojiPickerMenu.tsx} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename src/components/EmojiPicker/EmojiPickerMenu/{index.native.js => EmojiPickerMenu.native.tsx} (100%) rename src/components/EmojiPicker/EmojiPickerMenu/{index.js => EmojiPickerMenu.tsx} (100%) diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.native.js b/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu.native.tsx similarity index 100% rename from src/components/EmojiPicker/EmojiPickerMenu/index.native.js rename to src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu.native.tsx diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.js b/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu.tsx similarity index 100% rename from src/components/EmojiPicker/EmojiPickerMenu/index.js rename to src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu.tsx From 0385879671f0075b1a02f1bf27012385d4a082d3 Mon Sep 17 00:00:00 2001 From: Ashutosh Khanduala Date: Fri, 6 Oct 2023 19:50:45 +0530 Subject: [PATCH 02/45] refactor: barrel import EmojiPickerMenu Signed-off-by: Ashutosh Khanduala --- src/components/EmojiPicker/EmojiPickerMenu/index.ts | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 src/components/EmojiPicker/EmojiPickerMenu/index.ts diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.ts b/src/components/EmojiPicker/EmojiPickerMenu/index.ts new file mode 100644 index 000000000000..bcc226a34f54 --- /dev/null +++ b/src/components/EmojiPicker/EmojiPickerMenu/index.ts @@ -0,0 +1,2 @@ +import EmojiPickerMenu from './EmojiPickerMenu'; +export default EmojiPickerMenu; From 5fcc5cc6ef88e606046e51602bec2f11f739157e Mon Sep 17 00:00:00 2001 From: Ashutosh Khanduala Date: Sat, 7 Oct 2023 12:50:06 +0530 Subject: [PATCH 03/45] progress #1: convert class to function + add function keyword to class methods Signed-off-by: Ashutosh Khanduala --- .../EmojiPickerMenu/EmojiPickerMenu copy.tsx | 573 ++++++++++++++++++ 1 file changed, 573 insertions(+) create mode 100755 src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx diff --git a/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx b/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx new file mode 100755 index 000000000000..2719d4dfab50 --- /dev/null +++ b/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx @@ -0,0 +1,573 @@ +import React, {Component} from 'react'; +import {View, FlatList} from 'react-native'; +import {withOnyx} from 'react-native-onyx'; +import PropTypes from 'prop-types'; +import _ from 'underscore'; +import lodashGet from 'lodash/get'; +import CONST from '../../../CONST'; +import ONYXKEYS from '../../../ONYXKEYS'; +import styles from '../../../styles/styles'; +import * as StyleUtils from '../../../styles/StyleUtils'; +import emojis from '../../../../assets/emojis'; +import EmojiPickerMenuItem from '../EmojiPickerMenuItem'; +import Text from '../../Text'; +import withWindowDimensions, {windowDimensionsPropTypes} from '../../withWindowDimensions'; +import withLocalize, {withLocalizePropTypes} from '../../withLocalize'; +import compose from '../../../libs/compose'; +import getOperatingSystem from '../../../libs/getOperatingSystem'; +import * as User from '../../../libs/actions/User'; +import EmojiSkinToneList from '../EmojiSkinToneList'; +import * as EmojiUtils from '../../../libs/EmojiUtils'; +import CategoryShortcutBar from '../CategoryShortcutBar'; +import TextInput from '../../TextInput'; +import isEnterWhileComposition from '../../../libs/KeyboardShortcut/isEnterWhileComposition'; +import canFocusInputOnScreenFocus from '../../../libs/canFocusInputOnScreenFocus'; + +const propTypes = { + /** Function to add the selected emoji to the main compose text input */ + onEmojiSelected: PropTypes.func.isRequired, + + /** The ref to the search input (may be null on small screen widths) */ + forwardedRef: PropTypes.func, + + /** Stores user's preferred skin tone */ + preferredSkinTone: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + + /** Stores user's frequently used emojis */ + // eslint-disable-next-line react/forbid-prop-types + frequentlyUsedEmojis: PropTypes.arrayOf(PropTypes.object), + + /** Props related to the dimensions of the window */ + ...windowDimensionsPropTypes, + + ...withLocalizePropTypes, +}; + +const defaultProps = { + forwardedRef: () => {}, + preferredSkinTone: CONST.EMOJI_DEFAULT_SKIN_TONE, + frequentlyUsedEmojis: [], +}; + +const EmojiPickerMenu = (props) => { + // Ref for the emoji search input + this.searchInput = undefined; + + // Ref for emoji FlatList + this.emojiList = undefined; + + // We want consistent auto focus behavior on input between native and mWeb so we have some auto focus management code that will + // prevent auto focus when open picker for mobile device + this.shouldFocusInputOnScreenFocus = canFocusInputOnScreenFocus(); + + this.filterEmojis = _.debounce(this.filterEmojis.bind(this), 300); + this.highlightAdjacentEmoji = this.highlightAdjacentEmoji.bind(this); + this.setupEventHandlers = this.setupEventHandlers.bind(this); + this.cleanupEventHandlers = this.cleanupEventHandlers.bind(this); + this.renderItem = this.renderItem.bind(this); + this.isMobileLandscape = this.isMobileLandscape.bind(this); + this.onSelectionChange = this.onSelectionChange.bind(this); + this.updatePreferredSkinTone = this.updatePreferredSkinTone.bind(this); + this.setFirstNonHeaderIndex = this.setFirstNonHeaderIndex.bind(this); + this.getItemLayout = this.getItemLayout.bind(this); + this.scrollToHeader = this.scrollToHeader.bind(this); + + this.firstNonHeaderIndex = 0; + + const {filteredEmojis, headerEmojis, headerRowIndices} = this.getEmojisAndHeaderRowIndices(); + this.emojis = filteredEmojis; + this.headerEmojis = headerEmojis; + this.headerRowIndices = headerRowIndices; + + this.state = { + filteredEmojis: this.emojis, + headerIndices: this.headerRowIndices, + highlightedIndex: -1, + arePointerEventsDisabled: false, + selection: { + start: 0, + end: 0, + }, + isFocused: false, + isUsingKeyboardMovement: false, + }; + + function componentDidMount() { + // This callback prop is used by the parent component using the constructor to + // get a ref to the inner textInput element e.g. if we do + // this.textInput = el} /> this will not + // return a ref to the component, but rather the HTML element by default + if (this.shouldFocusInputOnScreenFocus && this.props.forwardedRef && _.isFunction(this.props.forwardedRef)) { + this.props.forwardedRef(this.searchInput); + } + this.setupEventHandlers(); + this.setFirstNonHeaderIndex(this.emojis); + } + + function componentDidUpdate(prevProps) { + if (prevProps.frequentlyUsedEmojis === this.props.frequentlyUsedEmojis) { + return; + } + + const {filteredEmojis, headerEmojis, headerRowIndices} = this.getEmojisAndHeaderRowIndices(); + this.emojis = filteredEmojis; + this.headerEmojis = headerEmojis; + this.headerRowIndices = headerRowIndices; + this.setState({ + filteredEmojis: this.emojis, + headerIndices: this.headerRowIndices, + }); + } + + function componentWillUnmount() { + this.cleanupEventHandlers(); + } + + /** + * On text input selection change + * + * @param {Event} event + */ + function onSelectionChange(event) { + this.setState({selection: event.nativeEvent.selection}); + } + + /** + * Calculate the filtered + header emojis and header row indices + * @returns {Object} + */ + function getEmojisAndHeaderRowIndices() { + // If we're on Windows, don't display the flag emojis (the last category), + // since Windows doesn't support them + const flagHeaderIndex = _.findIndex(emojis, (emoji) => emoji.header && emoji.code === 'flags'); + const filteredEmojis = + getOperatingSystem() === CONST.OS.WINDOWS + ? EmojiUtils.mergeEmojisWithFrequentlyUsedEmojis(emojis.slice(0, flagHeaderIndex)) + : EmojiUtils.mergeEmojisWithFrequentlyUsedEmojis(emojis); + + // Get the header emojis along with the code, index and icon. + // index is the actual header index starting at the first emoji and counting each one + const headerEmojis = EmojiUtils.getHeaderEmojis(filteredEmojis); + + // This is the indices of each header's Row + // The positions are static, and are calculated as index/numColumns (8 in our case) + // This is because each row of 8 emojis counts as one index to the flatlist + const headerRowIndices = _.map(headerEmojis, (headerEmoji) => Math.floor(headerEmoji.index / CONST.EMOJI_NUM_PER_ROW)); + + return {filteredEmojis, headerEmojis, headerRowIndices}; + } + + /** + * Find and store index of the first emoji item + * @param {Array} filteredEmojis + */ + function setFirstNonHeaderIndex(filteredEmojis) { + this.firstNonHeaderIndex = _.findIndex(filteredEmojis, (item) => !item.spacer && !item.header); + } + + /** + * Setup and attach keypress/mouse handlers for highlight navigation. + */ + function setupEventHandlers() { + if (!document) { + return; + } + + this.keyDownHandler = (keyBoardEvent) => { + if (keyBoardEvent.key.startsWith('Arrow')) { + if (!this.state.isFocused || keyBoardEvent.key === 'ArrowUp' || keyBoardEvent.key === 'ArrowDown') { + keyBoardEvent.preventDefault(); + } + + // Move the highlight when arrow keys are pressed + this.highlightAdjacentEmoji(keyBoardEvent.key); + return; + } + + // Select the currently highlighted emoji if enter is pressed + if (!isEnterWhileComposition(keyBoardEvent) && keyBoardEvent.key === CONST.KEYBOARD_SHORTCUTS.ENTER.shortcutKey && this.state.highlightedIndex !== -1) { + const item = this.state.filteredEmojis[this.state.highlightedIndex]; + if (!item) { + return; + } + const emoji = lodashGet(item, ['types', this.props.preferredSkinTone], item.code); + this.addToFrequentAndSelectEmoji(emoji, item); + return; + } + + // Enable keyboard movement if tab or enter is pressed or if shift is pressed while the input + // is not focused, so that the navigation and tab cycling can be done using the keyboard without + // interfering with the input behaviour. + if (keyBoardEvent.key === 'Tab' || keyBoardEvent.key === 'Enter' || (keyBoardEvent.key === 'Shift' && this.searchInput && !this.searchInput.isFocused())) { + this.setState({isUsingKeyboardMovement: true}); + return; + } + + // We allow typing in the search box if any key is pressed apart from Arrow keys. + if (this.searchInput && !this.searchInput.isFocused()) { + this.setState({selectTextOnFocus: false}); + this.searchInput.focus(); + + // Re-enable selection on the searchInput + this.setState({selectTextOnFocus: true}); + } + }; + + // Keyboard events are not bubbling on TextInput in RN-Web, Bubbling was needed for this event to trigger + // event handler attached to document root. To fix this, trigger event handler in Capture phase. + document.addEventListener('keydown', this.keyDownHandler, true); + + // Re-enable pointer events and hovering over EmojiPickerItems when the mouse moves + this.mouseMoveHandler = () => { + if (!this.state.arePointerEventsDisabled) { + return; + } + + this.setState({arePointerEventsDisabled: false}); + }; + document.addEventListener('mousemove', this.mouseMoveHandler); + } + + /** + * This function will be used with FlatList getItemLayout property for optimization purpose that allows skipping + * the measurement of dynamic content if we know the size (height or width) of items ahead of time. + * Generate and return an object with properties length(height of each individual row), + * offset(distance of the current row from the top of the FlatList), index(current row index) + * + * @param {*} data FlatList item + * @param {Number} index row index + * @returns {Object} + */ + function getItemLayout(data, index) { + return {length: CONST.EMOJI_PICKER_ITEM_HEIGHT, offset: CONST.EMOJI_PICKER_ITEM_HEIGHT * index, index}; + } + + /** + * Cleanup all mouse/keydown event listeners that we've set up + */ + function cleanupEventHandlers() { + if (!document) { + return; + } + + document.removeEventListener('keydown', this.keyDownHandler, true); + document.removeEventListener('mousemove', this.mouseMoveHandler); + } + + /** + * @param {String} emoji + * @param {Object} emojiObject + */ + function addToFrequentAndSelectEmoji(emoji, emojiObject) { + const frequentEmojiList = EmojiUtils.getFrequentlyUsedEmojis(emojiObject); + User.updateFrequentlyUsedEmojis(frequentEmojiList); + this.props.onEmojiSelected(emoji, emojiObject); + } + + /** + * Focuses the search Input and has the text selected + */ + function focusInputWithTextSelect() { + if (!this.searchInput) { + return; + } + + this.setState({selectTextOnFocus: true}); + this.searchInput.focus(); + } + + /** + * Highlights emojis adjacent to the currently highlighted emoji depending on the arrowKey + * @param {String} arrowKey + */ + function highlightAdjacentEmoji(arrowKey) { + if (this.state.filteredEmojis.length === 0) { + return; + } + + // Arrow Down and Arrow Right enable arrow navigation when search is focused + if (this.searchInput && this.searchInput.isFocused()) { + if (arrowKey !== 'ArrowDown' && arrowKey !== 'ArrowRight') { + return; + } + + if (arrowKey === 'ArrowRight' && !(this.searchInput.value.length === this.state.selection.start && this.state.selection.start === this.state.selection.end)) { + return; + } + + // Blur the input, change the highlight type to keyboard, and disable pointer events + this.searchInput.blur(); + this.setState({isUsingKeyboardMovement: true, arePointerEventsDisabled: true}); + + // We only want to hightlight the Emoji if none was highlighted already + // If we already have a highlighted Emoji, lets just skip the first navigation + if (this.state.highlightedIndex !== -1) { + return; + } + } + + // If nothing is highlighted and an arrow key is pressed + // select the first emoji, apply keyboard movement styles, and disable pointer events + if (this.state.highlightedIndex === -1) { + this.setState({highlightedIndex: this.firstNonHeaderIndex, isUsingKeyboardMovement: true, arePointerEventsDisabled: true}); + return; + } + + let newIndex = this.state.highlightedIndex; + const move = (steps, boundsCheck, onBoundReached = () => {}) => { + if (boundsCheck()) { + onBoundReached(); + return; + } + + // Move in the prescribed direction until we reach an element that isn't a header + const isHeader = (e) => e.header || e.spacer; + do { + newIndex += steps; + if (newIndex < 0) { + break; + } + } while (isHeader(this.state.filteredEmojis[newIndex])); + }; + + switch (arrowKey) { + case 'ArrowDown': + move(CONST.EMOJI_NUM_PER_ROW, () => this.state.highlightedIndex + CONST.EMOJI_NUM_PER_ROW > this.state.filteredEmojis.length - 1); + break; + case 'ArrowLeft': + move( + -1, + () => this.state.highlightedIndex - 1 < this.firstNonHeaderIndex, + () => { + // Reaching start of the list, arrow left set the focus to searchInput. + this.focusInputWithTextSelect(); + newIndex = -1; + }, + ); + break; + case 'ArrowRight': + move(1, () => this.state.highlightedIndex + 1 > this.state.filteredEmojis.length - 1); + break; + case 'ArrowUp': + move( + -CONST.EMOJI_NUM_PER_ROW, + () => this.state.highlightedIndex - CONST.EMOJI_NUM_PER_ROW < this.firstNonHeaderIndex, + () => { + // Reaching start of the list, arrow up set the focus to searchInput. + this.focusInputWithTextSelect(); + newIndex = -1; + }, + ); + break; + default: + break; + } + + // Actually highlight the new emoji, apply keyboard movement styles, and disable pointer events + if (newIndex !== this.state.highlightedIndex) { + this.setState({highlightedIndex: newIndex, isUsingKeyboardMovement: true, arePointerEventsDisabled: true}); + } + } + + function scrollToHeader(headerIndex) { + const calculatedOffset = Math.floor(headerIndex / CONST.EMOJI_NUM_PER_ROW) * CONST.EMOJI_PICKER_HEADER_HEIGHT; + this.emojiList.flashScrollIndicators(); + this.emojiList.scrollToOffset({offset: calculatedOffset, animated: true}); + } + + /** + * Filter the entire list of emojis to only emojis that have the search term in their keywords + * + * @param {String} searchTerm + */ + function filterEmojis(searchTerm) { + const normalizedSearchTerm = searchTerm.toLowerCase().trim().replaceAll(':', ''); + if (this.emojiList) { + this.emojiList.scrollToOffset({offset: 0, animated: false}); + } + if (normalizedSearchTerm === '') { + // There are no headers when searching, so we need to re-make them sticky when there is no search term + this.setState({ + filteredEmojis: this.emojis, + headerIndices: this.headerRowIndices, + highlightedIndex: -1, + }); + this.setFirstNonHeaderIndex(this.emojis); + return; + } + const newFilteredEmojiList = EmojiUtils.suggestEmojis(`:${normalizedSearchTerm}`, this.props.preferredLocale, this.emojis.length); + + // Remove sticky header indices. There are no headers while searching and we don't want to make emojis sticky + this.setState({filteredEmojis: newFilteredEmojiList, headerIndices: [], highlightedIndex: 0}); + this.setFirstNonHeaderIndex(newFilteredEmojiList); + } + + /** + * Check if its a landscape mode of mobile device + * + * @returns {Boolean} + */ + function isMobileLandscape() { + return this.props.isSmallScreenWidth && this.props.windowWidth >= this.props.windowHeight; + } + + /** + * @param {Number} skinTone + */ + function updatePreferredSkinTone(skinTone) { + if (this.props.preferredSkinTone === skinTone) { + return; + } + + User.updatePreferredSkinTone(skinTone); + } + + /** + * Return a unique key for each emoji item + * + * @param {Object} item + * @param {Number} index + * @returns {String} + */ + function keyExtractor(item, index) { + return `emoji_picker_${item.code}_${index}`; + } + + /** + * Given an emoji item object, render a component based on its type. + * Items with the code "SPACER" return nothing and are used to fill rows up to 8 + * so that the sticky headers function properly. + * + * @param {Object} item + * @param {Number} index + * @returns {*} + */ + function renderItem({item, index}) { + const {code, header, types} = item; + if (item.spacer) { + return null; + } + + if (header) { + return ( + + {this.props.translate(`emojiPicker.headers.${code}`)} + + ); + } + + const emojiCode = types && types[this.props.preferredSkinTone] ? types[this.props.preferredSkinTone] : code; + + const isEmojiFocused = index === this.state.highlightedIndex && this.state.isUsingKeyboardMovement; + + return ( + this.addToFrequentAndSelectEmoji(emoji, item)} + onHoverIn={() => this.setState({highlightedIndex: index, isUsingKeyboardMovement: false})} + onHoverOut={() => { + if (this.state.arePointerEventsDisabled) { + return; + } + this.setState({highlightedIndex: -1}); + }} + emoji={emojiCode} + onFocus={() => this.setState({highlightedIndex: index})} + onBlur={() => + this.setState((prevState) => ({ + // Only clear the highlighted index if the highlighted index is the same, + // meaning that the focus changed to an element that is not an emoji item. + highlightedIndex: prevState.highlightedIndex === index ? -1 : prevState.highlightedIndex, + })) + } + isFocused={isEmojiFocused} + isHighlighted={index === this.state.highlightedIndex} + isUsingKeyboardMovement={this.state.isUsingKeyboardMovement} + /> + ); + } + + const isFiltered = this.emojis.length !== this.state.filteredEmojis.length; + const listStyle = StyleUtils.getEmojiPickerListHeight(isFiltered, this.props.windowHeight); + const height = !listStyle.maxHeight || listStyle.height < listStyle.maxHeight ? listStyle.height : listStyle.maxHeight; + const overflowLimit = Math.floor(height / CONST.EMOJI_PICKER_ITEM_HEIGHT) * 8; + return ( + + + (this.searchInput = el)} + autoFocus={this.shouldFocusInputOnScreenFocus} + selectTextOnFocus={this.state.selectTextOnFocus} + onSelectionChange={this.onSelectionChange} + onFocus={() => this.setState({isFocused: true, highlightedIndex: -1, isUsingKeyboardMovement: false})} + onBlur={() => this.setState({isFocused: false})} + autoCorrect={false} + blurOnSubmit={this.state.filteredEmojis.length > 0} + /> + + {!isFiltered && ( + + )} + (this.emojiList = el)} + data={this.state.filteredEmojis} + renderItem={this.renderItem} + keyExtractor={this.keyExtractor} + numColumns={CONST.EMOJI_NUM_PER_ROW} + style={[ + listStyle, + // This prevents elastic scrolling when scroll reaches the start or end + {overscrollBehaviorY: 'contain'}, + // Set overflow to hidden to prevent elastic scrolling when there are not enough contents to scroll in FlatList + {overflowY: this.state.filteredEmojis.length > overflowLimit ? 'auto' : 'hidden'}, + // Set scrollPaddingTop to consider sticky headers while scrolling + {scrollPaddingTop: isFiltered ? 0 : CONST.EMOJI_PICKER_ITEM_HEIGHT}, + ]} + extraData={[this.state.filteredEmojis, this.state.highlightedIndex, this.props.preferredSkinTone]} + stickyHeaderIndices={this.state.headerIndices} + getItemLayout={this.getItemLayout} + contentContainerStyle={styles.flexGrow1} + ListEmptyComponent={{this.props.translate('common.noResultsFound')}} + /> + + + ); +}; + +EmojiPickerMenu.propTypes = propTypes; +EmojiPickerMenu.defaultProps = defaultProps; + +export default compose( + withWindowDimensions, + withLocalize, + withOnyx({ + preferredSkinTone: { + key: ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE, + }, + frequentlyUsedEmojis: { + key: ONYXKEYS.FREQUENTLY_USED_EMOJIS, + }, + }), +)( + React.forwardRef((props, ref) => ( + + )), +); From 5716e80878c15a1b1761faccb95d9a969756861a Mon Sep 17 00:00:00 2001 From: Ashutosh Khanduala Date: Sat, 7 Oct 2023 13:08:59 +0530 Subject: [PATCH 04/45] progress #2: props Signed-off-by: Ashutosh Khanduala --- .../EmojiPickerMenu/EmojiPickerMenu copy.tsx | 36 ++++++++++--------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx b/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx index 2719d4dfab50..ddcd2f9aaaaf 100755 --- a/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx +++ b/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx @@ -50,6 +50,8 @@ const defaultProps = { }; const EmojiPickerMenu = (props) => { + const {forwardedRef, frequentlyUsedEmojis, preferredSkinTone, onEmojiSelected, preferredLocale, isSmallScreenWidth, windowWidth, windowHeight, translate} = props; + // Ref for the emoji search input this.searchInput = undefined; @@ -97,15 +99,15 @@ const EmojiPickerMenu = (props) => { // get a ref to the inner textInput element e.g. if we do // this.textInput = el} /> this will not // return a ref to the component, but rather the HTML element by default - if (this.shouldFocusInputOnScreenFocus && this.props.forwardedRef && _.isFunction(this.props.forwardedRef)) { - this.props.forwardedRef(this.searchInput); + if (this.shouldFocusInputOnScreenFocus && forwardedRef && _.isFunction(forwardedRef)) { + forwardedRef(this.searchInput); } this.setupEventHandlers(); this.setFirstNonHeaderIndex(this.emojis); } function componentDidUpdate(prevProps) { - if (prevProps.frequentlyUsedEmojis === this.props.frequentlyUsedEmojis) { + if (prevProps.frequentlyUsedEmojis === frequentlyUsedEmojis) { return; } @@ -190,7 +192,7 @@ const EmojiPickerMenu = (props) => { if (!item) { return; } - const emoji = lodashGet(item, ['types', this.props.preferredSkinTone], item.code); + const emoji = lodashGet(item, ['types', preferredSkinTone], item.code); this.addToFrequentAndSelectEmoji(emoji, item); return; } @@ -261,7 +263,7 @@ const EmojiPickerMenu = (props) => { function addToFrequentAndSelectEmoji(emoji, emojiObject) { const frequentEmojiList = EmojiUtils.getFrequentlyUsedEmojis(emojiObject); User.updateFrequentlyUsedEmojis(frequentEmojiList); - this.props.onEmojiSelected(emoji, emojiObject); + onEmojiSelected(emoji, emojiObject); } /** @@ -395,7 +397,7 @@ const EmojiPickerMenu = (props) => { this.setFirstNonHeaderIndex(this.emojis); return; } - const newFilteredEmojiList = EmojiUtils.suggestEmojis(`:${normalizedSearchTerm}`, this.props.preferredLocale, this.emojis.length); + const newFilteredEmojiList = EmojiUtils.suggestEmojis(`:${normalizedSearchTerm}`, preferredLocale, this.emojis.length); // Remove sticky header indices. There are no headers while searching and we don't want to make emojis sticky this.setState({filteredEmojis: newFilteredEmojiList, headerIndices: [], highlightedIndex: 0}); @@ -408,14 +410,14 @@ const EmojiPickerMenu = (props) => { * @returns {Boolean} */ function isMobileLandscape() { - return this.props.isSmallScreenWidth && this.props.windowWidth >= this.props.windowHeight; + return isSmallScreenWidth && windowWidth >= windowHeight; } /** * @param {Number} skinTone */ function updatePreferredSkinTone(skinTone) { - if (this.props.preferredSkinTone === skinTone) { + if (preferredSkinTone === skinTone) { return; } @@ -451,12 +453,12 @@ const EmojiPickerMenu = (props) => { if (header) { return ( - {this.props.translate(`emojiPicker.headers.${code}`)} + {translate(`emojiPicker.headers.${code}`)} ); } - const emojiCode = types && types[this.props.preferredSkinTone] ? types[this.props.preferredSkinTone] : code; + const emojiCode = types && types[preferredSkinTone] ? types[preferredSkinTone] : code; const isEmojiFocused = index === this.state.highlightedIndex && this.state.isUsingKeyboardMovement; @@ -487,19 +489,19 @@ const EmojiPickerMenu = (props) => { } const isFiltered = this.emojis.length !== this.state.filteredEmojis.length; - const listStyle = StyleUtils.getEmojiPickerListHeight(isFiltered, this.props.windowHeight); + const listStyle = StyleUtils.getEmojiPickerListHeight(isFiltered, windowHeight); const height = !listStyle.maxHeight || listStyle.height < listStyle.maxHeight ? listStyle.height : listStyle.maxHeight; const overflowLimit = Math.floor(height / CONST.EMOJI_PICKER_ITEM_HEIGHT) * 8; return ( { // Set scrollPaddingTop to consider sticky headers while scrolling {scrollPaddingTop: isFiltered ? 0 : CONST.EMOJI_PICKER_ITEM_HEIGHT}, ]} - extraData={[this.state.filteredEmojis, this.state.highlightedIndex, this.props.preferredSkinTone]} + extraData={[this.state.filteredEmojis, this.state.highlightedIndex, preferredSkinTone]} stickyHeaderIndices={this.state.headerIndices} getItemLayout={this.getItemLayout} contentContainerStyle={styles.flexGrow1} - ListEmptyComponent={{this.props.translate('common.noResultsFound')}} + ListEmptyComponent={{translate('common.noResultsFound')}} /> ); From 52eb14c3a786a6efc56b59188d3c0009a766c1e5 Mon Sep 17 00:00:00 2001 From: Ashutosh Khanduala Date: Sat, 7 Oct 2023 13:17:49 +0530 Subject: [PATCH 05/45] progress #3: searchInputRef -> searchInputRef Signed-off-by: Ashutosh Khanduala --- .../EmojiPickerMenu/EmojiPickerMenu copy.tsx | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx b/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx index ddcd2f9aaaaf..1b57a53489d6 100755 --- a/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx +++ b/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx @@ -1,4 +1,4 @@ -import React, {Component} from 'react'; +import React, {Component, useRef} from 'react'; import {View, FlatList} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import PropTypes from 'prop-types'; @@ -22,6 +22,7 @@ import CategoryShortcutBar from '../CategoryShortcutBar'; import TextInput from '../../TextInput'; import isEnterWhileComposition from '../../../libs/KeyboardShortcut/isEnterWhileComposition'; import canFocusInputOnScreenFocus from '../../../libs/canFocusInputOnScreenFocus'; +import {TextInput as RNTextInput} from 'react-native'; const propTypes = { /** Function to add the selected emoji to the main compose text input */ @@ -53,7 +54,7 @@ const EmojiPickerMenu = (props) => { const {forwardedRef, frequentlyUsedEmojis, preferredSkinTone, onEmojiSelected, preferredLocale, isSmallScreenWidth, windowWidth, windowHeight, translate} = props; // Ref for the emoji search input - this.searchInput = undefined; + const searchInputRef = useRef(null); // TODO: is RNTextInput correct? // Ref for emoji FlatList this.emojiList = undefined; @@ -100,7 +101,7 @@ const EmojiPickerMenu = (props) => { // this.textInput = el} /> this will not // return a ref to the component, but rather the HTML element by default if (this.shouldFocusInputOnScreenFocus && forwardedRef && _.isFunction(forwardedRef)) { - forwardedRef(this.searchInput); + forwardedRef(searchInputRef.current); } this.setupEventHandlers(); this.setFirstNonHeaderIndex(this.emojis); @@ -200,15 +201,15 @@ const EmojiPickerMenu = (props) => { // Enable keyboard movement if tab or enter is pressed or if shift is pressed while the input // is not focused, so that the navigation and tab cycling can be done using the keyboard without // interfering with the input behaviour. - if (keyBoardEvent.key === 'Tab' || keyBoardEvent.key === 'Enter' || (keyBoardEvent.key === 'Shift' && this.searchInput && !this.searchInput.isFocused())) { + if (keyBoardEvent.key === 'Tab' || keyBoardEvent.key === 'Enter' || (keyBoardEvent.key === 'Shift' && !searchInputRef.current?.isFocused())) { this.setState({isUsingKeyboardMovement: true}); return; } // We allow typing in the search box if any key is pressed apart from Arrow keys. - if (this.searchInput && !this.searchInput.isFocused()) { + if (!searchInputRef.current?.isFocused()) { this.setState({selectTextOnFocus: false}); - this.searchInput.focus(); + searchInputRef.current.focus(); // Re-enable selection on the searchInput this.setState({selectTextOnFocus: true}); @@ -270,12 +271,12 @@ const EmojiPickerMenu = (props) => { * Focuses the search Input and has the text selected */ function focusInputWithTextSelect() { - if (!this.searchInput) { + if (!searchInputRef.current) { return; } this.setState({selectTextOnFocus: true}); - this.searchInput.focus(); + searchInputRef.current.focus(); } /** @@ -288,17 +289,17 @@ const EmojiPickerMenu = (props) => { } // Arrow Down and Arrow Right enable arrow navigation when search is focused - if (this.searchInput && this.searchInput.isFocused()) { + if (searchInputRef.current?.isFocused()) { if (arrowKey !== 'ArrowDown' && arrowKey !== 'ArrowRight') { return; } - if (arrowKey === 'ArrowRight' && !(this.searchInput.value.length === this.state.selection.start && this.state.selection.start === this.state.selection.end)) { + if (arrowKey === 'ArrowRight' && !(searchInputRef.current.value.length === this.state.selection.start && this.state.selection.start === this.state.selection.end)) { return; } // Blur the input, change the highlight type to keyboard, and disable pointer events - this.searchInput.blur(); + searchInputRef.current.blur(); this.setState({isUsingKeyboardMovement: true, arePointerEventsDisabled: true}); // We only want to hightlight the Emoji if none was highlighted already @@ -505,7 +506,7 @@ const EmojiPickerMenu = (props) => { accessibilityRole={CONST.ACCESSIBILITY_ROLE.TEXT} onChangeText={this.filterEmojis} defaultValue="" - ref={(el) => (this.searchInput = el)} + ref={searchInputRef} autoFocus={this.shouldFocusInputOnScreenFocus} selectTextOnFocus={this.state.selectTextOnFocus} onSelectionChange={this.onSelectionChange} From 7959d01cf16f8fad81226ed39e0d2ea90ba0c9f9 Mon Sep 17 00:00:00 2001 From: Ashutosh Khanduala Date: Sat, 7 Oct 2023 13:40:34 +0530 Subject: [PATCH 06/45] progress #4: emojiList -> emojiListRef Signed-off-by: Ashutosh Khanduala --- .../EmojiPickerMenu/EmojiPickerMenu copy.tsx | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx b/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx index 1b57a53489d6..6f8665e63409 100755 --- a/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx +++ b/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx @@ -57,7 +57,7 @@ const EmojiPickerMenu = (props) => { const searchInputRef = useRef(null); // TODO: is RNTextInput correct? // Ref for emoji FlatList - this.emojiList = undefined; + const emojiListRef = useRef(null); // We want consistent auto focus behavior on input between native and mWeb so we have some auto focus management code that will // prevent auto focus when open picker for mobile device @@ -374,8 +374,8 @@ const EmojiPickerMenu = (props) => { function scrollToHeader(headerIndex) { const calculatedOffset = Math.floor(headerIndex / CONST.EMOJI_NUM_PER_ROW) * CONST.EMOJI_PICKER_HEADER_HEIGHT; - this.emojiList.flashScrollIndicators(); - this.emojiList.scrollToOffset({offset: calculatedOffset, animated: true}); + emojiListRef.current?.flashScrollIndicators(); + emojiListRef.current?.scrollToOffset({offset: calculatedOffset, animated: true}); } /** @@ -385,9 +385,7 @@ const EmojiPickerMenu = (props) => { */ function filterEmojis(searchTerm) { const normalizedSearchTerm = searchTerm.toLowerCase().trim().replaceAll(':', ''); - if (this.emojiList) { - this.emojiList.scrollToOffset({offset: 0, animated: false}); - } + emojiListRef.current?.scrollToOffset({offset: 0, animated: false}); if (normalizedSearchTerm === '') { // There are no headers when searching, so we need to re-make them sticky when there is no search term this.setState({ @@ -523,7 +521,7 @@ const EmojiPickerMenu = (props) => { /> )} (this.emojiList = el)} + ref={emojiListRef} data={this.state.filteredEmojis} renderItem={this.renderItem} keyExtractor={this.keyExtractor} From 9354019096e34242dc6d845b121377f97572822e Mon Sep 17 00:00:00 2001 From: Ashutosh Khanduala Date: Sat, 7 Oct 2023 13:57:12 +0530 Subject: [PATCH 07/45] progress #5: filterEmojis() with useCallback + shouldFocusInputOnScreenFocus Signed-off-by: Ashutosh Khanduala --- .../EmojiPickerMenu/EmojiPickerMenu copy.tsx | 50 ++++++++++--------- 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx b/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx index 6f8665e63409..f493e9c0bcd4 100755 --- a/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx +++ b/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx @@ -1,4 +1,4 @@ -import React, {Component, useRef} from 'react'; +import React, {Component, useCallback, useRef} from 'react'; import {View, FlatList} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import PropTypes from 'prop-types'; @@ -61,9 +61,8 @@ const EmojiPickerMenu = (props) => { // We want consistent auto focus behavior on input between native and mWeb so we have some auto focus management code that will // prevent auto focus when open picker for mobile device - this.shouldFocusInputOnScreenFocus = canFocusInputOnScreenFocus(); + const shouldFocusInputOnScreenFocus = canFocusInputOnScreenFocus(); - this.filterEmojis = _.debounce(this.filterEmojis.bind(this), 300); this.highlightAdjacentEmoji = this.highlightAdjacentEmoji.bind(this); this.setupEventHandlers = this.setupEventHandlers.bind(this); this.cleanupEventHandlers = this.cleanupEventHandlers.bind(this); @@ -100,7 +99,7 @@ const EmojiPickerMenu = (props) => { // get a ref to the inner textInput element e.g. if we do // this.textInput = el} /> this will not // return a ref to the component, but rather the HTML element by default - if (this.shouldFocusInputOnScreenFocus && forwardedRef && _.isFunction(forwardedRef)) { + if (shouldFocusInputOnScreenFocus && forwardedRef && _.isFunction(forwardedRef)) { forwardedRef(searchInputRef.current); } this.setupEventHandlers(); @@ -383,25 +382,28 @@ const EmojiPickerMenu = (props) => { * * @param {String} searchTerm */ - function filterEmojis(searchTerm) { - const normalizedSearchTerm = searchTerm.toLowerCase().trim().replaceAll(':', ''); - emojiListRef.current?.scrollToOffset({offset: 0, animated: false}); - if (normalizedSearchTerm === '') { - // There are no headers when searching, so we need to re-make them sticky when there is no search term - this.setState({ - filteredEmojis: this.emojis, - headerIndices: this.headerRowIndices, - highlightedIndex: -1, - }); - this.setFirstNonHeaderIndex(this.emojis); - return; - } - const newFilteredEmojiList = EmojiUtils.suggestEmojis(`:${normalizedSearchTerm}`, preferredLocale, this.emojis.length); + const filterEmojis = useCallback( + _.debounce((searchTerm: string) => { + const normalizedSearchTerm = searchTerm.toLowerCase().trim().replaceAll(':', ''); + emojiListRef.current?.scrollToOffset({offset: 0, animated: false}); + if (normalizedSearchTerm === '') { + // There are no headers when searching, so we need to re-make them sticky when there is no search term + this.setState({ + filteredEmojis: this.emojis, + headerIndices: this.headerRowIndices, + highlightedIndex: -1, + }); + this.setFirstNonHeaderIndex(this.emojis); + return; + } + const newFilteredEmojiList = EmojiUtils.suggestEmojis(`:${normalizedSearchTerm}`, preferredLocale, this.emojis.length); - // Remove sticky header indices. There are no headers while searching and we don't want to make emojis sticky - this.setState({filteredEmojis: newFilteredEmojiList, headerIndices: [], highlightedIndex: 0}); - this.setFirstNonHeaderIndex(newFilteredEmojiList); - } + // Remove sticky header indices. There are no headers while searching and we don't want to make emojis sticky + this.setState({filteredEmojis: newFilteredEmojiList, headerIndices: [], highlightedIndex: 0}); + this.setFirstNonHeaderIndex(newFilteredEmojiList); + }, 300), + [], + ); /** * Check if its a landscape mode of mobile device @@ -502,10 +504,10 @@ const EmojiPickerMenu = (props) => { label={translate('common.search')} accessibilityLabel={translate('common.search')} accessibilityRole={CONST.ACCESSIBILITY_ROLE.TEXT} - onChangeText={this.filterEmojis} + onChangeText={filterEmojis} defaultValue="" ref={searchInputRef} - autoFocus={this.shouldFocusInputOnScreenFocus} + autoFocus={shouldFocusInputOnScreenFocus} selectTextOnFocus={this.state.selectTextOnFocus} onSelectionChange={this.onSelectionChange} onFocus={() => this.setState({isFocused: true, highlightedIndex: -1, isUsingKeyboardMovement: false})} From 65fdee9b8482052077213ed357930caccd3e308e Mon Sep 17 00:00:00 2001 From: Ashutosh Khanduala Date: Sat, 7 Oct 2023 14:07:08 +0530 Subject: [PATCH 08/45] progress #6: highlightAdjacentEmoji() + setupEventHandlers() + cleanupEventHandlers() Signed-off-by: Ashutosh Khanduala --- .../EmojiPickerMenu/EmojiPickerMenu copy.tsx | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx b/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx index f493e9c0bcd4..60a2a00692b4 100755 --- a/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx +++ b/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx @@ -63,9 +63,6 @@ const EmojiPickerMenu = (props) => { // prevent auto focus when open picker for mobile device const shouldFocusInputOnScreenFocus = canFocusInputOnScreenFocus(); - this.highlightAdjacentEmoji = this.highlightAdjacentEmoji.bind(this); - this.setupEventHandlers = this.setupEventHandlers.bind(this); - this.cleanupEventHandlers = this.cleanupEventHandlers.bind(this); this.renderItem = this.renderItem.bind(this); this.isMobileLandscape = this.isMobileLandscape.bind(this); this.onSelectionChange = this.onSelectionChange.bind(this); @@ -102,7 +99,7 @@ const EmojiPickerMenu = (props) => { if (shouldFocusInputOnScreenFocus && forwardedRef && _.isFunction(forwardedRef)) { forwardedRef(searchInputRef.current); } - this.setupEventHandlers(); + setupEventHandlers(); this.setFirstNonHeaderIndex(this.emojis); } @@ -122,7 +119,7 @@ const EmojiPickerMenu = (props) => { } function componentWillUnmount() { - this.cleanupEventHandlers(); + cleanupEventHandlers(); } /** @@ -182,7 +179,7 @@ const EmojiPickerMenu = (props) => { } // Move the highlight when arrow keys are pressed - this.highlightAdjacentEmoji(keyBoardEvent.key); + highlightAdjacentEmoji(keyBoardEvent.key); return; } @@ -248,12 +245,8 @@ const EmojiPickerMenu = (props) => { * Cleanup all mouse/keydown event listeners that we've set up */ function cleanupEventHandlers() { - if (!document) { - return; - } - - document.removeEventListener('keydown', this.keyDownHandler, true); - document.removeEventListener('mousemove', this.mouseMoveHandler); + document?.removeEventListener('keydown', this.keyDownHandler, true); + document?.removeEventListener('mousemove', this.mouseMoveHandler); } /** @@ -282,7 +275,7 @@ const EmojiPickerMenu = (props) => { * Highlights emojis adjacent to the currently highlighted emoji depending on the arrowKey * @param {String} arrowKey */ - function highlightAdjacentEmoji(arrowKey) { + function highlightAdjacentEmoji(arrowKey: KeyboardEvent['key']) { if (this.state.filteredEmojis.length === 0) { return; } From 35726f6d8be47fe34c9c8c481711ffb01cc61975 Mon Sep 17 00:00:00 2001 From: Ashutosh Khanduala Date: Sat, 7 Oct 2023 14:14:17 +0530 Subject: [PATCH 09/45] progress #7: renderItem() +onSelectionChange() Signed-off-by: Ashutosh Khanduala --- .../EmojiPickerMenu/EmojiPickerMenu copy.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx b/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx index 60a2a00692b4..afbdec31f42d 100755 --- a/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx +++ b/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx @@ -1,5 +1,5 @@ import React, {Component, useCallback, useRef} from 'react'; -import {View, FlatList} from 'react-native'; +import {View, FlatList, TextInputSelectionChangeEventData} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import PropTypes from 'prop-types'; import _ from 'underscore'; @@ -23,6 +23,7 @@ import TextInput from '../../TextInput'; import isEnterWhileComposition from '../../../libs/KeyboardShortcut/isEnterWhileComposition'; import canFocusInputOnScreenFocus from '../../../libs/canFocusInputOnScreenFocus'; import {TextInput as RNTextInput} from 'react-native'; +import {NativeSyntheticEvent} from 'react-native'; const propTypes = { /** Function to add the selected emoji to the main compose text input */ @@ -63,9 +64,6 @@ const EmojiPickerMenu = (props) => { // prevent auto focus when open picker for mobile device const shouldFocusInputOnScreenFocus = canFocusInputOnScreenFocus(); - this.renderItem = this.renderItem.bind(this); - this.isMobileLandscape = this.isMobileLandscape.bind(this); - this.onSelectionChange = this.onSelectionChange.bind(this); this.updatePreferredSkinTone = this.updatePreferredSkinTone.bind(this); this.setFirstNonHeaderIndex = this.setFirstNonHeaderIndex.bind(this); this.getItemLayout = this.getItemLayout.bind(this); @@ -127,7 +125,7 @@ const EmojiPickerMenu = (props) => { * * @param {Event} event */ - function onSelectionChange(event) { + function onSelectionChange(event: NativeSyntheticEvent) { this.setState({selection: event.nativeEvent.selection}); } @@ -404,6 +402,7 @@ const EmojiPickerMenu = (props) => { * @returns {Boolean} */ function isMobileLandscape() { + // TODO: This isnt used anywhere return isSmallScreenWidth && windowWidth >= windowHeight; } @@ -438,7 +437,8 @@ const EmojiPickerMenu = (props) => { * @param {Number} index * @returns {*} */ - function renderItem({item, index}) { + function renderItem({item, index}: {item: {}; index: number}) { + // TODO: TS types for item const {code, header, types} = item; if (item.spacer) { return null; @@ -502,7 +502,7 @@ const EmojiPickerMenu = (props) => { ref={searchInputRef} autoFocus={shouldFocusInputOnScreenFocus} selectTextOnFocus={this.state.selectTextOnFocus} - onSelectionChange={this.onSelectionChange} + onSelectionChange={onSelectionChange} onFocus={() => this.setState({isFocused: true, highlightedIndex: -1, isUsingKeyboardMovement: false})} onBlur={() => this.setState({isFocused: false})} autoCorrect={false} @@ -518,7 +518,7 @@ const EmojiPickerMenu = (props) => { Date: Sat, 7 Oct 2023 14:38:23 +0530 Subject: [PATCH 10/45] progress #8:updatePreferredSkinTone() + JsDoc upd in User.js Signed-off-by: Ashutosh Khanduala --- .../EmojiPickerMenu/EmojiPickerMenu copy.tsx | 11 +++++------ src/libs/actions/User.js | 2 +- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx b/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx index afbdec31f42d..8a628e6e682d 100755 --- a/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx +++ b/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx @@ -33,8 +33,7 @@ const propTypes = { forwardedRef: PropTypes.func, /** Stores user's preferred skin tone */ - preferredSkinTone: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), - + preferredSkinTone: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), // TODO: preferredSkinTone must be number (always) /** Stores user's frequently used emojis */ // eslint-disable-next-line react/forbid-prop-types frequentlyUsedEmojis: PropTypes.arrayOf(PropTypes.object), @@ -64,7 +63,6 @@ const EmojiPickerMenu = (props) => { // prevent auto focus when open picker for mobile device const shouldFocusInputOnScreenFocus = canFocusInputOnScreenFocus(); - this.updatePreferredSkinTone = this.updatePreferredSkinTone.bind(this); this.setFirstNonHeaderIndex = this.setFirstNonHeaderIndex.bind(this); this.getItemLayout = this.getItemLayout.bind(this); this.scrollToHeader = this.scrollToHeader.bind(this); @@ -409,11 +407,12 @@ const EmojiPickerMenu = (props) => { /** * @param {Number} skinTone */ - function updatePreferredSkinTone(skinTone) { - if (preferredSkinTone === skinTone) { + function updatePreferredSkinTone(skinTone: number) { + if (Number(preferredSkinTone) === skinTone) { // TODO: temp Number() for safety return; } + // TODO: Change JS Doc in User.js (type string => number) User.updatePreferredSkinTone(skinTone); } @@ -537,7 +536,7 @@ const EmojiPickerMenu = (props) => { ListEmptyComponent={{translate('common.noResultsFound')}} /> diff --git a/src/libs/actions/User.js b/src/libs/actions/User.js index 1830d1e51f6f..f939e64d0b9a 100644 --- a/src/libs/actions/User.js +++ b/src/libs/actions/User.js @@ -541,7 +541,7 @@ function subscribeToUserEvents() { /** * Sync preferredSkinTone with Onyx and Server - * @param {String} skinTone + * @param {Number} skinTone */ function updatePreferredSkinTone(skinTone) { const optimisticData = [ From 3aa5cfb7831f84fbe046ecfee4ddc239a2dd0597 Mon Sep 17 00:00:00 2001 From: Ashutosh Khanduala Date: Sat, 7 Oct 2023 15:25:30 +0530 Subject: [PATCH 11/45] progress #9:setFirstNonHeaderIndex() + getItemLayout() + scrollToHeader() Signed-off-by: Ashutosh Khanduala --- .../EmojiPickerMenu/EmojiPickerMenu copy.tsx | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx b/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx index 8a628e6e682d..9506b2c552f1 100755 --- a/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx +++ b/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx @@ -63,10 +63,6 @@ const EmojiPickerMenu = (props) => { // prevent auto focus when open picker for mobile device const shouldFocusInputOnScreenFocus = canFocusInputOnScreenFocus(); - this.setFirstNonHeaderIndex = this.setFirstNonHeaderIndex.bind(this); - this.getItemLayout = this.getItemLayout.bind(this); - this.scrollToHeader = this.scrollToHeader.bind(this); - this.firstNonHeaderIndex = 0; const {filteredEmojis, headerEmojis, headerRowIndices} = this.getEmojisAndHeaderRowIndices(); @@ -96,7 +92,7 @@ const EmojiPickerMenu = (props) => { forwardedRef(searchInputRef.current); } setupEventHandlers(); - this.setFirstNonHeaderIndex(this.emojis); + setFirstNonHeaderIndex(this.emojis); } function componentDidUpdate(prevProps) { @@ -156,7 +152,8 @@ const EmojiPickerMenu = (props) => { * Find and store index of the first emoji item * @param {Array} filteredEmojis */ - function setFirstNonHeaderIndex(filteredEmojis) { + function setFirstNonHeaderIndex(filteredEmojis: any[]) { + // TODO: Emoji Object type this.firstNonHeaderIndex = _.findIndex(filteredEmojis, (item) => !item.spacer && !item.header); } @@ -233,7 +230,8 @@ const EmojiPickerMenu = (props) => { * @param {Number} index row index * @returns {Object} */ - function getItemLayout(data, index) { + function getItemLayout(data: any, index: number) { + // TODO: data param is unused. Still find its type return {length: CONST.EMOJI_PICKER_ITEM_HEIGHT, offset: CONST.EMOJI_PICKER_ITEM_HEIGHT * index, index}; } @@ -360,7 +358,7 @@ const EmojiPickerMenu = (props) => { } } - function scrollToHeader(headerIndex) { + function scrollToHeader(headerIndex: number) { const calculatedOffset = Math.floor(headerIndex / CONST.EMOJI_NUM_PER_ROW) * CONST.EMOJI_PICKER_HEADER_HEIGHT; emojiListRef.current?.flashScrollIndicators(); emojiListRef.current?.scrollToOffset({offset: calculatedOffset, animated: true}); @@ -382,14 +380,14 @@ const EmojiPickerMenu = (props) => { headerIndices: this.headerRowIndices, highlightedIndex: -1, }); - this.setFirstNonHeaderIndex(this.emojis); + setFirstNonHeaderIndex(this.emojis); return; } const newFilteredEmojiList = EmojiUtils.suggestEmojis(`:${normalizedSearchTerm}`, preferredLocale, this.emojis.length); // Remove sticky header indices. There are no headers while searching and we don't want to make emojis sticky this.setState({filteredEmojis: newFilteredEmojiList, headerIndices: [], highlightedIndex: 0}); - this.setFirstNonHeaderIndex(newFilteredEmojiList); + setFirstNonHeaderIndex(newFilteredEmojiList); }, 300), [], ); @@ -511,7 +509,7 @@ const EmojiPickerMenu = (props) => { {!isFiltered && ( )} { ]} extraData={[this.state.filteredEmojis, this.state.highlightedIndex, preferredSkinTone]} stickyHeaderIndices={this.state.headerIndices} - getItemLayout={this.getItemLayout} + getItemLayout={getItemLayout} contentContainerStyle={styles.flexGrow1} ListEmptyComponent={{translate('common.noResultsFound')}} /> From 607b0cbd060f5fb39a5c9dc962d879e2061498a5 Mon Sep 17 00:00:00 2001 From: Ashutosh Khanduala Date: Sat, 7 Oct 2023 15:38:56 +0530 Subject: [PATCH 12/45] p#10: rename setFirstNonHeaderIndex -> updateFirstNonHeaderIndex() + firstNonHeaderIndex converted to ref Signed-off-by: Ashutosh Khanduala --- .../EmojiPickerMenu/EmojiPickerMenu copy.tsx | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx b/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx index 9506b2c552f1..ba35f2d80b17 100755 --- a/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx +++ b/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx @@ -63,7 +63,7 @@ const EmojiPickerMenu = (props) => { // prevent auto focus when open picker for mobile device const shouldFocusInputOnScreenFocus = canFocusInputOnScreenFocus(); - this.firstNonHeaderIndex = 0; + const firstNonHeaderIndex = useRef(0); const {filteredEmojis, headerEmojis, headerRowIndices} = this.getEmojisAndHeaderRowIndices(); this.emojis = filteredEmojis; @@ -92,7 +92,7 @@ const EmojiPickerMenu = (props) => { forwardedRef(searchInputRef.current); } setupEventHandlers(); - setFirstNonHeaderIndex(this.emojis); + updateFirstNonHeaderIndex(this.emojis); } function componentDidUpdate(prevProps) { @@ -152,9 +152,9 @@ const EmojiPickerMenu = (props) => { * Find and store index of the first emoji item * @param {Array} filteredEmojis */ - function setFirstNonHeaderIndex(filteredEmojis: any[]) { + function updateFirstNonHeaderIndex(filteredEmojis: any[]) { // TODO: Emoji Object type - this.firstNonHeaderIndex = _.findIndex(filteredEmojis, (item) => !item.spacer && !item.header); + firstNonHeaderIndex.current = _.findIndex(filteredEmojis, (item) => !item.spacer && !item.header); } /** @@ -298,7 +298,7 @@ const EmojiPickerMenu = (props) => { // If nothing is highlighted and an arrow key is pressed // select the first emoji, apply keyboard movement styles, and disable pointer events if (this.state.highlightedIndex === -1) { - this.setState({highlightedIndex: this.firstNonHeaderIndex, isUsingKeyboardMovement: true, arePointerEventsDisabled: true}); + this.setState({highlightedIndex: firstNonHeaderIndex.current, isUsingKeyboardMovement: true, arePointerEventsDisabled: true}); return; } @@ -326,7 +326,7 @@ const EmojiPickerMenu = (props) => { case 'ArrowLeft': move( -1, - () => this.state.highlightedIndex - 1 < this.firstNonHeaderIndex, + () => this.state.highlightedIndex - 1 < firstNonHeaderIndex.current, () => { // Reaching start of the list, arrow left set the focus to searchInput. this.focusInputWithTextSelect(); @@ -340,7 +340,7 @@ const EmojiPickerMenu = (props) => { case 'ArrowUp': move( -CONST.EMOJI_NUM_PER_ROW, - () => this.state.highlightedIndex - CONST.EMOJI_NUM_PER_ROW < this.firstNonHeaderIndex, + () => this.state.highlightedIndex - CONST.EMOJI_NUM_PER_ROW < firstNonHeaderIndex.current, () => { // Reaching start of the list, arrow up set the focus to searchInput. this.focusInputWithTextSelect(); @@ -380,14 +380,14 @@ const EmojiPickerMenu = (props) => { headerIndices: this.headerRowIndices, highlightedIndex: -1, }); - setFirstNonHeaderIndex(this.emojis); + updateFirstNonHeaderIndex(this.emojis); return; } const newFilteredEmojiList = EmojiUtils.suggestEmojis(`:${normalizedSearchTerm}`, preferredLocale, this.emojis.length); // Remove sticky header indices. There are no headers while searching and we don't want to make emojis sticky this.setState({filteredEmojis: newFilteredEmojiList, headerIndices: [], highlightedIndex: 0}); - setFirstNonHeaderIndex(newFilteredEmojiList); + updateFirstNonHeaderIndex(newFilteredEmojiList); }, 300), [], ); @@ -406,7 +406,8 @@ const EmojiPickerMenu = (props) => { * @param {Number} skinTone */ function updatePreferredSkinTone(skinTone: number) { - if (Number(preferredSkinTone) === skinTone) { // TODO: temp Number() for safety + if (Number(preferredSkinTone) === skinTone) { + // TODO: temp Number() for safety return; } From c0716467b9de0fcc1e8954e11c7cf97b81152be7 Mon Sep 17 00:00:00 2001 From: Ashutosh Khanduala Date: Sat, 7 Oct 2023 16:49:39 +0530 Subject: [PATCH 13/45] p#11: emojis Ref + filteredEmojis state Signed-off-by: Ashutosh Khanduala --- .../EmojiPickerMenu/EmojiPickerMenu copy.tsx | 46 +++++++++++-------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx b/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx index ba35f2d80b17..b43533649ca4 100755 --- a/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx +++ b/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx @@ -1,4 +1,4 @@ -import React, {Component, useCallback, useRef} from 'react'; +import React, {Component, useCallback, useRef, useState} from 'react'; import {View, FlatList, TextInputSelectionChangeEventData} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import PropTypes from 'prop-types'; @@ -65,13 +65,18 @@ const EmojiPickerMenu = (props) => { const firstNonHeaderIndex = useRef(0); - const {filteredEmojis, headerEmojis, headerRowIndices} = this.getEmojisAndHeaderRowIndices(); - this.emojis = filteredEmojis; - this.headerEmojis = headerEmojis; + const { headerEmojis, headerRowIndices} = getEmojisAndHeaderRowIndices(); + const emojis = useRef([]); // TODO: find TS type + if (emojis.current.length === 0) { + emojis.current = getEmojisAndHeaderRowIndices().filteredEmojis; + } this.headerRowIndices = headerRowIndices; + this.headerEmojis = headerEmojis; + + // TODO: Group releated states in objects + const [filteredEmojis, setFilteredEmojis] = useState(emojis.current); this.state = { - filteredEmojis: this.emojis, headerIndices: this.headerRowIndices, highlightedIndex: -1, arePointerEventsDisabled: false, @@ -92,7 +97,7 @@ const EmojiPickerMenu = (props) => { forwardedRef(searchInputRef.current); } setupEventHandlers(); - updateFirstNonHeaderIndex(this.emojis); + updateFirstNonHeaderIndex(emojis.current); } function componentDidUpdate(prevProps) { @@ -100,12 +105,12 @@ const EmojiPickerMenu = (props) => { return; } - const {filteredEmojis, headerEmojis, headerRowIndices} = this.getEmojisAndHeaderRowIndices(); - this.emojis = filteredEmojis; + const {filteredEmojis, headerEmojis, headerRowIndices} = getEmojisAndHeaderRowIndices(); + emojis.current = filteredEmojis; this.headerEmojis = headerEmojis; this.headerRowIndices = headerRowIndices; + setFilteredEmojis(emojis.current); this.setState({ - filteredEmojis: this.emojis, headerIndices: this.headerRowIndices, }); } @@ -152,7 +157,7 @@ const EmojiPickerMenu = (props) => { * Find and store index of the first emoji item * @param {Array} filteredEmojis */ - function updateFirstNonHeaderIndex(filteredEmojis: any[]) { + function updateFirstNonHeaderIndex(filteredEmojis: Object[]) { // TODO: Emoji Object type firstNonHeaderIndex.current = _.findIndex(filteredEmojis, (item) => !item.spacer && !item.header); } @@ -270,7 +275,7 @@ const EmojiPickerMenu = (props) => { * @param {String} arrowKey */ function highlightAdjacentEmoji(arrowKey: KeyboardEvent['key']) { - if (this.state.filteredEmojis.length === 0) { + if (filteredEmojis.length === 0) { return; } @@ -321,7 +326,7 @@ const EmojiPickerMenu = (props) => { switch (arrowKey) { case 'ArrowDown': - move(CONST.EMOJI_NUM_PER_ROW, () => this.state.highlightedIndex + CONST.EMOJI_NUM_PER_ROW > this.state.filteredEmojis.length - 1); + move(CONST.EMOJI_NUM_PER_ROW, () => this.state.highlightedIndex + CONST.EMOJI_NUM_PER_ROW > filteredEmojis.length - 1); break; case 'ArrowLeft': move( @@ -335,7 +340,7 @@ const EmojiPickerMenu = (props) => { ); break; case 'ArrowRight': - move(1, () => this.state.highlightedIndex + 1 > this.state.filteredEmojis.length - 1); + move(1, () => this.state.highlightedIndex + 1 > filteredEmojis.length - 1); break; case 'ArrowUp': move( @@ -375,18 +380,19 @@ const EmojiPickerMenu = (props) => { emojiListRef.current?.scrollToOffset({offset: 0, animated: false}); if (normalizedSearchTerm === '') { // There are no headers when searching, so we need to re-make them sticky when there is no search term + setFilteredEmojis(emojis.current); this.setState({ - filteredEmojis: this.emojis, headerIndices: this.headerRowIndices, highlightedIndex: -1, }); - updateFirstNonHeaderIndex(this.emojis); + updateFirstNonHeaderIndex(emojis.current); return; } - const newFilteredEmojiList = EmojiUtils.suggestEmojis(`:${normalizedSearchTerm}`, preferredLocale, this.emojis.length); + const newFilteredEmojiList = EmojiUtils.suggestEmojis(`:${normalizedSearchTerm}`, preferredLocale, emojis.current.length); // Remove sticky header indices. There are no headers while searching and we don't want to make emojis sticky - this.setState({filteredEmojis: newFilteredEmojiList, headerIndices: [], highlightedIndex: 0}); + setFilteredEmojis(newFilteredEmojiList); + this.setState({headerIndices: [], highlightedIndex: 0}); updateFirstNonHeaderIndex(newFilteredEmojiList); }, 300), [], @@ -480,7 +486,7 @@ const EmojiPickerMenu = (props) => { ); } - const isFiltered = this.emojis.length !== this.state.filteredEmojis.length; + const isFiltered = emojis.current.length !== filteredEmojis.length; const listStyle = StyleUtils.getEmojiPickerListHeight(isFiltered, windowHeight); const height = !listStyle.maxHeight || listStyle.height < listStyle.maxHeight ? listStyle.height : listStyle.maxHeight; const overflowLimit = Math.floor(height / CONST.EMOJI_PICKER_ITEM_HEIGHT) * 8; @@ -504,7 +510,7 @@ const EmojiPickerMenu = (props) => { onFocus={() => this.setState({isFocused: true, highlightedIndex: -1, isUsingKeyboardMovement: false})} onBlur={() => this.setState({isFocused: false})} autoCorrect={false} - blurOnSubmit={this.state.filteredEmojis.length > 0} + blurOnSubmit={filteredEmojis.length > 0} /> {!isFiltered && ( @@ -524,7 +530,7 @@ const EmojiPickerMenu = (props) => { // This prevents elastic scrolling when scroll reaches the start or end {overscrollBehaviorY: 'contain'}, // Set overflow to hidden to prevent elastic scrolling when there are not enough contents to scroll in FlatList - {overflowY: this.state.filteredEmojis.length > overflowLimit ? 'auto' : 'hidden'}, + {overflowY: filteredEmojis.length > overflowLimit ? 'auto' : 'hidden'}, // Set scrollPaddingTop to consider sticky headers while scrolling {scrollPaddingTop: isFiltered ? 0 : CONST.EMOJI_PICKER_ITEM_HEIGHT}, ]} From 878f663f2db524d25a40f68b49743b684d06cace Mon Sep 17 00:00:00 2001 From: Ashutosh Khanduala Date: Sat, 7 Oct 2023 17:55:30 +0530 Subject: [PATCH 14/45] p#12: headerRowIndices Ref + headerIndices state Signed-off-by: Ashutosh Khanduala --- .../EmojiPickerMenu/EmojiPickerMenu copy.tsx | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx b/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx index b43533649ca4..87b41bf62a01 100755 --- a/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx +++ b/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx @@ -65,19 +65,21 @@ const EmojiPickerMenu = (props) => { const firstNonHeaderIndex = useRef(0); - const { headerEmojis, headerRowIndices} = getEmojisAndHeaderRowIndices(); + const {headerEmojis} = getEmojisAndHeaderRowIndices(); const emojis = useRef([]); // TODO: find TS type if (emojis.current.length === 0) { emojis.current = getEmojisAndHeaderRowIndices().filteredEmojis; } - this.headerRowIndices = headerRowIndices; + const headerRowIndices = useRef([]); // TODO: Maybe this ref is not needed. headerIndices state might suffice + if (headerRowIndices.current.length === 0) { + headerRowIndices.current = getEmojisAndHeaderRowIndices().headerRowIndices; + } this.headerEmojis = headerEmojis; // TODO: Group releated states in objects const [filteredEmojis, setFilteredEmojis] = useState(emojis.current); - + const [headerIndices, setHeaderIndices] = useState(headerRowIndices.current); this.state = { - headerIndices: this.headerRowIndices, highlightedIndex: -1, arePointerEventsDisabled: false, selection: { @@ -105,14 +107,12 @@ const EmojiPickerMenu = (props) => { return; } - const {filteredEmojis, headerEmojis, headerRowIndices} = getEmojisAndHeaderRowIndices(); - emojis.current = filteredEmojis; - this.headerEmojis = headerEmojis; - this.headerRowIndices = headerRowIndices; + const emojisAndHeaderRowIndices = getEmojisAndHeaderRowIndices(); + emojis.current = emojisAndHeaderRowIndices.filteredEmojis; + headerRowIndices.current = emojisAndHeaderRowIndices.headerRowIndices; + this.headerEmojis = emojisAndHeaderRowIndices.headerEmojis; setFilteredEmojis(emojis.current); - this.setState({ - headerIndices: this.headerRowIndices, - }); + setHeaderIndices(headerRowIndices.current); } function componentWillUnmount() { @@ -381,8 +381,8 @@ const EmojiPickerMenu = (props) => { if (normalizedSearchTerm === '') { // There are no headers when searching, so we need to re-make them sticky when there is no search term setFilteredEmojis(emojis.current); + setHeaderIndices(headerRowIndices.current); this.setState({ - headerIndices: this.headerRowIndices, highlightedIndex: -1, }); updateFirstNonHeaderIndex(emojis.current); @@ -392,7 +392,8 @@ const EmojiPickerMenu = (props) => { // Remove sticky header indices. There are no headers while searching and we don't want to make emojis sticky setFilteredEmojis(newFilteredEmojiList); - this.setState({headerIndices: [], highlightedIndex: 0}); + setHeaderIndices([]); + this.setState({highlightedIndex: 0}); updateFirstNonHeaderIndex(newFilteredEmojiList); }, 300), [], @@ -535,7 +536,7 @@ const EmojiPickerMenu = (props) => { {scrollPaddingTop: isFiltered ? 0 : CONST.EMOJI_PICKER_ITEM_HEIGHT}, ]} extraData={[this.state.filteredEmojis, this.state.highlightedIndex, preferredSkinTone]} - stickyHeaderIndices={this.state.headerIndices} + stickyHeaderIndices={headerIndices} getItemLayout={getItemLayout} contentContainerStyle={styles.flexGrow1} ListEmptyComponent={{translate('common.noResultsFound')}} From cbd0dc4a2e4c624198093f4e23b676022939ad05 Mon Sep 17 00:00:00 2001 From: Ashutosh Khanduala Date: Sat, 7 Oct 2023 18:17:05 +0530 Subject: [PATCH 15/45] p#13: headerEmojis ref (IMP!!: this should be a ref) Signed-off-by: Ashutosh Khanduala --- .../EmojiPickerMenu/EmojiPickerMenu copy.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx b/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx index 87b41bf62a01..fcf4327cadb0 100755 --- a/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx +++ b/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx @@ -65,7 +65,7 @@ const EmojiPickerMenu = (props) => { const firstNonHeaderIndex = useRef(0); - const {headerEmojis} = getEmojisAndHeaderRowIndices(); + // TODO: Group the 3 refs into 1?? Adv:- code would look cleaner + there will be only 1 getEmojisAndHeaderRowIndices() call. const emojis = useRef([]); // TODO: find TS type if (emojis.current.length === 0) { emojis.current = getEmojisAndHeaderRowIndices().filteredEmojis; @@ -74,7 +74,10 @@ const EmojiPickerMenu = (props) => { if (headerRowIndices.current.length === 0) { headerRowIndices.current = getEmojisAndHeaderRowIndices().headerRowIndices; } - this.headerEmojis = headerEmojis; + const headerEmojis = useRef([]); // TODO: find TS type + if (headerEmojis.current.length === 0) { + headerEmojis.current = getEmojisAndHeaderRowIndices().headerEmojis; + } // TODO: Group releated states in objects const [filteredEmojis, setFilteredEmojis] = useState(emojis.current); @@ -110,7 +113,7 @@ const EmojiPickerMenu = (props) => { const emojisAndHeaderRowIndices = getEmojisAndHeaderRowIndices(); emojis.current = emojisAndHeaderRowIndices.filteredEmojis; headerRowIndices.current = emojisAndHeaderRowIndices.headerRowIndices; - this.headerEmojis = emojisAndHeaderRowIndices.headerEmojis; + headerEmojis.current = emojisAndHeaderRowIndices.headerEmojis; setFilteredEmojis(emojis.current); setHeaderIndices(headerRowIndices.current); } @@ -516,7 +519,7 @@ const EmojiPickerMenu = (props) => { {!isFiltered && ( )} From 4a7995ee88e76f7a04a2ad8f05618db286a2ccc9 Mon Sep 17 00:00:00 2001 From: Ashutosh Khanduala Date: Sat, 7 Oct 2023 18:35:37 +0530 Subject: [PATCH 16/45] p#14: highlightedIndex state Signed-off-by: Ashutosh Khanduala --- .../EmojiPickerMenu/EmojiPickerMenu copy.tsx | 62 ++++++++++--------- 1 file changed, 33 insertions(+), 29 deletions(-) diff --git a/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx b/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx index fcf4327cadb0..1674540c7828 100755 --- a/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx +++ b/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx @@ -82,8 +82,8 @@ const EmojiPickerMenu = (props) => { // TODO: Group releated states in objects const [filteredEmojis, setFilteredEmojis] = useState(emojis.current); const [headerIndices, setHeaderIndices] = useState(headerRowIndices.current); + const [highlightedIndex, setHighlightedIndex] = useState(-1); this.state = { - highlightedIndex: -1, arePointerEventsDisabled: false, selection: { start: 0, @@ -185,8 +185,8 @@ const EmojiPickerMenu = (props) => { } // Select the currently highlighted emoji if enter is pressed - if (!isEnterWhileComposition(keyBoardEvent) && keyBoardEvent.key === CONST.KEYBOARD_SHORTCUTS.ENTER.shortcutKey && this.state.highlightedIndex !== -1) { - const item = this.state.filteredEmojis[this.state.highlightedIndex]; + if (!isEnterWhileComposition(keyBoardEvent) && keyBoardEvent.key === CONST.KEYBOARD_SHORTCUTS.ENTER.shortcutKey && highlightedIndex !== -1) { + const item = this.state.filteredEmojis[highlightedIndex]; if (!item) { return; } @@ -298,19 +298,20 @@ const EmojiPickerMenu = (props) => { // We only want to hightlight the Emoji if none was highlighted already // If we already have a highlighted Emoji, lets just skip the first navigation - if (this.state.highlightedIndex !== -1) { + if (highlightedIndex !== -1) { return; } } // If nothing is highlighted and an arrow key is pressed // select the first emoji, apply keyboard movement styles, and disable pointer events - if (this.state.highlightedIndex === -1) { - this.setState({highlightedIndex: firstNonHeaderIndex.current, isUsingKeyboardMovement: true, arePointerEventsDisabled: true}); + if (highlightedIndex === -1) { + setHighlightedIndex(firstNonHeaderIndex.current); + this.setState({isUsingKeyboardMovement: true, arePointerEventsDisabled: true}); return; } - let newIndex = this.state.highlightedIndex; + let newIndex = highlightedIndex; const move = (steps, boundsCheck, onBoundReached = () => {}) => { if (boundsCheck()) { onBoundReached(); @@ -329,12 +330,12 @@ const EmojiPickerMenu = (props) => { switch (arrowKey) { case 'ArrowDown': - move(CONST.EMOJI_NUM_PER_ROW, () => this.state.highlightedIndex + CONST.EMOJI_NUM_PER_ROW > filteredEmojis.length - 1); + move(CONST.EMOJI_NUM_PER_ROW, () => highlightedIndex + CONST.EMOJI_NUM_PER_ROW > filteredEmojis.length - 1); break; case 'ArrowLeft': move( -1, - () => this.state.highlightedIndex - 1 < firstNonHeaderIndex.current, + () => highlightedIndex - 1 < firstNonHeaderIndex.current, () => { // Reaching start of the list, arrow left set the focus to searchInput. this.focusInputWithTextSelect(); @@ -343,12 +344,12 @@ const EmojiPickerMenu = (props) => { ); break; case 'ArrowRight': - move(1, () => this.state.highlightedIndex + 1 > filteredEmojis.length - 1); + move(1, () => highlightedIndex + 1 > filteredEmojis.length - 1); break; case 'ArrowUp': move( -CONST.EMOJI_NUM_PER_ROW, - () => this.state.highlightedIndex - CONST.EMOJI_NUM_PER_ROW < firstNonHeaderIndex.current, + () => highlightedIndex - CONST.EMOJI_NUM_PER_ROW < firstNonHeaderIndex.current, () => { // Reaching start of the list, arrow up set the focus to searchInput. this.focusInputWithTextSelect(); @@ -361,8 +362,9 @@ const EmojiPickerMenu = (props) => { } // Actually highlight the new emoji, apply keyboard movement styles, and disable pointer events - if (newIndex !== this.state.highlightedIndex) { - this.setState({highlightedIndex: newIndex, isUsingKeyboardMovement: true, arePointerEventsDisabled: true}); + if (newIndex !== highlightedIndex) { + setHighlightedIndex(newIndex); + this.setState({isUsingKeyboardMovement: true, arePointerEventsDisabled: true}); } } @@ -385,9 +387,7 @@ const EmojiPickerMenu = (props) => { // There are no headers when searching, so we need to re-make them sticky when there is no search term setFilteredEmojis(emojis.current); setHeaderIndices(headerRowIndices.current); - this.setState({ - highlightedIndex: -1, - }); + setHighlightedIndex(-1); updateFirstNonHeaderIndex(emojis.current); return; } @@ -396,7 +396,7 @@ const EmojiPickerMenu = (props) => { // Remove sticky header indices. There are no headers while searching and we don't want to make emojis sticky setFilteredEmojis(newFilteredEmojiList); setHeaderIndices([]); - this.setState({highlightedIndex: 0}); + setHighlightedIndex(0); updateFirstNonHeaderIndex(newFilteredEmojiList); }, 300), [], @@ -462,29 +462,30 @@ const EmojiPickerMenu = (props) => { const emojiCode = types && types[preferredSkinTone] ? types[preferredSkinTone] : code; - const isEmojiFocused = index === this.state.highlightedIndex && this.state.isUsingKeyboardMovement; + const isEmojiFocused = index === highlightedIndex && this.state.isUsingKeyboardMovement; return ( this.addToFrequentAndSelectEmoji(emoji, item)} - onHoverIn={() => this.setState({highlightedIndex: index, isUsingKeyboardMovement: false})} + onHoverIn={() => { + setHighlightedIndex(index); + this.setState({isUsingKeyboardMovement: false}); + }} onHoverOut={() => { if (this.state.arePointerEventsDisabled) { return; } - this.setState({highlightedIndex: -1}); + setHighlightedIndex(-1); }} emoji={emojiCode} - onFocus={() => this.setState({highlightedIndex: index})} + onFocus={() => void setHighlightedIndex(index)} onBlur={() => - this.setState((prevState) => ({ - // Only clear the highlighted index if the highlighted index is the same, - // meaning that the focus changed to an element that is not an emoji item. - highlightedIndex: prevState.highlightedIndex === index ? -1 : prevState.highlightedIndex, - })) + // Only clear the highlighted index if the highlighted index is the same, + // meaning that the focus changed to an element that is not an emoji item. + setHighlightedIndex((prevState) => (prevState === index ? -1 : prevState)) } isFocused={isEmojiFocused} - isHighlighted={index === this.state.highlightedIndex} + isHighlighted={index === highlightedIndex} isUsingKeyboardMovement={this.state.isUsingKeyboardMovement} /> ); @@ -511,7 +512,10 @@ const EmojiPickerMenu = (props) => { autoFocus={shouldFocusInputOnScreenFocus} selectTextOnFocus={this.state.selectTextOnFocus} onSelectionChange={onSelectionChange} - onFocus={() => this.setState({isFocused: true, highlightedIndex: -1, isUsingKeyboardMovement: false})} + onFocus={() => { + setHighlightedIndex(-1); + this.setState({isFocused: true, isUsingKeyboardMovement: false}); + }} onBlur={() => this.setState({isFocused: false})} autoCorrect={false} blurOnSubmit={filteredEmojis.length > 0} @@ -538,7 +542,7 @@ const EmojiPickerMenu = (props) => { // Set scrollPaddingTop to consider sticky headers while scrolling {scrollPaddingTop: isFiltered ? 0 : CONST.EMOJI_PICKER_ITEM_HEIGHT}, ]} - extraData={[this.state.filteredEmojis, this.state.highlightedIndex, preferredSkinTone]} + extraData={[this.state.filteredEmojis, highlightedIndex, preferredSkinTone]} stickyHeaderIndices={headerIndices} getItemLayout={getItemLayout} contentContainerStyle={styles.flexGrow1} From 5634ec41a50e556a93b5bd69fe3f3b026bcf8344 Mon Sep 17 00:00:00 2001 From: Ashutosh Khanduala Date: Sat, 7 Oct 2023 18:41:59 +0530 Subject: [PATCH 17/45] p#15: arePointerEventsDisabled state Signed-off-by: Ashutosh Khanduala --- .../EmojiPickerMenu/EmojiPickerMenu copy.tsx | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx b/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx index 1674540c7828..c9195eeb5dbd 100755 --- a/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx +++ b/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx @@ -83,8 +83,8 @@ const EmojiPickerMenu = (props) => { const [filteredEmojis, setFilteredEmojis] = useState(emojis.current); const [headerIndices, setHeaderIndices] = useState(headerRowIndices.current); const [highlightedIndex, setHighlightedIndex] = useState(-1); + const [arePointerEventsDisabled, setArePointerEventsDisabled] = useState(false); this.state = { - arePointerEventsDisabled: false, selection: { start: 0, end: 0, @@ -219,11 +219,10 @@ const EmojiPickerMenu = (props) => { // Re-enable pointer events and hovering over EmojiPickerItems when the mouse moves this.mouseMoveHandler = () => { - if (!this.state.arePointerEventsDisabled) { + if (!arePointerEventsDisabled) { return; } - - this.setState({arePointerEventsDisabled: false}); + setArePointerEventsDisabled(false); }; document.addEventListener('mousemove', this.mouseMoveHandler); } @@ -294,7 +293,8 @@ const EmojiPickerMenu = (props) => { // Blur the input, change the highlight type to keyboard, and disable pointer events searchInputRef.current.blur(); - this.setState({isUsingKeyboardMovement: true, arePointerEventsDisabled: true}); + setArePointerEventsDisabled(true); + this.setState({isUsingKeyboardMovement: true}); // We only want to hightlight the Emoji if none was highlighted already // If we already have a highlighted Emoji, lets just skip the first navigation @@ -307,7 +307,8 @@ const EmojiPickerMenu = (props) => { // select the first emoji, apply keyboard movement styles, and disable pointer events if (highlightedIndex === -1) { setHighlightedIndex(firstNonHeaderIndex.current); - this.setState({isUsingKeyboardMovement: true, arePointerEventsDisabled: true}); + setArePointerEventsDisabled(true); + this.setState({isUsingKeyboardMovement: true}); return; } @@ -364,7 +365,8 @@ const EmojiPickerMenu = (props) => { // Actually highlight the new emoji, apply keyboard movement styles, and disable pointer events if (newIndex !== highlightedIndex) { setHighlightedIndex(newIndex); - this.setState({isUsingKeyboardMovement: true, arePointerEventsDisabled: true}); + setArePointerEventsDisabled(true); + this.setState({isUsingKeyboardMovement: true}); } } @@ -472,7 +474,7 @@ const EmojiPickerMenu = (props) => { this.setState({isUsingKeyboardMovement: false}); }} onHoverOut={() => { - if (this.state.arePointerEventsDisabled) { + if (arePointerEventsDisabled) { return; } setHighlightedIndex(-1); @@ -499,7 +501,7 @@ const EmojiPickerMenu = (props) => { Date: Sat, 7 Oct 2023 18:47:13 +0530 Subject: [PATCH 18/45] p#16: selection state Signed-off-by: Ashutosh Khanduala --- .../EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx b/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx index c9195eeb5dbd..2001cd0076b9 100755 --- a/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx +++ b/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx @@ -84,11 +84,8 @@ const EmojiPickerMenu = (props) => { const [headerIndices, setHeaderIndices] = useState(headerRowIndices.current); const [highlightedIndex, setHighlightedIndex] = useState(-1); const [arePointerEventsDisabled, setArePointerEventsDisabled] = useState(false); + const [selection, setSelection] = useState({start: 0, end: 0}); this.state = { - selection: { - start: 0, - end: 0, - }, isFocused: false, isUsingKeyboardMovement: false, }; @@ -128,7 +125,7 @@ const EmojiPickerMenu = (props) => { * @param {Event} event */ function onSelectionChange(event: NativeSyntheticEvent) { - this.setState({selection: event.nativeEvent.selection}); + setSelection(event.nativeEvent.selection); } /** @@ -287,7 +284,7 @@ const EmojiPickerMenu = (props) => { return; } - if (arrowKey === 'ArrowRight' && !(searchInputRef.current.value.length === this.state.selection.start && this.state.selection.start === this.state.selection.end)) { + if (arrowKey === 'ArrowRight' && !(searchInputRef.current.value.length === selection.start && selection.start === selection.end)) { return; } From 76bde4627a4b16fa9fdc2592a7054636d71cb4bf Mon Sep 17 00:00:00 2001 From: Ashutosh Khanduala Date: Sat, 7 Oct 2023 18:51:23 +0530 Subject: [PATCH 19/45] p#17: isFocused state Signed-off-by: Ashutosh Khanduala --- .../EmojiPickerMenu/EmojiPickerMenu copy.tsx | 9 +- .../EmojiPicker/EmojiPickerMenu/ref.tsx | 568 ++++++++++++++++++ 2 files changed, 573 insertions(+), 4 deletions(-) create mode 100644 src/components/EmojiPicker/EmojiPickerMenu/ref.tsx diff --git a/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx b/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx index 2001cd0076b9..e7c1c8751625 100755 --- a/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx +++ b/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx @@ -85,8 +85,8 @@ const EmojiPickerMenu = (props) => { const [highlightedIndex, setHighlightedIndex] = useState(-1); const [arePointerEventsDisabled, setArePointerEventsDisabled] = useState(false); const [selection, setSelection] = useState({start: 0, end: 0}); + const [isFocused, setIsFocused] = useState(false); this.state = { - isFocused: false, isUsingKeyboardMovement: false, }; @@ -172,7 +172,7 @@ const EmojiPickerMenu = (props) => { this.keyDownHandler = (keyBoardEvent) => { if (keyBoardEvent.key.startsWith('Arrow')) { - if (!this.state.isFocused || keyBoardEvent.key === 'ArrowUp' || keyBoardEvent.key === 'ArrowDown') { + if (!isFocused || keyBoardEvent.key === 'ArrowUp' || keyBoardEvent.key === 'ArrowDown') { keyBoardEvent.preventDefault(); } @@ -513,9 +513,10 @@ const EmojiPickerMenu = (props) => { onSelectionChange={onSelectionChange} onFocus={() => { setHighlightedIndex(-1); - this.setState({isFocused: true, isUsingKeyboardMovement: false}); + setIsFocused(true); + this.setState({isUsingKeyboardMovement: false}); }} - onBlur={() => this.setState({isFocused: false})} + onBlur={() => setIsFocused(false)} autoCorrect={false} blurOnSubmit={filteredEmojis.length > 0} /> diff --git a/src/components/EmojiPicker/EmojiPickerMenu/ref.tsx b/src/components/EmojiPicker/EmojiPickerMenu/ref.tsx new file mode 100644 index 000000000000..b3e54c1e037b --- /dev/null +++ b/src/components/EmojiPicker/EmojiPickerMenu/ref.tsx @@ -0,0 +1,568 @@ +import React, {Component, FC, useCallback, useRef, useState} from 'react'; +import {View, FlatList, NativeSyntheticEvent, TextInputSelectionChangeEventData} from 'react-native'; +import {withOnyx} from 'react-native-onyx'; +import PropTypes from 'prop-types'; +import _ from 'underscore'; +import lodashGet from 'lodash/get'; +import CONST from '../../../CONST'; +import ONYXKEYS from '../../../ONYXKEYS'; +import styles from '../../../styles/styles'; +import * as StyleUtils from '../../../styles/StyleUtils'; +import emojis from '../../../../assets/emojis'; +import EmojiPickerMenuItem from '../EmojiPickerMenuItem'; +import Text from '../../Text'; +import withWindowDimensions, {windowDimensionsPropTypes} from '../../withWindowDimensions'; +import withLocalize, {withLocalizePropTypes} from '../../withLocalize'; +import compose from '../../../libs/compose'; +import getOperatingSystem from '../../../libs/getOperatingSystem'; +import * as User from '../../../libs/actions/User'; +import EmojiSkinToneList from '../EmojiSkinToneList'; +import * as EmojiUtils from '../../../libs/EmojiUtils'; +import CategoryShortcutBar from '../CategoryShortcutBar'; +import TextInput from '../../TextInput'; +import isEnterWhileComposition from '../../../libs/KeyboardShortcut/isEnterWhileComposition'; +import canFocusInputOnScreenFocus from '../../../libs/canFocusInputOnScreenFocus'; +import {TextInput as RNTextInput} from 'react-native'; +import _ from 'lodash'; + +/* ISSUES +TODO: NO TS from Onyx? +*/ +const propTypes = { + /** Function to add the selected emoji to the main compose text input */ + onEmojiSelected: PropTypes.func.isRequired, + + /** The ref to the search input (may be null on small screen widths) */ + forwardedRef: PropTypes.func, + + /** Stores user's preferred skin tone */ + preferredSkinTone: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + + /** Stores user's frequently used emojis */ + // eslint-disable-next-line react/forbid-prop-types + frequentlyUsedEmojis: PropTypes.arrayOf(PropTypes.object), + + /** Props related to the dimensions of the window */ + ...windowDimensionsPropTypes, + + ...withLocalizePropTypes, +}; + +const defaultProps = { + forwardedRef: () => {}, + preferredSkinTone: CONST.EMOJI_DEFAULT_SKIN_TONE, + frequentlyUsedEmojis: [], +}; + +const EmojiPickerMenu: FC = (props) => { + const {forwardedRef, frequentlyUsedEmojis, preferredSkinTone, onEmojiSelected, preferredLocale, isSmallScreenWidth, windowWidth, windowHeight, translate} = props; + + // Ref for the emoji search input + const searchInput = useRef(null); // TODO: is RNTextInput correct? + + // Ref for emoji FlatList + const emojiList = useRef(null); + + // We want consistent auto focus behavior on input between native and mWeb so we have some auto focus management code that will + // prevent auto focus when open picker for mobile device + const shouldFocusInputOnScreenFocus = canFocusInputOnScreenFocus(); + + const firstNonHeaderIndex = useRef(0); + + const headerEmojis = useRef([]); // TODO: find type HeaderEmoji + if (headerEmojis.current.length === 0) { + headerEmojis.current = getEmojisAndHeaderRowIndices().headerRowIndices; + } + + const [filteredEmojis, setFilteredEmojis] = useState(() => getEmojisAndHeaderRowIndices().filteredEmojis); + const [headerIndices, setHeaderIndices] = useState(() => getEmojisAndHeaderRowIndices().headerRowIndices); + this.state = { + highlightedIndex: -1, + arePointerEventsDisabled: false, + selection: { + start: 0, + end: 0, + }, + isFocused: false, + isUsingKeyboardMovement: false, + }; + + function componentDidMount() { + // This callback prop is used by the parent component using the constructor to + // get a ref to the inner textInput element e.g. if we do + // this.textInput = el} /> this will not + // return a ref to the component, but rather the HTML element by default + if (shouldFocusInputOnScreenFocus && forwardedRef && _.isFunction(forwardedRef)) { + forwardedRef(searchInput.current); + } + setupEventHandlers(); + setFirstNonHeaderIndex(filteredEmojis); + } + + function componentDidUpdate(prevProps) { + if (prevProps.frequentlyUsedEmojis === this.props.frequentlyUsedEmojis) { + return; + } + + const emojisAndHeaderRowIndices = getEmojisAndHeaderRowIndices(); + + headerEmojis.current = emojisAndHeaderRowIndices.headerEmojis; + + setFilteredEmojis(emojisAndHeaderRowIndices.filteredEmojis); + setHeaderIndices(emojisAndHeaderRowIndices.headerRowIndices); + } + + function componentWillUnmount() { + cleanupEventHandlers(); + } + + /** + * On text input selection change + * + * @param {Event} event + */ + function onSelectionChange(event: NativeSyntheticEvent) { + this.setState({selection: event.nativeEvent.selection}); + } + + /** + * Calculate the filtered + header emojis and header row indices + * @returns {Object} + */ + function getEmojisAndHeaderRowIndices() { + // If we're on Windows, don't display the flag emojis (the last category), + // since Windows doesn't support them + const flagHeaderIndex = _.findIndex(emojis, (emoji) => emoji.header && emoji.code === 'flags'); + const filteredEmojis = + getOperatingSystem() === CONST.OS.WINDOWS + ? EmojiUtils.mergeEmojisWithFrequentlyUsedEmojis(emojis.slice(0, flagHeaderIndex)) + : EmojiUtils.mergeEmojisWithFrequentlyUsedEmojis(emojis); + + // Get the header emojis along with the code, index and icon. + // index is the actual header index starting at the first emoji and counting each one + const headerEmojis = EmojiUtils.getHeaderEmojis(filteredEmojis); + + // This is the indices of each header's Row + // The positions are static, and are calculated as index/numColumns (8 in our case) + // This is because each row of 8 emojis counts as one index to the flatlist + const headerRowIndices = _.map(headerEmojis, (headerEmoji) => Math.floor(headerEmoji.index / CONST.EMOJI_NUM_PER_ROW)); + + return {filteredEmojis, headerEmojis, headerRowIndices}; + } + + /** + * Find and store index of the first emoji item + * @param {Array} filteredEmojis + */ + function setFirstNonHeaderIndex(filteredEmojis: any[]) { + // TODO: array of what? + firstNonHeaderIndex.current = _.findIndex(filteredEmojis, (item) => !item.spacer && !item.header); + } + + /** + * Setup and attach keypress/mouse handlers for highlight navigation. + */ + function setupEventHandlers() { + if (!document) { + return; + } + + this.keyDownHandler = (keyBoardEvent: KeyboardEvent) => { + if (keyBoardEvent.key.startsWith('Arrow')) { + if (!this.state.isFocused || keyBoardEvent.key === 'ArrowUp' || keyBoardEvent.key === 'ArrowDown') { + keyBoardEvent.preventDefault(); + } + + // Move the highlight when arrow keys are pressed + highlightAdjacentEmoji(keyBoardEvent.key); + return; + } + + // Select the currently highlighted emoji if enter is pressed + if (!isEnterWhileComposition(keyBoardEvent) && keyBoardEvent.key === CONST.KEYBOARD_SHORTCUTS.ENTER.shortcutKey && this.state.highlightedIndex !== -1) { + const item = filteredEmojis[this.state.highlightedIndex]; + if (!item) { + return; + } + const emoji = lodashGet(item, ['types', this.props.preferredSkinTone], item.code); + this.addToFrequentAndSelectEmoji(emoji, item); + return; + } + + // Enable keyboard movement if tab or enter is pressed or if shift is pressed while the input + // is not focused, so that the navigation and tab cycling can be done using the keyboard without + // interfering with the input behaviour. + if (keyBoardEvent.key === 'Tab' || keyBoardEvent.key === 'Enter' || (keyBoardEvent.key === 'Shift' && !searchInput.current?.isFocused())) { + this.setState({isUsingKeyboardMovement: true}); + return; + } + + // We allow typing in the search box if any key is pressed apart from Arrow keys. + if (!searchInput.current?.isFocused()) { + this.setState({selectTextOnFocus: false}); + searchInput.current.focus(); + + // Re-enable selection on the searchInput + this.setState({selectTextOnFocus: true}); + } + }; + + // Keyboard events are not bubbling on TextInput in RN-Web, Bubbling was needed for this event to trigger + // event handler attached to document root. To fix this, trigger event handler in Capture phase. + document.addEventListener('keydown', this.keyDownHandler, true); + + // Re-enable pointer events and hovering over EmojiPickerItems when the mouse moves + this.mouseMoveHandler = () => { + if (!this.state.arePointerEventsDisabled) { + return; + } + + this.setState({arePointerEventsDisabled: false}); + }; + document.addEventListener('mousemove', this.mouseMoveHandler); + } + + /** + * This function will be used with FlatList getItemLayout property for optimization purpose that allows skipping + * the measurement of dynamic content if we know the size (height or width) of items ahead of time. + * Generate and return an object with properties length(height of each individual row), + * offset(distance of the current row from the top of the FlatList), index(current row index) + * + * @param {*} data FlatList item + * @param {Number} index row index + * @returns {Object} + */ + function getItemLayout(data: {}, index: number) { + // TODO: TYPES of data?? also data isnt used here. Maybe remove it + return {length: CONST.EMOJI_PICKER_ITEM_HEIGHT, offset: CONST.EMOJI_PICKER_ITEM_HEIGHT * index, index}; + } + + /** + * Cleanup all mouse/keydown event listeners that we've set up + */ + function cleanupEventHandlers() { + document?.removeEventListener('keydown', this.keyDownHandler, true); + document?.removeEventListener('mousemove', this.mouseMoveHandler); + } + + /** + * @param {String} emoji + * @param {Object} emojiObject + */ + function addToFrequentAndSelectEmoji(emoji, emojiObject) { + const frequentEmojiList = EmojiUtils.getFrequentlyUsedEmojis(emojiObject); + User.updateFrequentlyUsedEmojis(frequentEmojiList); + this.props.onEmojiSelected(emoji, emojiObject); + } + + /** + * Focuses the search Input and has the text selected + */ + function focusInputWithTextSelect() { + if (!searchInput.current) { + return; + } + + this.setState({selectTextOnFocus: true}); + searchInput.current.focus(); + } + + /** + * Highlights emojis adjacent to the currently highlighted emoji depending on the arrowKey + * @param {String} arrowKey + */ + function highlightAdjacentEmoji(arrowKey: KeyboardEvent['key']) { + if (filteredEmojis.length === 0) { + return; + } + + // Arrow Down and Arrow Right enable arrow navigation when search is focused + if (searchInput.current?.isFocused()) { + if (arrowKey !== 'ArrowDown' && arrowKey !== 'ArrowRight') { + return; + } + + if (arrowKey === 'ArrowRight' && !(searchInput.current.value.length === this.state.selection.start && this.state.selection.start === this.state.selection.end)) { + return; + } + + // Blur the input, change the highlight type to keyboard, and disable pointer events + searchInput.current.blur(); + this.setState({isUsingKeyboardMovement: true, arePointerEventsDisabled: true}); + + // We only want to hightlight the Emoji if none was highlighted already + // If we already have a highlighted Emoji, lets just skip the first navigation + if (this.state.highlightedIndex !== -1) { + return; + } + } + + // If nothing is highlighted and an arrow key is pressed + // select the first emoji, apply keyboard movement styles, and disable pointer events + if (this.state.highlightedIndex === -1) { + this.setState({highlightedIndex: firstNonHeaderIndex.current, isUsingKeyboardMovement: true, arePointerEventsDisabled: true}); + return; + } + + let newIndex = this.state.highlightedIndex; + const move = (steps, boundsCheck, onBoundReached = () => {}) => { + if (boundsCheck()) { + onBoundReached(); + return; + } + + // Move in the prescribed direction until we reach an element that isn't a header + const isHeader = (e) => e.header || e.spacer; + do { + newIndex += steps; + if (newIndex < 0) { + break; + } + } while (isHeader(filteredEmojis[newIndex])); + }; + + switch (arrowKey) { + case 'ArrowDown': + move(CONST.EMOJI_NUM_PER_ROW, () => this.state.highlightedIndex + CONST.EMOJI_NUM_PER_ROW > filteredEmojis.length - 1); + break; + case 'ArrowLeft': + move( + -1, + () => this.state.highlightedIndex - 1 < firstNonHeaderIndex.current, + () => { + // Reaching start of the list, arrow left set the focus to searchInput. + this.focusInputWithTextSelect(); + newIndex = -1; + }, + ); + break; + case 'ArrowRight': + move(1, () => this.state.highlightedIndex + 1 > filteredEmojis.length - 1); + break; + case 'ArrowUp': + move( + -CONST.EMOJI_NUM_PER_ROW, + () => this.state.highlightedIndex - CONST.EMOJI_NUM_PER_ROW < firstNonHeaderIndex.current, + () => { + // Reaching start of the list, arrow up set the focus to searchInput. + this.focusInputWithTextSelect(); + newIndex = -1; + }, + ); + break; + default: + break; + } + + // Actually highlight the new emoji, apply keyboard movement styles, and disable pointer events + if (newIndex !== this.state.highlightedIndex) { + this.setState({highlightedIndex: newIndex, isUsingKeyboardMovement: true, arePointerEventsDisabled: true}); + } + } + + function scrollToHeader(headerIndex: number) { + const calculatedOffset = Math.floor(headerIndex / CONST.EMOJI_NUM_PER_ROW) * CONST.EMOJI_PICKER_HEADER_HEIGHT; + emojiList.current?.flashScrollIndicators(); + emojiList.current?.scrollToOffset({offset: calculatedOffset, animated: true}); + } + + /** + * Filter the entire list of emojis to only emojis that have the search term in their keywords + * + * @param {String} searchTerm + */ + const filterEmojis = useCallback( + _.debounce((searchTerm: string) => { + const normalizedSearchTerm = searchTerm.toLowerCase().trim().replaceAll(':', ''); + emojiList.current?.scrollToOffset({offset: 0, animated: false}); + + if (normalizedSearchTerm === '') { + // There are no headers when searching, so we need to re-make them sticky when there is no search term + setFilteredEmojis(filteredEmojis); // TODO: is this necessary? + setHeaderIndices(headerIndices); // TODO: is this necessary? + this.setState({ + highlightedIndex: -1, + }); + setFirstNonHeaderIndex(filteredEmojis); + return; + } + const newFilteredEmojiList = EmojiUtils.suggestEmojis(`:${normalizedSearchTerm}`, this.props.preferredLocale, filteredEmojis.length); + // Remove sticky header indices. There are no headers while searching and we don't want to make emojis sticky + setFilteredEmojis(newFilteredEmojiList); + setHeaderIndices([]); + this.setState({highlightedIndex: 0}); + setFirstNonHeaderIndex(newFilteredEmojiList); + }, 300), + [filteredEmojis, headerIndices], + ); + + /** + * Check if its a landscape mode of mobile device + * + * @returns {Boolean} + */ + function isMobileLandscape() { + // TODO: find Why its unused + return this.props.isSmallScreenWidth && this.props.windowWidth >= this.props.windowHeight; + } + + /** + * @param {Number} skinTone + */ + function updatePreferredSkinTone(skinTone: number) { + if (this.props.preferredSkinTone === skinTone) { + return; + } + + User.updatePreferredSkinTone(skinTone.toString()); + } + + /** + * Return a unique key for each emoji item + * + * @param {Object} item + * @param {Number} index + * @returns {String} + */ + function keyExtractor(item, index) { + return `emoji_picker_${item.code}_${index}`; + } + + /** + * Given an emoji item object, render a component based on its type. + * Items with the code "SPACER" return nothing and are used to fill rows up to 8 + * so that the sticky headers function properly. + * + * @param {Object} item // TODO: Find Type + * @param {Number} index + * @returns {*} + */ + function renderItem({item, index}: {item: {}; index: number}) { + const {code, header, types} = item; + if (item.spacer) { + return null; + } + + if (header) { + return ( + + {this.props.translate(`emojiPicker.headers.${code}`)} + + ); + } + + const emojiCode = types && types[this.props.preferredSkinTone] ? types[this.props.preferredSkinTone] : code; + + const isEmojiFocused = index === this.state.highlightedIndex && this.state.isUsingKeyboardMovement; + + return ( + this.addToFrequentAndSelectEmoji(emoji, item)} + onHoverIn={() => this.setState({highlightedIndex: index, isUsingKeyboardMovement: false})} + onHoverOut={() => { + if (this.state.arePointerEventsDisabled) { + return; + } + this.setState({highlightedIndex: -1}); + }} + emoji={emojiCode} + onFocus={() => this.setState({highlightedIndex: index})} + onBlur={() => + this.setState((prevState) => ({ + // Only clear the highlighted index if the highlighted index is the same, + // meaning that the focus changed to an element that is not an emoji item. + highlightedIndex: prevState.highlightedIndex === index ? -1 : prevState.highlightedIndex, + })) + } + isFocused={isEmojiFocused} + isHighlighted={index === this.state.highlightedIndex} + isUsingKeyboardMovement={this.state.isUsingKeyboardMovement} + /> + ); + } + + const isFiltered = filteredEmojis.current.length !== this.state.filteredEmojis.length; + const listStyle = StyleUtils.getEmojiPickerListHeight(isFiltered, this.props.windowHeight); + const height = !listStyle.maxHeight || listStyle.height < listStyle.maxHeight ? listStyle.height : listStyle.maxHeight; + const overflowLimit = Math.floor(height / CONST.EMOJI_PICKER_ITEM_HEIGHT) * 8; + return ( + + + this.setState({isFocused: true, highlightedIndex: -1, isUsingKeyboardMovement: false})} + onBlur={() => this.setState({isFocused: false})} + autoCorrect={false} + blurOnSubmit={this.state.filteredEmojis.length > 0} + /> + + {!isFiltered && ( + + )} + + ); +}; + +EmojiPickerMenu.propTypes = propTypes; +EmojiPickerMenu.defaultProps = defaultProps; + +export default compose( + withWindowDimensions, + withLocalize, + withOnyx({ + preferredSkinTone: { + key: ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE, + }, + frequentlyUsedEmojis: { + key: ONYXKEYS.FREQUENTLY_USED_EMOJIS, + }, + }), +)( + React.forwardRef((props, ref) => ( + + )), +); From 7ee44e4ec78b81a05b6cad1d11fc9904d196688c Mon Sep 17 00:00:00 2001 From: Ashutosh Khanduala Date: Sat, 7 Oct 2023 18:55:53 +0530 Subject: [PATCH 20/45] p#17: isUsingKeyboardMovement state Signed-off-by: Ashutosh Khanduala --- .../EmojiPickerMenu/EmojiPickerMenu copy.tsx | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx b/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx index e7c1c8751625..95272049c26d 100755 --- a/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx +++ b/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx @@ -86,9 +86,7 @@ const EmojiPickerMenu = (props) => { const [arePointerEventsDisabled, setArePointerEventsDisabled] = useState(false); const [selection, setSelection] = useState({start: 0, end: 0}); const [isFocused, setIsFocused] = useState(false); - this.state = { - isUsingKeyboardMovement: false, - }; + const [isUsingKeyboardMovement, setIsUsingKeyboardMovement] = useState(false); function componentDidMount() { // This callback prop is used by the parent component using the constructor to @@ -196,7 +194,7 @@ const EmojiPickerMenu = (props) => { // is not focused, so that the navigation and tab cycling can be done using the keyboard without // interfering with the input behaviour. if (keyBoardEvent.key === 'Tab' || keyBoardEvent.key === 'Enter' || (keyBoardEvent.key === 'Shift' && !searchInputRef.current?.isFocused())) { - this.setState({isUsingKeyboardMovement: true}); + setIsUsingKeyboardMovement(true); return; } @@ -291,7 +289,7 @@ const EmojiPickerMenu = (props) => { // Blur the input, change the highlight type to keyboard, and disable pointer events searchInputRef.current.blur(); setArePointerEventsDisabled(true); - this.setState({isUsingKeyboardMovement: true}); + setIsUsingKeyboardMovement(true); // We only want to hightlight the Emoji if none was highlighted already // If we already have a highlighted Emoji, lets just skip the first navigation @@ -305,7 +303,7 @@ const EmojiPickerMenu = (props) => { if (highlightedIndex === -1) { setHighlightedIndex(firstNonHeaderIndex.current); setArePointerEventsDisabled(true); - this.setState({isUsingKeyboardMovement: true}); + setIsUsingKeyboardMovement(true); return; } @@ -363,7 +361,7 @@ const EmojiPickerMenu = (props) => { if (newIndex !== highlightedIndex) { setHighlightedIndex(newIndex); setArePointerEventsDisabled(true); - this.setState({isUsingKeyboardMovement: true}); + setIsUsingKeyboardMovement(true); } } @@ -461,14 +459,14 @@ const EmojiPickerMenu = (props) => { const emojiCode = types && types[preferredSkinTone] ? types[preferredSkinTone] : code; - const isEmojiFocused = index === highlightedIndex && this.state.isUsingKeyboardMovement; + const isEmojiFocused = index === highlightedIndex && isUsingKeyboardMovement; return ( this.addToFrequentAndSelectEmoji(emoji, item)} onHoverIn={() => { setHighlightedIndex(index); - this.setState({isUsingKeyboardMovement: false}); + setIsUsingKeyboardMovement(false); }} onHoverOut={() => { if (arePointerEventsDisabled) { @@ -485,7 +483,7 @@ const EmojiPickerMenu = (props) => { } isFocused={isEmojiFocused} isHighlighted={index === highlightedIndex} - isUsingKeyboardMovement={this.state.isUsingKeyboardMovement} + isUsingKeyboardMovement={isUsingKeyboardMovement} /> ); } @@ -514,7 +512,7 @@ const EmojiPickerMenu = (props) => { onFocus={() => { setHighlightedIndex(-1); setIsFocused(true); - this.setState({isUsingKeyboardMovement: false}); + setIsUsingKeyboardMovement(false); }} onBlur={() => setIsFocused(false)} autoCorrect={false} From d95a5ec4eb16c390bf28431498616b1a9a70fd1e Mon Sep 17 00:00:00 2001 From: Ashutosh Khanduala Date: Sat, 7 Oct 2023 18:58:23 +0530 Subject: [PATCH 21/45] p#18: convert missed this.state.filteredEmojis Signed-off-by: Ashutosh Khanduala --- .../EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx b/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx index 95272049c26d..499f70704089 100755 --- a/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx +++ b/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx @@ -181,7 +181,7 @@ const EmojiPickerMenu = (props) => { // Select the currently highlighted emoji if enter is pressed if (!isEnterWhileComposition(keyBoardEvent) && keyBoardEvent.key === CONST.KEYBOARD_SHORTCUTS.ENTER.shortcutKey && highlightedIndex !== -1) { - const item = this.state.filteredEmojis[highlightedIndex]; + const item = filteredEmojis[highlightedIndex]; if (!item) { return; } @@ -321,7 +321,7 @@ const EmojiPickerMenu = (props) => { if (newIndex < 0) { break; } - } while (isHeader(this.state.filteredEmojis[newIndex])); + } while (isHeader(filteredEmojis[newIndex])); }; switch (arrowKey) { @@ -527,7 +527,7 @@ const EmojiPickerMenu = (props) => { )} { // Set scrollPaddingTop to consider sticky headers while scrolling {scrollPaddingTop: isFiltered ? 0 : CONST.EMOJI_PICKER_ITEM_HEIGHT}, ]} - extraData={[this.state.filteredEmojis, highlightedIndex, preferredSkinTone]} + extraData={[filteredEmojis, highlightedIndex, preferredSkinTone]} stickyHeaderIndices={headerIndices} getItemLayout={getItemLayout} contentContainerStyle={styles.flexGrow1} From 76b0fa90a1ce8f1f0031f2be731070dcc87c4a59 Mon Sep 17 00:00:00 2001 From: Ashutosh Khanduala Date: Sat, 7 Oct 2023 19:02:57 +0530 Subject: [PATCH 22/45] p#19: selectTextOnFocus state Signed-off-by: Ashutosh Khanduala --- .../EmojiPickerMenu/EmojiPickerMenu copy.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx b/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx index 499f70704089..1cd92c6001cd 100755 --- a/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx +++ b/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx @@ -87,7 +87,7 @@ const EmojiPickerMenu = (props) => { const [selection, setSelection] = useState({start: 0, end: 0}); const [isFocused, setIsFocused] = useState(false); const [isUsingKeyboardMovement, setIsUsingKeyboardMovement] = useState(false); - + const [selectTextOnFocus, setSelectTextOnFocus] = useState(false) function componentDidMount() { // This callback prop is used by the parent component using the constructor to // get a ref to the inner textInput element e.g. if we do @@ -200,11 +200,11 @@ const EmojiPickerMenu = (props) => { // We allow typing in the search box if any key is pressed apart from Arrow keys. if (!searchInputRef.current?.isFocused()) { - this.setState({selectTextOnFocus: false}); - searchInputRef.current.focus(); + setSelectTextOnFocus(false) + searchInputRef.current?.focus(); // Re-enable selection on the searchInput - this.setState({selectTextOnFocus: true}); + setSelectTextOnFocus(true); } }; @@ -263,7 +263,7 @@ const EmojiPickerMenu = (props) => { return; } - this.setState({selectTextOnFocus: true}); + setSelectTextOnFocus(true); searchInputRef.current.focus(); } @@ -507,7 +507,7 @@ const EmojiPickerMenu = (props) => { defaultValue="" ref={searchInputRef} autoFocus={shouldFocusInputOnScreenFocus} - selectTextOnFocus={this.state.selectTextOnFocus} + selectTextOnFocus={selectTextOnFocus} onSelectionChange={onSelectionChange} onFocus={() => { setHighlightedIndex(-1); From c8fab62e9fb41aab2663476200877a29040b3cae Mon Sep 17 00:00:00 2001 From: Ashutosh Khanduala Date: Sat, 7 Oct 2023 19:16:01 +0530 Subject: [PATCH 23/45] p#20: keyDownHandler() + keyExtractor() Signed-off-by: Ashutosh Khanduala --- .../EmojiPickerMenu/EmojiPickerMenu copy.tsx | 87 ++++++++++--------- 1 file changed, 44 insertions(+), 43 deletions(-) diff --git a/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx b/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx index 1cd92c6001cd..17378c9989ec 100755 --- a/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx +++ b/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx @@ -87,7 +87,7 @@ const EmojiPickerMenu = (props) => { const [selection, setSelection] = useState({start: 0, end: 0}); const [isFocused, setIsFocused] = useState(false); const [isUsingKeyboardMovement, setIsUsingKeyboardMovement] = useState(false); - const [selectTextOnFocus, setSelectTextOnFocus] = useState(false) + const [selectTextOnFocus, setSelectTextOnFocus] = useState(false); function componentDidMount() { // This callback prop is used by the parent component using the constructor to // get a ref to the inner textInput element e.g. if we do @@ -160,57 +160,57 @@ const EmojiPickerMenu = (props) => { firstNonHeaderIndex.current = _.findIndex(filteredEmojis, (item) => !item.spacer && !item.header); } - /** - * Setup and attach keypress/mouse handlers for highlight navigation. - */ - function setupEventHandlers() { - if (!document) { + const keyDownHandler = (keyBoardEvent: KeyboardEvent) => { + if (keyBoardEvent.key.startsWith('Arrow')) { + if (!isFocused || keyBoardEvent.key === 'ArrowUp' || keyBoardEvent.key === 'ArrowDown') { + keyBoardEvent.preventDefault(); + } + + // Move the highlight when arrow keys are pressed + highlightAdjacentEmoji(keyBoardEvent.key); return; } - this.keyDownHandler = (keyBoardEvent) => { - if (keyBoardEvent.key.startsWith('Arrow')) { - if (!isFocused || keyBoardEvent.key === 'ArrowUp' || keyBoardEvent.key === 'ArrowDown') { - keyBoardEvent.preventDefault(); - } - - // Move the highlight when arrow keys are pressed - highlightAdjacentEmoji(keyBoardEvent.key); + // Select the currently highlighted emoji if enter is pressed + if (!isEnterWhileComposition(keyBoardEvent) && keyBoardEvent.key === CONST.KEYBOARD_SHORTCUTS.ENTER.shortcutKey && highlightedIndex !== -1) { + const item = filteredEmojis[highlightedIndex]; + if (!item) { return; } + const emoji = lodashGet(item, ['types', preferredSkinTone], item.code); + this.addToFrequentAndSelectEmoji(emoji, item); + return; + } - // Select the currently highlighted emoji if enter is pressed - if (!isEnterWhileComposition(keyBoardEvent) && keyBoardEvent.key === CONST.KEYBOARD_SHORTCUTS.ENTER.shortcutKey && highlightedIndex !== -1) { - const item = filteredEmojis[highlightedIndex]; - if (!item) { - return; - } - const emoji = lodashGet(item, ['types', preferredSkinTone], item.code); - this.addToFrequentAndSelectEmoji(emoji, item); - return; - } + // Enable keyboard movement if tab or enter is pressed or if shift is pressed while the input + // is not focused, so that the navigation and tab cycling can be done using the keyboard without + // interfering with the input behaviour. + if (keyBoardEvent.key === 'Tab' || keyBoardEvent.key === 'Enter' || (keyBoardEvent.key === 'Shift' && !searchInputRef.current?.isFocused())) { + setIsUsingKeyboardMovement(true); + return; + } - // Enable keyboard movement if tab or enter is pressed or if shift is pressed while the input - // is not focused, so that the navigation and tab cycling can be done using the keyboard without - // interfering with the input behaviour. - if (keyBoardEvent.key === 'Tab' || keyBoardEvent.key === 'Enter' || (keyBoardEvent.key === 'Shift' && !searchInputRef.current?.isFocused())) { - setIsUsingKeyboardMovement(true); - return; - } + // We allow typing in the search box if any key is pressed apart from Arrow keys. + if (!searchInputRef.current?.isFocused()) { + setSelectTextOnFocus(false); + searchInputRef.current?.focus(); - // We allow typing in the search box if any key is pressed apart from Arrow keys. - if (!searchInputRef.current?.isFocused()) { - setSelectTextOnFocus(false) - searchInputRef.current?.focus(); + // Re-enable selection on the searchInput + setSelectTextOnFocus(true); + } + }; - // Re-enable selection on the searchInput - setSelectTextOnFocus(true); - } - }; + /** + * Setup and attach keypress/mouse handlers for highlight navigation. + */ + function setupEventHandlers() { + if (!document) { + return; + } // Keyboard events are not bubbling on TextInput in RN-Web, Bubbling was needed for this event to trigger // event handler attached to document root. To fix this, trigger event handler in Capture phase. - document.addEventListener('keydown', this.keyDownHandler, true); + document.addEventListener('keydown', keyDownHandler, true); // Re-enable pointer events and hovering over EmojiPickerItems when the mouse moves this.mouseMoveHandler = () => { @@ -241,7 +241,7 @@ const EmojiPickerMenu = (props) => { * Cleanup all mouse/keydown event listeners that we've set up */ function cleanupEventHandlers() { - document?.removeEventListener('keydown', this.keyDownHandler, true); + document?.removeEventListener('keydown', keyDownHandler, true); document?.removeEventListener('mousemove', this.mouseMoveHandler); } @@ -429,7 +429,8 @@ const EmojiPickerMenu = (props) => { * @param {Number} index * @returns {String} */ - function keyExtractor(item, index) { + function keyExtractor(item, index: number) { + // TODO: find type of item return `emoji_picker_${item.code}_${index}`; } @@ -529,7 +530,7 @@ const EmojiPickerMenu = (props) => { ref={emojiListRef} data={filteredEmojis} renderItem={renderItem} - keyExtractor={this.keyExtractor} + keyExtractor={keyExtractor} numColumns={CONST.EMOJI_NUM_PER_ROW} style={[ listStyle, From 4ddcfaa566716e47b4f3f76536453792b80527ab Mon Sep 17 00:00:00 2001 From: Ashutosh Khanduala Date: Sat, 7 Oct 2023 19:17:52 +0530 Subject: [PATCH 24/45] p#20: mouseMoveHandler() Signed-off-by: Ashutosh Khanduala --- .../EmojiPickerMenu/EmojiPickerMenu copy.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx b/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx index 17378c9989ec..06065129b6df 100755 --- a/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx +++ b/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx @@ -199,6 +199,12 @@ const EmojiPickerMenu = (props) => { setSelectTextOnFocus(true); } }; + const mouseMoveHandler = () => { + if (!arePointerEventsDisabled) { + return; + } + setArePointerEventsDisabled(false); + }; /** * Setup and attach keypress/mouse handlers for highlight navigation. @@ -213,13 +219,7 @@ const EmojiPickerMenu = (props) => { document.addEventListener('keydown', keyDownHandler, true); // Re-enable pointer events and hovering over EmojiPickerItems when the mouse moves - this.mouseMoveHandler = () => { - if (!arePointerEventsDisabled) { - return; - } - setArePointerEventsDisabled(false); - }; - document.addEventListener('mousemove', this.mouseMoveHandler); + document.addEventListener('mousemove', mouseMoveHandler); } /** @@ -242,7 +242,7 @@ const EmojiPickerMenu = (props) => { */ function cleanupEventHandlers() { document?.removeEventListener('keydown', keyDownHandler, true); - document?.removeEventListener('mousemove', this.mouseMoveHandler); + document?.removeEventListener('mousemove', mouseMoveHandler); } /** From c684aee25e39a8af35377f866fd0dad6d913b9e4 Mon Sep 17 00:00:00 2001 From: Ashutosh Khanduala Date: Sat, 7 Oct 2023 19:22:09 +0530 Subject: [PATCH 25/45] p#21: focusInputWithTextSelect() + addToFrequentAndSelectEmoji() Signed-off-by: Ashutosh Khanduala --- .../EmojiPickerMenu/EmojiPickerMenu copy.tsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx b/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx index 06065129b6df..4691be28ee91 100755 --- a/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx +++ b/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx @@ -178,7 +178,7 @@ const EmojiPickerMenu = (props) => { return; } const emoji = lodashGet(item, ['types', preferredSkinTone], item.code); - this.addToFrequentAndSelectEmoji(emoji, item); + addToFrequentAndSelectEmoji(emoji, item); return; } @@ -249,7 +249,8 @@ const EmojiPickerMenu = (props) => { * @param {String} emoji * @param {Object} emojiObject */ - function addToFrequentAndSelectEmoji(emoji, emojiObject) { + function addToFrequentAndSelectEmoji(emoji: string, emojiObject: {}) { + // TODO: type of emojiObject const frequentEmojiList = EmojiUtils.getFrequentlyUsedEmojis(emojiObject); User.updateFrequentlyUsedEmojis(frequentEmojiList); onEmojiSelected(emoji, emojiObject); @@ -334,7 +335,7 @@ const EmojiPickerMenu = (props) => { () => highlightedIndex - 1 < firstNonHeaderIndex.current, () => { // Reaching start of the list, arrow left set the focus to searchInput. - this.focusInputWithTextSelect(); + focusInputWithTextSelect(); newIndex = -1; }, ); @@ -348,7 +349,7 @@ const EmojiPickerMenu = (props) => { () => highlightedIndex - CONST.EMOJI_NUM_PER_ROW < firstNonHeaderIndex.current, () => { // Reaching start of the list, arrow up set the focus to searchInput. - this.focusInputWithTextSelect(); + focusInputWithTextSelect(); newIndex = -1; }, ); @@ -464,7 +465,7 @@ const EmojiPickerMenu = (props) => { return ( this.addToFrequentAndSelectEmoji(emoji, item)} + onPress={(emoji) => addToFrequentAndSelectEmoji(emoji, item)} onHoverIn={() => { setHighlightedIndex(index); setIsUsingKeyboardMovement(false); From 24c9681d548838f332a2afbfdbd594ba16fa849f Mon Sep 17 00:00:00 2001 From: Ashutosh Khanduala Date: Sat, 7 Oct 2023 19:27:25 +0530 Subject: [PATCH 26/45] p#21: cleanup Signed-off-by: Ashutosh Khanduala --- .../EmojiPickerMenu/EmojiPickerMenu copy.tsx | 2 +- .../EmojiPicker/EmojiPickerMenu/ref.tsx | 568 ------------------ 2 files changed, 1 insertion(+), 569 deletions(-) delete mode 100644 src/components/EmojiPicker/EmojiPickerMenu/ref.tsx diff --git a/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx b/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx index 4691be28ee91..9bbdb58039b1 100755 --- a/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx +++ b/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx @@ -440,7 +440,7 @@ const EmojiPickerMenu = (props) => { * Items with the code "SPACER" return nothing and are used to fill rows up to 8 * so that the sticky headers function properly. * - * @param {Object} item + * @param {Object} item // TODO: Find Type * @param {Number} index * @returns {*} */ diff --git a/src/components/EmojiPicker/EmojiPickerMenu/ref.tsx b/src/components/EmojiPicker/EmojiPickerMenu/ref.tsx deleted file mode 100644 index b3e54c1e037b..000000000000 --- a/src/components/EmojiPicker/EmojiPickerMenu/ref.tsx +++ /dev/null @@ -1,568 +0,0 @@ -import React, {Component, FC, useCallback, useRef, useState} from 'react'; -import {View, FlatList, NativeSyntheticEvent, TextInputSelectionChangeEventData} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; -import PropTypes from 'prop-types'; -import _ from 'underscore'; -import lodashGet from 'lodash/get'; -import CONST from '../../../CONST'; -import ONYXKEYS from '../../../ONYXKEYS'; -import styles from '../../../styles/styles'; -import * as StyleUtils from '../../../styles/StyleUtils'; -import emojis from '../../../../assets/emojis'; -import EmojiPickerMenuItem from '../EmojiPickerMenuItem'; -import Text from '../../Text'; -import withWindowDimensions, {windowDimensionsPropTypes} from '../../withWindowDimensions'; -import withLocalize, {withLocalizePropTypes} from '../../withLocalize'; -import compose from '../../../libs/compose'; -import getOperatingSystem from '../../../libs/getOperatingSystem'; -import * as User from '../../../libs/actions/User'; -import EmojiSkinToneList from '../EmojiSkinToneList'; -import * as EmojiUtils from '../../../libs/EmojiUtils'; -import CategoryShortcutBar from '../CategoryShortcutBar'; -import TextInput from '../../TextInput'; -import isEnterWhileComposition from '../../../libs/KeyboardShortcut/isEnterWhileComposition'; -import canFocusInputOnScreenFocus from '../../../libs/canFocusInputOnScreenFocus'; -import {TextInput as RNTextInput} from 'react-native'; -import _ from 'lodash'; - -/* ISSUES -TODO: NO TS from Onyx? -*/ -const propTypes = { - /** Function to add the selected emoji to the main compose text input */ - onEmojiSelected: PropTypes.func.isRequired, - - /** The ref to the search input (may be null on small screen widths) */ - forwardedRef: PropTypes.func, - - /** Stores user's preferred skin tone */ - preferredSkinTone: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), - - /** Stores user's frequently used emojis */ - // eslint-disable-next-line react/forbid-prop-types - frequentlyUsedEmojis: PropTypes.arrayOf(PropTypes.object), - - /** Props related to the dimensions of the window */ - ...windowDimensionsPropTypes, - - ...withLocalizePropTypes, -}; - -const defaultProps = { - forwardedRef: () => {}, - preferredSkinTone: CONST.EMOJI_DEFAULT_SKIN_TONE, - frequentlyUsedEmojis: [], -}; - -const EmojiPickerMenu: FC = (props) => { - const {forwardedRef, frequentlyUsedEmojis, preferredSkinTone, onEmojiSelected, preferredLocale, isSmallScreenWidth, windowWidth, windowHeight, translate} = props; - - // Ref for the emoji search input - const searchInput = useRef(null); // TODO: is RNTextInput correct? - - // Ref for emoji FlatList - const emojiList = useRef(null); - - // We want consistent auto focus behavior on input between native and mWeb so we have some auto focus management code that will - // prevent auto focus when open picker for mobile device - const shouldFocusInputOnScreenFocus = canFocusInputOnScreenFocus(); - - const firstNonHeaderIndex = useRef(0); - - const headerEmojis = useRef([]); // TODO: find type HeaderEmoji - if (headerEmojis.current.length === 0) { - headerEmojis.current = getEmojisAndHeaderRowIndices().headerRowIndices; - } - - const [filteredEmojis, setFilteredEmojis] = useState(() => getEmojisAndHeaderRowIndices().filteredEmojis); - const [headerIndices, setHeaderIndices] = useState(() => getEmojisAndHeaderRowIndices().headerRowIndices); - this.state = { - highlightedIndex: -1, - arePointerEventsDisabled: false, - selection: { - start: 0, - end: 0, - }, - isFocused: false, - isUsingKeyboardMovement: false, - }; - - function componentDidMount() { - // This callback prop is used by the parent component using the constructor to - // get a ref to the inner textInput element e.g. if we do - // this.textInput = el} /> this will not - // return a ref to the component, but rather the HTML element by default - if (shouldFocusInputOnScreenFocus && forwardedRef && _.isFunction(forwardedRef)) { - forwardedRef(searchInput.current); - } - setupEventHandlers(); - setFirstNonHeaderIndex(filteredEmojis); - } - - function componentDidUpdate(prevProps) { - if (prevProps.frequentlyUsedEmojis === this.props.frequentlyUsedEmojis) { - return; - } - - const emojisAndHeaderRowIndices = getEmojisAndHeaderRowIndices(); - - headerEmojis.current = emojisAndHeaderRowIndices.headerEmojis; - - setFilteredEmojis(emojisAndHeaderRowIndices.filteredEmojis); - setHeaderIndices(emojisAndHeaderRowIndices.headerRowIndices); - } - - function componentWillUnmount() { - cleanupEventHandlers(); - } - - /** - * On text input selection change - * - * @param {Event} event - */ - function onSelectionChange(event: NativeSyntheticEvent) { - this.setState({selection: event.nativeEvent.selection}); - } - - /** - * Calculate the filtered + header emojis and header row indices - * @returns {Object} - */ - function getEmojisAndHeaderRowIndices() { - // If we're on Windows, don't display the flag emojis (the last category), - // since Windows doesn't support them - const flagHeaderIndex = _.findIndex(emojis, (emoji) => emoji.header && emoji.code === 'flags'); - const filteredEmojis = - getOperatingSystem() === CONST.OS.WINDOWS - ? EmojiUtils.mergeEmojisWithFrequentlyUsedEmojis(emojis.slice(0, flagHeaderIndex)) - : EmojiUtils.mergeEmojisWithFrequentlyUsedEmojis(emojis); - - // Get the header emojis along with the code, index and icon. - // index is the actual header index starting at the first emoji and counting each one - const headerEmojis = EmojiUtils.getHeaderEmojis(filteredEmojis); - - // This is the indices of each header's Row - // The positions are static, and are calculated as index/numColumns (8 in our case) - // This is because each row of 8 emojis counts as one index to the flatlist - const headerRowIndices = _.map(headerEmojis, (headerEmoji) => Math.floor(headerEmoji.index / CONST.EMOJI_NUM_PER_ROW)); - - return {filteredEmojis, headerEmojis, headerRowIndices}; - } - - /** - * Find and store index of the first emoji item - * @param {Array} filteredEmojis - */ - function setFirstNonHeaderIndex(filteredEmojis: any[]) { - // TODO: array of what? - firstNonHeaderIndex.current = _.findIndex(filteredEmojis, (item) => !item.spacer && !item.header); - } - - /** - * Setup and attach keypress/mouse handlers for highlight navigation. - */ - function setupEventHandlers() { - if (!document) { - return; - } - - this.keyDownHandler = (keyBoardEvent: KeyboardEvent) => { - if (keyBoardEvent.key.startsWith('Arrow')) { - if (!this.state.isFocused || keyBoardEvent.key === 'ArrowUp' || keyBoardEvent.key === 'ArrowDown') { - keyBoardEvent.preventDefault(); - } - - // Move the highlight when arrow keys are pressed - highlightAdjacentEmoji(keyBoardEvent.key); - return; - } - - // Select the currently highlighted emoji if enter is pressed - if (!isEnterWhileComposition(keyBoardEvent) && keyBoardEvent.key === CONST.KEYBOARD_SHORTCUTS.ENTER.shortcutKey && this.state.highlightedIndex !== -1) { - const item = filteredEmojis[this.state.highlightedIndex]; - if (!item) { - return; - } - const emoji = lodashGet(item, ['types', this.props.preferredSkinTone], item.code); - this.addToFrequentAndSelectEmoji(emoji, item); - return; - } - - // Enable keyboard movement if tab or enter is pressed or if shift is pressed while the input - // is not focused, so that the navigation and tab cycling can be done using the keyboard without - // interfering with the input behaviour. - if (keyBoardEvent.key === 'Tab' || keyBoardEvent.key === 'Enter' || (keyBoardEvent.key === 'Shift' && !searchInput.current?.isFocused())) { - this.setState({isUsingKeyboardMovement: true}); - return; - } - - // We allow typing in the search box if any key is pressed apart from Arrow keys. - if (!searchInput.current?.isFocused()) { - this.setState({selectTextOnFocus: false}); - searchInput.current.focus(); - - // Re-enable selection on the searchInput - this.setState({selectTextOnFocus: true}); - } - }; - - // Keyboard events are not bubbling on TextInput in RN-Web, Bubbling was needed for this event to trigger - // event handler attached to document root. To fix this, trigger event handler in Capture phase. - document.addEventListener('keydown', this.keyDownHandler, true); - - // Re-enable pointer events and hovering over EmojiPickerItems when the mouse moves - this.mouseMoveHandler = () => { - if (!this.state.arePointerEventsDisabled) { - return; - } - - this.setState({arePointerEventsDisabled: false}); - }; - document.addEventListener('mousemove', this.mouseMoveHandler); - } - - /** - * This function will be used with FlatList getItemLayout property for optimization purpose that allows skipping - * the measurement of dynamic content if we know the size (height or width) of items ahead of time. - * Generate and return an object with properties length(height of each individual row), - * offset(distance of the current row from the top of the FlatList), index(current row index) - * - * @param {*} data FlatList item - * @param {Number} index row index - * @returns {Object} - */ - function getItemLayout(data: {}, index: number) { - // TODO: TYPES of data?? also data isnt used here. Maybe remove it - return {length: CONST.EMOJI_PICKER_ITEM_HEIGHT, offset: CONST.EMOJI_PICKER_ITEM_HEIGHT * index, index}; - } - - /** - * Cleanup all mouse/keydown event listeners that we've set up - */ - function cleanupEventHandlers() { - document?.removeEventListener('keydown', this.keyDownHandler, true); - document?.removeEventListener('mousemove', this.mouseMoveHandler); - } - - /** - * @param {String} emoji - * @param {Object} emojiObject - */ - function addToFrequentAndSelectEmoji(emoji, emojiObject) { - const frequentEmojiList = EmojiUtils.getFrequentlyUsedEmojis(emojiObject); - User.updateFrequentlyUsedEmojis(frequentEmojiList); - this.props.onEmojiSelected(emoji, emojiObject); - } - - /** - * Focuses the search Input and has the text selected - */ - function focusInputWithTextSelect() { - if (!searchInput.current) { - return; - } - - this.setState({selectTextOnFocus: true}); - searchInput.current.focus(); - } - - /** - * Highlights emojis adjacent to the currently highlighted emoji depending on the arrowKey - * @param {String} arrowKey - */ - function highlightAdjacentEmoji(arrowKey: KeyboardEvent['key']) { - if (filteredEmojis.length === 0) { - return; - } - - // Arrow Down and Arrow Right enable arrow navigation when search is focused - if (searchInput.current?.isFocused()) { - if (arrowKey !== 'ArrowDown' && arrowKey !== 'ArrowRight') { - return; - } - - if (arrowKey === 'ArrowRight' && !(searchInput.current.value.length === this.state.selection.start && this.state.selection.start === this.state.selection.end)) { - return; - } - - // Blur the input, change the highlight type to keyboard, and disable pointer events - searchInput.current.blur(); - this.setState({isUsingKeyboardMovement: true, arePointerEventsDisabled: true}); - - // We only want to hightlight the Emoji if none was highlighted already - // If we already have a highlighted Emoji, lets just skip the first navigation - if (this.state.highlightedIndex !== -1) { - return; - } - } - - // If nothing is highlighted and an arrow key is pressed - // select the first emoji, apply keyboard movement styles, and disable pointer events - if (this.state.highlightedIndex === -1) { - this.setState({highlightedIndex: firstNonHeaderIndex.current, isUsingKeyboardMovement: true, arePointerEventsDisabled: true}); - return; - } - - let newIndex = this.state.highlightedIndex; - const move = (steps, boundsCheck, onBoundReached = () => {}) => { - if (boundsCheck()) { - onBoundReached(); - return; - } - - // Move in the prescribed direction until we reach an element that isn't a header - const isHeader = (e) => e.header || e.spacer; - do { - newIndex += steps; - if (newIndex < 0) { - break; - } - } while (isHeader(filteredEmojis[newIndex])); - }; - - switch (arrowKey) { - case 'ArrowDown': - move(CONST.EMOJI_NUM_PER_ROW, () => this.state.highlightedIndex + CONST.EMOJI_NUM_PER_ROW > filteredEmojis.length - 1); - break; - case 'ArrowLeft': - move( - -1, - () => this.state.highlightedIndex - 1 < firstNonHeaderIndex.current, - () => { - // Reaching start of the list, arrow left set the focus to searchInput. - this.focusInputWithTextSelect(); - newIndex = -1; - }, - ); - break; - case 'ArrowRight': - move(1, () => this.state.highlightedIndex + 1 > filteredEmojis.length - 1); - break; - case 'ArrowUp': - move( - -CONST.EMOJI_NUM_PER_ROW, - () => this.state.highlightedIndex - CONST.EMOJI_NUM_PER_ROW < firstNonHeaderIndex.current, - () => { - // Reaching start of the list, arrow up set the focus to searchInput. - this.focusInputWithTextSelect(); - newIndex = -1; - }, - ); - break; - default: - break; - } - - // Actually highlight the new emoji, apply keyboard movement styles, and disable pointer events - if (newIndex !== this.state.highlightedIndex) { - this.setState({highlightedIndex: newIndex, isUsingKeyboardMovement: true, arePointerEventsDisabled: true}); - } - } - - function scrollToHeader(headerIndex: number) { - const calculatedOffset = Math.floor(headerIndex / CONST.EMOJI_NUM_PER_ROW) * CONST.EMOJI_PICKER_HEADER_HEIGHT; - emojiList.current?.flashScrollIndicators(); - emojiList.current?.scrollToOffset({offset: calculatedOffset, animated: true}); - } - - /** - * Filter the entire list of emojis to only emojis that have the search term in their keywords - * - * @param {String} searchTerm - */ - const filterEmojis = useCallback( - _.debounce((searchTerm: string) => { - const normalizedSearchTerm = searchTerm.toLowerCase().trim().replaceAll(':', ''); - emojiList.current?.scrollToOffset({offset: 0, animated: false}); - - if (normalizedSearchTerm === '') { - // There are no headers when searching, so we need to re-make them sticky when there is no search term - setFilteredEmojis(filteredEmojis); // TODO: is this necessary? - setHeaderIndices(headerIndices); // TODO: is this necessary? - this.setState({ - highlightedIndex: -1, - }); - setFirstNonHeaderIndex(filteredEmojis); - return; - } - const newFilteredEmojiList = EmojiUtils.suggestEmojis(`:${normalizedSearchTerm}`, this.props.preferredLocale, filteredEmojis.length); - // Remove sticky header indices. There are no headers while searching and we don't want to make emojis sticky - setFilteredEmojis(newFilteredEmojiList); - setHeaderIndices([]); - this.setState({highlightedIndex: 0}); - setFirstNonHeaderIndex(newFilteredEmojiList); - }, 300), - [filteredEmojis, headerIndices], - ); - - /** - * Check if its a landscape mode of mobile device - * - * @returns {Boolean} - */ - function isMobileLandscape() { - // TODO: find Why its unused - return this.props.isSmallScreenWidth && this.props.windowWidth >= this.props.windowHeight; - } - - /** - * @param {Number} skinTone - */ - function updatePreferredSkinTone(skinTone: number) { - if (this.props.preferredSkinTone === skinTone) { - return; - } - - User.updatePreferredSkinTone(skinTone.toString()); - } - - /** - * Return a unique key for each emoji item - * - * @param {Object} item - * @param {Number} index - * @returns {String} - */ - function keyExtractor(item, index) { - return `emoji_picker_${item.code}_${index}`; - } - - /** - * Given an emoji item object, render a component based on its type. - * Items with the code "SPACER" return nothing and are used to fill rows up to 8 - * so that the sticky headers function properly. - * - * @param {Object} item // TODO: Find Type - * @param {Number} index - * @returns {*} - */ - function renderItem({item, index}: {item: {}; index: number}) { - const {code, header, types} = item; - if (item.spacer) { - return null; - } - - if (header) { - return ( - - {this.props.translate(`emojiPicker.headers.${code}`)} - - ); - } - - const emojiCode = types && types[this.props.preferredSkinTone] ? types[this.props.preferredSkinTone] : code; - - const isEmojiFocused = index === this.state.highlightedIndex && this.state.isUsingKeyboardMovement; - - return ( - this.addToFrequentAndSelectEmoji(emoji, item)} - onHoverIn={() => this.setState({highlightedIndex: index, isUsingKeyboardMovement: false})} - onHoverOut={() => { - if (this.state.arePointerEventsDisabled) { - return; - } - this.setState({highlightedIndex: -1}); - }} - emoji={emojiCode} - onFocus={() => this.setState({highlightedIndex: index})} - onBlur={() => - this.setState((prevState) => ({ - // Only clear the highlighted index if the highlighted index is the same, - // meaning that the focus changed to an element that is not an emoji item. - highlightedIndex: prevState.highlightedIndex === index ? -1 : prevState.highlightedIndex, - })) - } - isFocused={isEmojiFocused} - isHighlighted={index === this.state.highlightedIndex} - isUsingKeyboardMovement={this.state.isUsingKeyboardMovement} - /> - ); - } - - const isFiltered = filteredEmojis.current.length !== this.state.filteredEmojis.length; - const listStyle = StyleUtils.getEmojiPickerListHeight(isFiltered, this.props.windowHeight); - const height = !listStyle.maxHeight || listStyle.height < listStyle.maxHeight ? listStyle.height : listStyle.maxHeight; - const overflowLimit = Math.floor(height / CONST.EMOJI_PICKER_ITEM_HEIGHT) * 8; - return ( - - - this.setState({isFocused: true, highlightedIndex: -1, isUsingKeyboardMovement: false})} - onBlur={() => this.setState({isFocused: false})} - autoCorrect={false} - blurOnSubmit={this.state.filteredEmojis.length > 0} - /> - - {!isFiltered && ( - - )} - - ); -}; - -EmojiPickerMenu.propTypes = propTypes; -EmojiPickerMenu.defaultProps = defaultProps; - -export default compose( - withWindowDimensions, - withLocalize, - withOnyx({ - preferredSkinTone: { - key: ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE, - }, - frequentlyUsedEmojis: { - key: ONYXKEYS.FREQUENTLY_USED_EMOJIS, - }, - }), -)( - React.forwardRef((props, ref) => ( - - )), -); From 8e2540fe8d5cc8dd2fbcd2809379a62b10d06dc0 Mon Sep 17 00:00:00 2001 From: Ashutosh Khanduala Date: Sat, 7 Oct 2023 19:31:41 +0530 Subject: [PATCH 27/45] p#22: lifecycle methods: componentDidMount + componentWillUnmount Signed-off-by: Ashutosh Khanduala --- .../EmojiPickerMenu/EmojiPickerMenu copy.tsx | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx b/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx index 9bbdb58039b1..4bf81ea66284 100755 --- a/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx +++ b/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx @@ -1,4 +1,4 @@ -import React, {Component, useCallback, useRef, useState} from 'react'; +import React, {Component, useCallback, useEffect, useRef, useState} from 'react'; import {View, FlatList, TextInputSelectionChangeEventData} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import PropTypes from 'prop-types'; @@ -88,7 +88,8 @@ const EmojiPickerMenu = (props) => { const [isFocused, setIsFocused] = useState(false); const [isUsingKeyboardMovement, setIsUsingKeyboardMovement] = useState(false); const [selectTextOnFocus, setSelectTextOnFocus] = useState(false); - function componentDidMount() { + + useEffect(() => { // This callback prop is used by the parent component using the constructor to // get a ref to the inner textInput element e.g. if we do // this.textInput = el} /> this will not @@ -96,9 +97,14 @@ const EmojiPickerMenu = (props) => { if (shouldFocusInputOnScreenFocus && forwardedRef && _.isFunction(forwardedRef)) { forwardedRef(searchInputRef.current); } + setupEventHandlers(); updateFirstNonHeaderIndex(emojis.current); - } + + return () => { + cleanupEventHandlers(); + }; + }, []); function componentDidUpdate(prevProps) { if (prevProps.frequentlyUsedEmojis === frequentlyUsedEmojis) { @@ -113,10 +119,6 @@ const EmojiPickerMenu = (props) => { setHeaderIndices(headerRowIndices.current); } - function componentWillUnmount() { - cleanupEventHandlers(); - } - /** * On text input selection change * From 57ac93b3bd957989254de4f277067288deadeaba Mon Sep 17 00:00:00 2001 From: Ashutosh Khanduala Date: Sat, 7 Oct 2023 19:39:21 +0530 Subject: [PATCH 28/45] p#22: lifecycle method: componentDidUpdate() Signed-off-by: Ashutosh Khanduala --- .../EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx b/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx index 4bf81ea66284..b93fdc117a53 100755 --- a/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx +++ b/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx @@ -106,18 +106,14 @@ const EmojiPickerMenu = (props) => { }; }, []); - function componentDidUpdate(prevProps) { - if (prevProps.frequentlyUsedEmojis === frequentlyUsedEmojis) { - return; - } - + useEffect(() => { const emojisAndHeaderRowIndices = getEmojisAndHeaderRowIndices(); emojis.current = emojisAndHeaderRowIndices.filteredEmojis; headerRowIndices.current = emojisAndHeaderRowIndices.headerRowIndices; headerEmojis.current = emojisAndHeaderRowIndices.headerEmojis; setFilteredEmojis(emojis.current); setHeaderIndices(headerRowIndices.current); - } + }, [frequentlyUsedEmojis]); /** * On text input selection change From 60483ea8274caac775190d84bd57fc9ceef7660b Mon Sep 17 00:00:00 2001 From: Ashutosh Khanduala Date: Sat, 7 Oct 2023 20:41:29 +0530 Subject: [PATCH 29/45] p#23: cleanup: rename class component file as old.EmojiPickerMenu.tsx Signed-off-by: Ashutosh Khanduala --- .../{EmojiPickerMenu.tsx => old.EmojiPickerMenu.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/components/EmojiPicker/EmojiPickerMenu/{EmojiPickerMenu.tsx => old.EmojiPickerMenu.tsx} (100%) diff --git a/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu.tsx b/src/components/EmojiPicker/EmojiPickerMenu/old.EmojiPickerMenu.tsx similarity index 100% rename from src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu.tsx rename to src/components/EmojiPicker/EmojiPickerMenu/old.EmojiPickerMenu.tsx From 1a0cc8ee7286bdad6baa8967a27111da1fcbb607 Mon Sep 17 00:00:00 2001 From: Ashutosh Khanduala Date: Sat, 7 Oct 2023 20:45:28 +0530 Subject: [PATCH 30/45] fix: all emojis not visible Signed-off-by: Ashutosh Khanduala --- .../EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx b/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx index b93fdc117a53..6e479b8c1c99 100755 --- a/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx +++ b/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx @@ -8,7 +8,7 @@ import CONST from '../../../CONST'; import ONYXKEYS from '../../../ONYXKEYS'; import styles from '../../../styles/styles'; import * as StyleUtils from '../../../styles/StyleUtils'; -import emojis from '../../../../assets/emojis'; +import emojiAssets from '../../../../assets/emojis'; import EmojiPickerMenuItem from '../EmojiPickerMenuItem'; import Text from '../../Text'; import withWindowDimensions, {windowDimensionsPropTypes} from '../../withWindowDimensions'; @@ -131,11 +131,11 @@ const EmojiPickerMenu = (props) => { function getEmojisAndHeaderRowIndices() { // If we're on Windows, don't display the flag emojis (the last category), // since Windows doesn't support them - const flagHeaderIndex = _.findIndex(emojis, (emoji) => emoji.header && emoji.code === 'flags'); + const flagHeaderIndex = _.findIndex(emojiAssets, (emoji) => emoji.header && emoji.code === 'flags'); const filteredEmojis = getOperatingSystem() === CONST.OS.WINDOWS - ? EmojiUtils.mergeEmojisWithFrequentlyUsedEmojis(emojis.slice(0, flagHeaderIndex)) - : EmojiUtils.mergeEmojisWithFrequentlyUsedEmojis(emojis); + ? EmojiUtils.mergeEmojisWithFrequentlyUsedEmojis(emojiAssets.slice(0, flagHeaderIndex)) + : EmojiUtils.mergeEmojisWithFrequentlyUsedEmojis(emojiAssets); // Get the header emojis along with the code, index and icon. // index is the actual header index starting at the first emoji and counting each one From 58bec01277bf4ce8af79b89cd08dc3cc69c8f8de Mon Sep 17 00:00:00 2001 From: Ashutosh Khanduala Date: Sat, 7 Oct 2023 21:12:19 +0530 Subject: [PATCH 31/45] rename EmojiPickerMenu copy to EmojiPickerMenu Signed-off-by: Ashutosh Khanduala --- .../{EmojiPickerMenu copy.tsx => EmojiPickerMenu.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/components/EmojiPicker/EmojiPickerMenu/{EmojiPickerMenu copy.tsx => EmojiPickerMenu.tsx} (100%) diff --git a/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx b/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu.tsx similarity index 100% rename from src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu copy.tsx rename to src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu.tsx From 5ee4b643a53deb1c65df09f93a5ed20e7f9e9475 Mon Sep 17 00:00:00 2001 From: Ashutosh Khanduala Date: Wed, 11 Oct 2023 02:37:39 +0530 Subject: [PATCH 32/45] fix: remove TS types Signed-off-by: Ashutosh Khanduala --- .../EmojiPickerMenu/EmojiPickerMenu.tsx | 43 ++++++++----------- 1 file changed, 17 insertions(+), 26 deletions(-) diff --git a/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu.tsx b/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu.tsx index 6e479b8c1c99..84bc179ec643 100755 --- a/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu.tsx +++ b/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu.tsx @@ -54,10 +54,10 @@ const EmojiPickerMenu = (props) => { const {forwardedRef, frequentlyUsedEmojis, preferredSkinTone, onEmojiSelected, preferredLocale, isSmallScreenWidth, windowWidth, windowHeight, translate} = props; // Ref for the emoji search input - const searchInputRef = useRef(null); // TODO: is RNTextInput correct? + const searchInputRef = useRef(null); // Ref for emoji FlatList - const emojiListRef = useRef(null); + const emojiListRef = useRef(null); // We want consistent auto focus behavior on input between native and mWeb so we have some auto focus management code that will // prevent auto focus when open picker for mobile device @@ -66,20 +66,19 @@ const EmojiPickerMenu = (props) => { const firstNonHeaderIndex = useRef(0); // TODO: Group the 3 refs into 1?? Adv:- code would look cleaner + there will be only 1 getEmojisAndHeaderRowIndices() call. - const emojis = useRef([]); // TODO: find TS type + const emojis = useRef([]); if (emojis.current.length === 0) { emojis.current = getEmojisAndHeaderRowIndices().filteredEmojis; } - const headerRowIndices = useRef([]); // TODO: Maybe this ref is not needed. headerIndices state might suffice + const headerRowIndices = useRef([]); // TODO: Maybe this ref is not needed. headerIndices state might suffice if (headerRowIndices.current.length === 0) { headerRowIndices.current = getEmojisAndHeaderRowIndices().headerRowIndices; } - const headerEmojis = useRef([]); // TODO: find TS type + const headerEmojis = useRef([]); if (headerEmojis.current.length === 0) { headerEmojis.current = getEmojisAndHeaderRowIndices().headerEmojis; } - // TODO: Group releated states in objects const [filteredEmojis, setFilteredEmojis] = useState(emojis.current); const [headerIndices, setHeaderIndices] = useState(headerRowIndices.current); const [highlightedIndex, setHighlightedIndex] = useState(-1); @@ -120,7 +119,7 @@ const EmojiPickerMenu = (props) => { * * @param {Event} event */ - function onSelectionChange(event: NativeSyntheticEvent) { + function onSelectionChange(event) { setSelection(event.nativeEvent.selection); } @@ -153,12 +152,11 @@ const EmojiPickerMenu = (props) => { * Find and store index of the first emoji item * @param {Array} filteredEmojis */ - function updateFirstNonHeaderIndex(filteredEmojis: Object[]) { - // TODO: Emoji Object type + function updateFirstNonHeaderIndex(filteredEmojis) { firstNonHeaderIndex.current = _.findIndex(filteredEmojis, (item) => !item.spacer && !item.header); } - const keyDownHandler = (keyBoardEvent: KeyboardEvent) => { + const keyDownHandler = (keyBoardEvent) => { if (keyBoardEvent.key.startsWith('Arrow')) { if (!isFocused || keyBoardEvent.key === 'ArrowUp' || keyBoardEvent.key === 'ArrowDown') { keyBoardEvent.preventDefault(); @@ -230,8 +228,7 @@ const EmojiPickerMenu = (props) => { * @param {Number} index row index * @returns {Object} */ - function getItemLayout(data: any, index: number) { - // TODO: data param is unused. Still find its type + function getItemLayout(data, index) { return {length: CONST.EMOJI_PICKER_ITEM_HEIGHT, offset: CONST.EMOJI_PICKER_ITEM_HEIGHT * index, index}; } @@ -247,8 +244,7 @@ const EmojiPickerMenu = (props) => { * @param {String} emoji * @param {Object} emojiObject */ - function addToFrequentAndSelectEmoji(emoji: string, emojiObject: {}) { - // TODO: type of emojiObject + function addToFrequentAndSelectEmoji(emoji, emojiObject) { const frequentEmojiList = EmojiUtils.getFrequentlyUsedEmojis(emojiObject); User.updateFrequentlyUsedEmojis(frequentEmojiList); onEmojiSelected(emoji, emojiObject); @@ -270,7 +266,7 @@ const EmojiPickerMenu = (props) => { * Highlights emojis adjacent to the currently highlighted emoji depending on the arrowKey * @param {String} arrowKey */ - function highlightAdjacentEmoji(arrowKey: KeyboardEvent['key']) { + function highlightAdjacentEmoji(arrowKey) { if (filteredEmojis.length === 0) { return; } @@ -364,7 +360,7 @@ const EmojiPickerMenu = (props) => { } } - function scrollToHeader(headerIndex: number) { + function scrollToHeader(headerIndex) { const calculatedOffset = Math.floor(headerIndex / CONST.EMOJI_NUM_PER_ROW) * CONST.EMOJI_PICKER_HEADER_HEIGHT; emojiListRef.current?.flashScrollIndicators(); emojiListRef.current?.scrollToOffset({offset: calculatedOffset, animated: true}); @@ -376,7 +372,7 @@ const EmojiPickerMenu = (props) => { * @param {String} searchTerm */ const filterEmojis = useCallback( - _.debounce((searchTerm: string) => { + _.debounce((searchTerm) => { const normalizedSearchTerm = searchTerm.toLowerCase().trim().replaceAll(':', ''); emojiListRef.current?.scrollToOffset({offset: 0, animated: false}); if (normalizedSearchTerm === '') { @@ -404,20 +400,17 @@ const EmojiPickerMenu = (props) => { * @returns {Boolean} */ function isMobileLandscape() { - // TODO: This isnt used anywhere return isSmallScreenWidth && windowWidth >= windowHeight; } /** * @param {Number} skinTone */ - function updatePreferredSkinTone(skinTone: number) { + function updatePreferredSkinTone(skinTone) { if (Number(preferredSkinTone) === skinTone) { - // TODO: temp Number() for safety return; } - // TODO: Change JS Doc in User.js (type string => number) User.updatePreferredSkinTone(skinTone); } @@ -428,8 +421,7 @@ const EmojiPickerMenu = (props) => { * @param {Number} index * @returns {String} */ - function keyExtractor(item, index: number) { - // TODO: find type of item + function keyExtractor(item, index) { return `emoji_picker_${item.code}_${index}`; } @@ -438,12 +430,11 @@ const EmojiPickerMenu = (props) => { * Items with the code "SPACER" return nothing and are used to fill rows up to 8 * so that the sticky headers function properly. * - * @param {Object} item // TODO: Find Type + * @param {Object} item * @param {Number} index * @returns {*} */ - function renderItem({item, index}: {item: {}; index: number}) { - // TODO: TS types for item + function renderItem({item, index}) { const {code, header, types} = item; if (item.spacer) { return null; From 94022aa753c9d33be59dd2f8339df09df4c28b5e Mon Sep 17 00:00:00 2001 From: Ashutosh Khanduala Date: Wed, 11 Oct 2023 02:40:46 +0530 Subject: [PATCH 33/45] remove TODO comments Signed-off-by: Ashutosh Khanduala --- .../EmojiPicker/EmojiPickerMenu/EmojiPickerMenu.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu.tsx b/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu.tsx index 84bc179ec643..796871573d0d 100755 --- a/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu.tsx +++ b/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu.tsx @@ -33,7 +33,7 @@ const propTypes = { forwardedRef: PropTypes.func, /** Stores user's preferred skin tone */ - preferredSkinTone: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), // TODO: preferredSkinTone must be number (always) + preferredSkinTone: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), /** Stores user's frequently used emojis */ // eslint-disable-next-line react/forbid-prop-types frequentlyUsedEmojis: PropTypes.arrayOf(PropTypes.object), @@ -65,12 +65,11 @@ const EmojiPickerMenu = (props) => { const firstNonHeaderIndex = useRef(0); - // TODO: Group the 3 refs into 1?? Adv:- code would look cleaner + there will be only 1 getEmojisAndHeaderRowIndices() call. const emojis = useRef([]); if (emojis.current.length === 0) { emojis.current = getEmojisAndHeaderRowIndices().filteredEmojis; } - const headerRowIndices = useRef([]); // TODO: Maybe this ref is not needed. headerIndices state might suffice + const headerRowIndices = useRef([]); if (headerRowIndices.current.length === 0) { headerRowIndices.current = getEmojisAndHeaderRowIndices().headerRowIndices; } @@ -512,7 +511,7 @@ const EmojiPickerMenu = (props) => { {!isFiltered && ( )} From 7f8040789350930b5e8c695d5d255b55c7f41a25 Mon Sep 17 00:00:00 2001 From: Ashutosh Khanduala Date: Wed, 11 Oct 2023 02:41:56 +0530 Subject: [PATCH 34/45] rename all .ts* back to .js* Signed-off-by: Ashutosh Khanduala --- .../EmojiPickerMenu/{EmojiPickerMenu.tsx => EmojiPickerMenu.jsx} | 0 .../{EmojiPickerMenu.native.tsx => EmojiPickerMenu.native.jsx} | 0 src/components/EmojiPicker/EmojiPickerMenu/{index.ts => index.js} | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename src/components/EmojiPicker/EmojiPickerMenu/{EmojiPickerMenu.tsx => EmojiPickerMenu.jsx} (100%) rename src/components/EmojiPicker/EmojiPickerMenu/{EmojiPickerMenu.native.tsx => EmojiPickerMenu.native.jsx} (100%) rename src/components/EmojiPicker/EmojiPickerMenu/{index.ts => index.js} (100%) diff --git a/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu.tsx b/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu.jsx similarity index 100% rename from src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu.tsx rename to src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu.jsx diff --git a/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu.native.tsx b/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu.native.jsx similarity index 100% rename from src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu.native.tsx rename to src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu.native.jsx diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.ts b/src/components/EmojiPicker/EmojiPickerMenu/index.js similarity index 100% rename from src/components/EmojiPicker/EmojiPickerMenu/index.ts rename to src/components/EmojiPicker/EmojiPickerMenu/index.js From c9e17620d627377ab40331d5ec3b4a257041bd0e Mon Sep 17 00:00:00 2001 From: Ashutosh Khanduala Date: Wed, 11 Oct 2023 02:42:07 +0530 Subject: [PATCH 35/45] delete old class component Signed-off-by: Ashutosh Khanduala --- .../EmojiPickerMenu/old.EmojiPickerMenu.tsx | 579 ------------------ 1 file changed, 579 deletions(-) delete mode 100755 src/components/EmojiPicker/EmojiPickerMenu/old.EmojiPickerMenu.tsx diff --git a/src/components/EmojiPicker/EmojiPickerMenu/old.EmojiPickerMenu.tsx b/src/components/EmojiPicker/EmojiPickerMenu/old.EmojiPickerMenu.tsx deleted file mode 100755 index e7af97145347..000000000000 --- a/src/components/EmojiPicker/EmojiPickerMenu/old.EmojiPickerMenu.tsx +++ /dev/null @@ -1,579 +0,0 @@ -import React, {Component} from 'react'; -import {View, FlatList} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; -import PropTypes from 'prop-types'; -import _ from 'underscore'; -import lodashGet from 'lodash/get'; -import CONST from '../../../CONST'; -import ONYXKEYS from '../../../ONYXKEYS'; -import styles from '../../../styles/styles'; -import * as StyleUtils from '../../../styles/StyleUtils'; -import emojis from '../../../../assets/emojis'; -import EmojiPickerMenuItem from '../EmojiPickerMenuItem'; -import Text from '../../Text'; -import withWindowDimensions, {windowDimensionsPropTypes} from '../../withWindowDimensions'; -import withLocalize, {withLocalizePropTypes} from '../../withLocalize'; -import compose from '../../../libs/compose'; -import getOperatingSystem from '../../../libs/getOperatingSystem'; -import * as User from '../../../libs/actions/User'; -import EmojiSkinToneList from '../EmojiSkinToneList'; -import * as EmojiUtils from '../../../libs/EmojiUtils'; -import CategoryShortcutBar from '../CategoryShortcutBar'; -import TextInput from '../../TextInput'; -import isEnterWhileComposition from '../../../libs/KeyboardShortcut/isEnterWhileComposition'; -import canFocusInputOnScreenFocus from '../../../libs/canFocusInputOnScreenFocus'; - -const propTypes = { - /** Function to add the selected emoji to the main compose text input */ - onEmojiSelected: PropTypes.func.isRequired, - - /** The ref to the search input (may be null on small screen widths) */ - forwardedRef: PropTypes.func, - - /** Stores user's preferred skin tone */ - preferredSkinTone: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), - - /** Stores user's frequently used emojis */ - // eslint-disable-next-line react/forbid-prop-types - frequentlyUsedEmojis: PropTypes.arrayOf(PropTypes.object), - - /** Props related to the dimensions of the window */ - ...windowDimensionsPropTypes, - - ...withLocalizePropTypes, -}; - -const defaultProps = { - forwardedRef: () => {}, - preferredSkinTone: CONST.EMOJI_DEFAULT_SKIN_TONE, - frequentlyUsedEmojis: [], -}; - -class EmojiPickerMenu extends Component { - constructor(props) { - super(props); - - // Ref for the emoji search input - this.searchInput = undefined; - - // Ref for emoji FlatList - this.emojiList = undefined; - - // We want consistent auto focus behavior on input between native and mWeb so we have some auto focus management code that will - // prevent auto focus when open picker for mobile device - this.shouldFocusInputOnScreenFocus = canFocusInputOnScreenFocus(); - - this.filterEmojis = _.debounce(this.filterEmojis.bind(this), 300); - this.highlightAdjacentEmoji = this.highlightAdjacentEmoji.bind(this); - this.setupEventHandlers = this.setupEventHandlers.bind(this); - this.cleanupEventHandlers = this.cleanupEventHandlers.bind(this); - this.renderItem = this.renderItem.bind(this); - this.isMobileLandscape = this.isMobileLandscape.bind(this); - this.onSelectionChange = this.onSelectionChange.bind(this); - this.updatePreferredSkinTone = this.updatePreferredSkinTone.bind(this); - this.setFirstNonHeaderIndex = this.setFirstNonHeaderIndex.bind(this); - this.getItemLayout = this.getItemLayout.bind(this); - this.scrollToHeader = this.scrollToHeader.bind(this); - - this.firstNonHeaderIndex = 0; - - const {filteredEmojis, headerEmojis, headerRowIndices} = this.getEmojisAndHeaderRowIndices(); - this.emojis = filteredEmojis; - this.headerEmojis = headerEmojis; - this.headerRowIndices = headerRowIndices; - - this.state = { - filteredEmojis: this.emojis, - headerIndices: this.headerRowIndices, - highlightedIndex: -1, - arePointerEventsDisabled: false, - selection: { - start: 0, - end: 0, - }, - isFocused: false, - isUsingKeyboardMovement: false, - }; - } - - componentDidMount() { - // This callback prop is used by the parent component using the constructor to - // get a ref to the inner textInput element e.g. if we do - // this.textInput = el} /> this will not - // return a ref to the component, but rather the HTML element by default - if (this.shouldFocusInputOnScreenFocus && this.props.forwardedRef && _.isFunction(this.props.forwardedRef)) { - this.props.forwardedRef(this.searchInput); - } - this.setupEventHandlers(); - this.setFirstNonHeaderIndex(this.emojis); - } - - componentDidUpdate(prevProps) { - if (prevProps.frequentlyUsedEmojis === this.props.frequentlyUsedEmojis) { - return; - } - - const {filteredEmojis, headerEmojis, headerRowIndices} = this.getEmojisAndHeaderRowIndices(); - this.emojis = filteredEmojis; - this.headerEmojis = headerEmojis; - this.headerRowIndices = headerRowIndices; - this.setState({ - filteredEmojis: this.emojis, - headerIndices: this.headerRowIndices, - }); - } - - componentWillUnmount() { - this.cleanupEventHandlers(); - } - - /** - * On text input selection change - * - * @param {Event} event - */ - onSelectionChange(event) { - this.setState({selection: event.nativeEvent.selection}); - } - - /** - * Calculate the filtered + header emojis and header row indices - * @returns {Object} - */ - getEmojisAndHeaderRowIndices() { - // If we're on Windows, don't display the flag emojis (the last category), - // since Windows doesn't support them - const flagHeaderIndex = _.findIndex(emojis, (emoji) => emoji.header && emoji.code === 'flags'); - const filteredEmojis = - getOperatingSystem() === CONST.OS.WINDOWS - ? EmojiUtils.mergeEmojisWithFrequentlyUsedEmojis(emojis.slice(0, flagHeaderIndex)) - : EmojiUtils.mergeEmojisWithFrequentlyUsedEmojis(emojis); - - // Get the header emojis along with the code, index and icon. - // index is the actual header index starting at the first emoji and counting each one - const headerEmojis = EmojiUtils.getHeaderEmojis(filteredEmojis); - - // This is the indices of each header's Row - // The positions are static, and are calculated as index/numColumns (8 in our case) - // This is because each row of 8 emojis counts as one index to the flatlist - const headerRowIndices = _.map(headerEmojis, (headerEmoji) => Math.floor(headerEmoji.index / CONST.EMOJI_NUM_PER_ROW)); - - return {filteredEmojis, headerEmojis, headerRowIndices}; - } - - /** - * Find and store index of the first emoji item - * @param {Array} filteredEmojis - */ - setFirstNonHeaderIndex(filteredEmojis) { - this.firstNonHeaderIndex = _.findIndex(filteredEmojis, (item) => !item.spacer && !item.header); - } - - /** - * Setup and attach keypress/mouse handlers for highlight navigation. - */ - setupEventHandlers() { - if (!document) { - return; - } - - this.keyDownHandler = (keyBoardEvent) => { - if (keyBoardEvent.key.startsWith('Arrow')) { - if (!this.state.isFocused || keyBoardEvent.key === 'ArrowUp' || keyBoardEvent.key === 'ArrowDown') { - keyBoardEvent.preventDefault(); - } - - // Move the highlight when arrow keys are pressed - this.highlightAdjacentEmoji(keyBoardEvent.key); - return; - } - - // Select the currently highlighted emoji if enter is pressed - if (!isEnterWhileComposition(keyBoardEvent) && keyBoardEvent.key === CONST.KEYBOARD_SHORTCUTS.ENTER.shortcutKey && this.state.highlightedIndex !== -1) { - const item = this.state.filteredEmojis[this.state.highlightedIndex]; - if (!item) { - return; - } - const emoji = lodashGet(item, ['types', this.props.preferredSkinTone], item.code); - this.addToFrequentAndSelectEmoji(emoji, item); - return; - } - - // Enable keyboard movement if tab or enter is pressed or if shift is pressed while the input - // is not focused, so that the navigation and tab cycling can be done using the keyboard without - // interfering with the input behaviour. - if (keyBoardEvent.key === 'Tab' || keyBoardEvent.key === 'Enter' || (keyBoardEvent.key === 'Shift' && this.searchInput && !this.searchInput.isFocused())) { - this.setState({isUsingKeyboardMovement: true}); - return; - } - - // We allow typing in the search box if any key is pressed apart from Arrow keys. - if (this.searchInput && !this.searchInput.isFocused()) { - this.setState({selectTextOnFocus: false}); - this.searchInput.focus(); - - // Re-enable selection on the searchInput - this.setState({selectTextOnFocus: true}); - } - }; - - // Keyboard events are not bubbling on TextInput in RN-Web, Bubbling was needed for this event to trigger - // event handler attached to document root. To fix this, trigger event handler in Capture phase. - document.addEventListener('keydown', this.keyDownHandler, true); - - // Re-enable pointer events and hovering over EmojiPickerItems when the mouse moves - this.mouseMoveHandler = () => { - if (!this.state.arePointerEventsDisabled) { - return; - } - - this.setState({arePointerEventsDisabled: false}); - }; - document.addEventListener('mousemove', this.mouseMoveHandler); - } - - /** - * This function will be used with FlatList getItemLayout property for optimization purpose that allows skipping - * the measurement of dynamic content if we know the size (height or width) of items ahead of time. - * Generate and return an object with properties length(height of each individual row), - * offset(distance of the current row from the top of the FlatList), index(current row index) - * - * @param {*} data FlatList item - * @param {Number} index row index - * @returns {Object} - */ - getItemLayout(data, index) { - return {length: CONST.EMOJI_PICKER_ITEM_HEIGHT, offset: CONST.EMOJI_PICKER_ITEM_HEIGHT * index, index}; - } - - /** - * Cleanup all mouse/keydown event listeners that we've set up - */ - cleanupEventHandlers() { - if (!document) { - return; - } - - document.removeEventListener('keydown', this.keyDownHandler, true); - document.removeEventListener('mousemove', this.mouseMoveHandler); - } - - /** - * @param {String} emoji - * @param {Object} emojiObject - */ - addToFrequentAndSelectEmoji(emoji, emojiObject) { - const frequentEmojiList = EmojiUtils.getFrequentlyUsedEmojis(emojiObject); - User.updateFrequentlyUsedEmojis(frequentEmojiList); - this.props.onEmojiSelected(emoji, emojiObject); - } - - /** - * Focuses the search Input and has the text selected - */ - focusInputWithTextSelect() { - if (!this.searchInput) { - return; - } - - this.setState({selectTextOnFocus: true}); - this.searchInput.focus(); - } - - /** - * Highlights emojis adjacent to the currently highlighted emoji depending on the arrowKey - * @param {String} arrowKey - */ - highlightAdjacentEmoji(arrowKey) { - if (this.state.filteredEmojis.length === 0) { - return; - } - - // Arrow Down and Arrow Right enable arrow navigation when search is focused - if (this.searchInput && this.searchInput.isFocused()) { - if (arrowKey !== 'ArrowDown' && arrowKey !== 'ArrowRight') { - return; - } - - if (arrowKey === 'ArrowRight' && !(this.searchInput.value.length === this.state.selection.start && this.state.selection.start === this.state.selection.end)) { - return; - } - - // Blur the input, change the highlight type to keyboard, and disable pointer events - this.searchInput.blur(); - this.setState({isUsingKeyboardMovement: true, arePointerEventsDisabled: true}); - - // We only want to hightlight the Emoji if none was highlighted already - // If we already have a highlighted Emoji, lets just skip the first navigation - if (this.state.highlightedIndex !== -1) { - return; - } - } - - // If nothing is highlighted and an arrow key is pressed - // select the first emoji, apply keyboard movement styles, and disable pointer events - if (this.state.highlightedIndex === -1) { - this.setState({highlightedIndex: this.firstNonHeaderIndex, isUsingKeyboardMovement: true, arePointerEventsDisabled: true}); - return; - } - - let newIndex = this.state.highlightedIndex; - const move = (steps, boundsCheck, onBoundReached = () => {}) => { - if (boundsCheck()) { - onBoundReached(); - return; - } - - // Move in the prescribed direction until we reach an element that isn't a header - const isHeader = (e) => e.header || e.spacer; - do { - newIndex += steps; - if (newIndex < 0) { - break; - } - } while (isHeader(this.state.filteredEmojis[newIndex])); - }; - - switch (arrowKey) { - case 'ArrowDown': - move(CONST.EMOJI_NUM_PER_ROW, () => this.state.highlightedIndex + CONST.EMOJI_NUM_PER_ROW > this.state.filteredEmojis.length - 1); - break; - case 'ArrowLeft': - move( - -1, - () => this.state.highlightedIndex - 1 < this.firstNonHeaderIndex, - () => { - // Reaching start of the list, arrow left set the focus to searchInput. - this.focusInputWithTextSelect(); - newIndex = -1; - }, - ); - break; - case 'ArrowRight': - move(1, () => this.state.highlightedIndex + 1 > this.state.filteredEmojis.length - 1); - break; - case 'ArrowUp': - move( - -CONST.EMOJI_NUM_PER_ROW, - () => this.state.highlightedIndex - CONST.EMOJI_NUM_PER_ROW < this.firstNonHeaderIndex, - () => { - // Reaching start of the list, arrow up set the focus to searchInput. - this.focusInputWithTextSelect(); - newIndex = -1; - }, - ); - break; - default: - break; - } - - // Actually highlight the new emoji, apply keyboard movement styles, and disable pointer events - if (newIndex !== this.state.highlightedIndex) { - this.setState({highlightedIndex: newIndex, isUsingKeyboardMovement: true, arePointerEventsDisabled: true}); - } - } - - scrollToHeader(headerIndex) { - const calculatedOffset = Math.floor(headerIndex / CONST.EMOJI_NUM_PER_ROW) * CONST.EMOJI_PICKER_HEADER_HEIGHT; - this.emojiList.flashScrollIndicators(); - this.emojiList.scrollToOffset({offset: calculatedOffset, animated: true}); - } - - /** - * Filter the entire list of emojis to only emojis that have the search term in their keywords - * - * @param {String} searchTerm - */ - filterEmojis(searchTerm) { - const normalizedSearchTerm = searchTerm.toLowerCase().trim().replaceAll(':', ''); - if (this.emojiList) { - this.emojiList.scrollToOffset({offset: 0, animated: false}); - } - if (normalizedSearchTerm === '') { - // There are no headers when searching, so we need to re-make them sticky when there is no search term - this.setState({ - filteredEmojis: this.emojis, - headerIndices: this.headerRowIndices, - highlightedIndex: -1, - }); - this.setFirstNonHeaderIndex(this.emojis); - return; - } - const newFilteredEmojiList = EmojiUtils.suggestEmojis(`:${normalizedSearchTerm}`, this.props.preferredLocale, this.emojis.length); - - // Remove sticky header indices. There are no headers while searching and we don't want to make emojis sticky - this.setState({filteredEmojis: newFilteredEmojiList, headerIndices: [], highlightedIndex: 0}); - this.setFirstNonHeaderIndex(newFilteredEmojiList); - } - - /** - * Check if its a landscape mode of mobile device - * - * @returns {Boolean} - */ - isMobileLandscape() { - return this.props.isSmallScreenWidth && this.props.windowWidth >= this.props.windowHeight; - } - - /** - * @param {Number} skinTone - */ - updatePreferredSkinTone(skinTone) { - if (this.props.preferredSkinTone === skinTone) { - return; - } - - User.updatePreferredSkinTone(skinTone); - } - - /** - * Return a unique key for each emoji item - * - * @param {Object} item - * @param {Number} index - * @returns {String} - */ - keyExtractor(item, index) { - return `emoji_picker_${item.code}_${index}`; - } - - /** - * Given an emoji item object, render a component based on its type. - * Items with the code "SPACER" return nothing and are used to fill rows up to 8 - * so that the sticky headers function properly. - * - * @param {Object} item - * @param {Number} index - * @returns {*} - */ - renderItem({item, index}) { - const {code, header, types} = item; - if (item.spacer) { - return null; - } - - if (header) { - return ( - - {this.props.translate(`emojiPicker.headers.${code}`)} - - ); - } - - const emojiCode = types && types[this.props.preferredSkinTone] ? types[this.props.preferredSkinTone] : code; - - const isEmojiFocused = index === this.state.highlightedIndex && this.state.isUsingKeyboardMovement; - - return ( - this.addToFrequentAndSelectEmoji(emoji, item)} - onHoverIn={() => this.setState({highlightedIndex: index, isUsingKeyboardMovement: false})} - onHoverOut={() => { - if (this.state.arePointerEventsDisabled) { - return; - } - this.setState({highlightedIndex: -1}); - }} - emoji={emojiCode} - onFocus={() => this.setState({highlightedIndex: index})} - onBlur={() => - this.setState((prevState) => ({ - // Only clear the highlighted index if the highlighted index is the same, - // meaning that the focus changed to an element that is not an emoji item. - highlightedIndex: prevState.highlightedIndex === index ? -1 : prevState.highlightedIndex, - })) - } - isFocused={isEmojiFocused} - isHighlighted={index === this.state.highlightedIndex} - isUsingKeyboardMovement={this.state.isUsingKeyboardMovement} - /> - ); - } - - render() { - const isFiltered = this.emojis.length !== this.state.filteredEmojis.length; - const listStyle = StyleUtils.getEmojiPickerListHeight(isFiltered, this.props.windowHeight); - const height = !listStyle.maxHeight || listStyle.height < listStyle.maxHeight ? listStyle.height : listStyle.maxHeight; - const overflowLimit = Math.floor(height / CONST.EMOJI_PICKER_ITEM_HEIGHT) * 8; - return ( - - - (this.searchInput = el)} - autoFocus={this.shouldFocusInputOnScreenFocus} - selectTextOnFocus={this.state.selectTextOnFocus} - onSelectionChange={this.onSelectionChange} - onFocus={() => this.setState({isFocused: true, highlightedIndex: -1, isUsingKeyboardMovement: false})} - onBlur={() => this.setState({isFocused: false})} - autoCorrect={false} - blurOnSubmit={this.state.filteredEmojis.length > 0} - /> - - {!isFiltered && ( - - )} - (this.emojiList = el)} - data={this.state.filteredEmojis} - renderItem={this.renderItem} - keyExtractor={this.keyExtractor} - numColumns={CONST.EMOJI_NUM_PER_ROW} - style={[ - listStyle, - // This prevents elastic scrolling when scroll reaches the start or end - {overscrollBehaviorY: 'contain'}, - // Set overflow to hidden to prevent elastic scrolling when there are not enough contents to scroll in FlatList - {overflowY: this.state.filteredEmojis.length > overflowLimit ? 'auto' : 'hidden'}, - // Set scrollPaddingTop to consider sticky headers while scrolling - {scrollPaddingTop: isFiltered ? 0 : CONST.EMOJI_PICKER_ITEM_HEIGHT}, - ]} - extraData={[this.state.filteredEmojis, this.state.highlightedIndex, this.props.preferredSkinTone]} - stickyHeaderIndices={this.state.headerIndices} - getItemLayout={this.getItemLayout} - contentContainerStyle={styles.flexGrow1} - ListEmptyComponent={{this.props.translate('common.noResultsFound')}} - /> - - - ); - } -} - -EmojiPickerMenu.propTypes = propTypes; -EmojiPickerMenu.defaultProps = defaultProps; - -export default compose( - withWindowDimensions, - withLocalize, - withOnyx({ - preferredSkinTone: { - key: ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE, - }, - frequentlyUsedEmojis: { - key: ONYXKEYS.FREQUENTLY_USED_EMOJIS, - }, - }), -)( - React.forwardRef((props, ref) => ( - - )), -); From e102c5e3bb7671a333d6d9b323c35c834fff606c Mon Sep 17 00:00:00 2001 From: Ashutosh Khanduala Date: Wed, 11 Oct 2023 04:35:58 +0530 Subject: [PATCH 36/45] fix: fix Bad Practice. headerEmoji (ref) was being read during render Signed-off-by: Ashutosh Khanduala --- .../EmojiPicker/EmojiPickerMenu/EmojiPickerMenu.jsx | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu.jsx b/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu.jsx index 796871573d0d..3ea4007a4182 100755 --- a/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu.jsx +++ b/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu.jsx @@ -73,10 +73,7 @@ const EmojiPickerMenu = (props) => { if (headerRowIndices.current.length === 0) { headerRowIndices.current = getEmojisAndHeaderRowIndices().headerRowIndices; } - const headerEmojis = useRef([]); - if (headerEmojis.current.length === 0) { - headerEmojis.current = getEmojisAndHeaderRowIndices().headerEmojis; - } + const [headerEmojis, setHeaderEmojis] = useState(() => getEmojisAndHeaderRowIndices().headerEmojis); const [filteredEmojis, setFilteredEmojis] = useState(emojis.current); const [headerIndices, setHeaderIndices] = useState(headerRowIndices.current); @@ -108,7 +105,7 @@ const EmojiPickerMenu = (props) => { const emojisAndHeaderRowIndices = getEmojisAndHeaderRowIndices(); emojis.current = emojisAndHeaderRowIndices.filteredEmojis; headerRowIndices.current = emojisAndHeaderRowIndices.headerRowIndices; - headerEmojis.current = emojisAndHeaderRowIndices.headerEmojis; + setHeaderEmojis(emojisAndHeaderRowIndices.headerEmojis); setFilteredEmojis(emojis.current); setHeaderIndices(headerRowIndices.current); }, [frequentlyUsedEmojis]); @@ -511,7 +508,7 @@ const EmojiPickerMenu = (props) => { {!isFiltered && ( )} From 3f70fb4cb2922776ed690c38366f7f5c1588bf5d Mon Sep 17 00:00:00 2001 From: Ashutosh Khanduala Date: Wed, 11 Oct 2023 12:11:26 +0530 Subject: [PATCH 37/45] remove index.js Signed-off-by: Ashutosh Khanduala --- src/components/EmojiPicker/EmojiPickerMenu/index.js | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 src/components/EmojiPicker/EmojiPickerMenu/index.js diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.js b/src/components/EmojiPicker/EmojiPickerMenu/index.js deleted file mode 100644 index bcc226a34f54..000000000000 --- a/src/components/EmojiPicker/EmojiPickerMenu/index.js +++ /dev/null @@ -1,2 +0,0 @@ -import EmojiPickerMenu from './EmojiPickerMenu'; -export default EmojiPickerMenu; From 25f7ba6e6c011dd3cc7b3dcda2a95d32ca44d847 Mon Sep 17 00:00:00 2001 From: Ashutosh Khanduala Date: Wed, 11 Oct 2023 12:12:42 +0530 Subject: [PATCH 38/45] rename: EmojiPickerMenu*.jsx -> index*.js Signed-off-by: Ashutosh Khanduala --- .../EmojiPicker/EmojiPickerMenu/{EmojiPickerMenu.jsx => index.js} | 0 .../{EmojiPickerMenu.native.jsx => index.native.js} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename src/components/EmojiPicker/EmojiPickerMenu/{EmojiPickerMenu.jsx => index.js} (100%) rename src/components/EmojiPicker/EmojiPickerMenu/{EmojiPickerMenu.native.jsx => index.native.js} (100%) diff --git a/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu.jsx b/src/components/EmojiPicker/EmojiPickerMenu/index.js similarity index 100% rename from src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu.jsx rename to src/components/EmojiPicker/EmojiPickerMenu/index.js diff --git a/src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu.native.jsx b/src/components/EmojiPicker/EmojiPickerMenu/index.native.js similarity index 100% rename from src/components/EmojiPicker/EmojiPickerMenu/EmojiPickerMenu.native.jsx rename to src/components/EmojiPicker/EmojiPickerMenu/index.native.js From bcffdbe5e122c2ac511c79a6f3769fa7abf77961 Mon Sep 17 00:00:00 2001 From: Ashutosh Khanduala Date: Thu, 12 Oct 2023 13:33:07 +0530 Subject: [PATCH 39/45] fix: eslint error: "ES2020 optional chaining is forbidden" --- .../EmojiPicker/EmojiPickerMenu/index.js | 30 ++++++++++++------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.js b/src/components/EmojiPicker/EmojiPickerMenu/index.js index 481fcb931326..e15ab08daf6e 100755 --- a/src/components/EmojiPicker/EmojiPickerMenu/index.js +++ b/src/components/EmojiPicker/EmojiPickerMenu/index.js @@ -176,7 +176,7 @@ function EmojiPickerMenu(props) { } // Arrow Down and Arrow Right enable arrow navigation when search is focused - if (searchInputRef.current?.isFocused()) { + if (searchInputRef.current && searchInputRef.current.isFocused()) { if (arrowKey !== 'ArrowDown' && arrowKey !== 'ArrowRight') { return; } @@ -289,15 +289,15 @@ function EmojiPickerMenu(props) { // Enable keyboard movement if tab or enter is pressed or if shift is pressed while the input // is not focused, so that the navigation and tab cycling can be done using the keyboard without // interfering with the input behaviour. - if (keyBoardEvent.key === 'Tab' || keyBoardEvent.key === 'Enter' || (keyBoardEvent.key === 'Shift' && !searchInputRef.current?.isFocused())) { + if (keyBoardEvent.key === 'Tab' || keyBoardEvent.key === 'Enter' || (keyBoardEvent.key === 'Shift' && searchInputRef.current && !searchInputRef.current.isFocused())) { setIsUsingKeyboardMovement(true); return; } // We allow typing in the search box if any key is pressed apart from Arrow keys. - if (!searchInputRef.current?.isFocused()) { + if (searchInputRef.current && !searchInputRef.current.isFocused()) { setSelectTextOnFocus(false); - searchInputRef.current?.focus(); + searchInputRef.current.focus(); // Re-enable selection on the searchInput setSelectTextOnFocus(true); @@ -324,8 +324,12 @@ function EmojiPickerMenu(props) { * Cleanup all mouse/keydown event listeners that we've set up */ function cleanupEventHandlers() { - document?.removeEventListener('keydown', keyDownHandler, true); - document?.removeEventListener('mousemove', mouseMoveHandler); + if (!document) { + return; + } + + document.removeEventListener('keydown', keyDownHandler, true); + document.removeEventListener('mousemove', mouseMoveHandler); } useEffect(() => { @@ -346,9 +350,13 @@ function EmojiPickerMenu(props) { }, []); function scrollToHeader(headerIndex) { + if (!emojiListRef.current) { + return; + } + const calculatedOffset = Math.floor(headerIndex / CONST.EMOJI_NUM_PER_ROW) * CONST.EMOJI_PICKER_HEADER_HEIGHT; - emojiListRef.current?.flashScrollIndicators(); - emojiListRef.current?.scrollToOffset({offset: calculatedOffset, animated: true}); + emojiListRef.current.flashScrollIndicators(); + emojiListRef.current.scrollToOffset({offset: calculatedOffset, animated: true}); } /** @@ -359,7 +367,9 @@ function EmojiPickerMenu(props) { const filterEmojis = useCallback( _.debounce((searchTerm) => { const normalizedSearchTerm = searchTerm.toLowerCase().trim().replaceAll(':', ''); - emojiListRef.current?.scrollToOffset({offset: 0, animated: false}); + if (emojiListRef.current) { + emojiListRef.current.scrollToOffset({offset: 0, animated: false}); + } if (normalizedSearchTerm === '') { // There are no headers when searching, so we need to re-make them sticky when there is no search term setFilteredEmojis(emojis.current); @@ -451,7 +461,7 @@ function EmojiPickerMenu(props) { setHighlightedIndex(-1); }} emoji={emojiCode} - onFocus={() => void setHighlightedIndex(index)} + onFocus={() => setHighlightedIndex(index)} onBlur={() => // Only clear the highlighted index if the highlighted index is the same, // meaning that the focus changed to an element that is not an emoji item. From 7e999a6240a0b5362b08996faffa426be2d70432 Mon Sep 17 00:00:00 2001 From: Ashutosh Khanduala Date: Thu, 12 Oct 2023 13:39:47 +0530 Subject: [PATCH 40/45] fix: eslint error: "JSX props should not use functions" --- .../EmojiPicker/EmojiPickerMenu/index.js | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.js b/src/components/EmojiPicker/EmojiPickerMenu/index.js index e15ab08daf6e..b79880478ab4 100755 --- a/src/components/EmojiPicker/EmojiPickerMenu/index.js +++ b/src/components/EmojiPicker/EmojiPickerMenu/index.js @@ -121,9 +121,9 @@ function EmojiPickerMenu(props) { * * @param {Event} event */ - function onSelectionChange(event) { + const onSelectionChange = useCallback((event) => { setSelection(event.nativeEvent.selection); - } + }, []); /** * Find and store index of the first emoji item @@ -150,9 +150,7 @@ function EmojiPickerMenu(props) { * @param {Number} index row index * @returns {Object} */ - function getItemLayout(data, index) { - return {length: CONST.EMOJI_PICKER_ITEM_HEIGHT, offset: CONST.EMOJI_PICKER_ITEM_HEIGHT * index, index}; - } + const getItemLayout = useCallback((data, index) => ({length: CONST.EMOJI_PICKER_ITEM_HEIGHT, offset: CONST.EMOJI_PICKER_ITEM_HEIGHT * index, index}), []); /** * Focuses the search Input and has the text selected @@ -349,7 +347,7 @@ function EmojiPickerMenu(props) { }; }, []); - function scrollToHeader(headerIndex) { + const scrollToHeader = useCallback((headerIndex) => { if (!emojiListRef.current) { return; } @@ -357,7 +355,7 @@ function EmojiPickerMenu(props) { const calculatedOffset = Math.floor(headerIndex / CONST.EMOJI_NUM_PER_ROW) * CONST.EMOJI_PICKER_HEADER_HEIGHT; emojiListRef.current.flashScrollIndicators(); emojiListRef.current.scrollToOffset({offset: calculatedOffset, animated: true}); - } + }, []); /** * Filter the entire list of emojis to only emojis that have the search term in their keywords @@ -401,13 +399,13 @@ function EmojiPickerMenu(props) { /** * @param {Number} skinTone */ - function updatePreferredSkinTone(skinTone) { + const updatePreferredSkinTone = useCallback((skinTone) => { if (Number(preferredSkinTone) === skinTone) { return; } User.updatePreferredSkinTone(skinTone); - } + }, []); /** * Return a unique key for each emoji item @@ -416,9 +414,7 @@ function EmojiPickerMenu(props) { * @param {Number} index * @returns {String} */ - function keyExtractor(item, index) { - return `emoji_picker_${item.code}_${index}`; - } + const keyExtractor = useCallback((item, index) => `emoji_picker_${item.code}_${index}`, []); /** * Given an emoji item object, render a component based on its type. @@ -429,7 +425,7 @@ function EmojiPickerMenu(props) { * @param {Number} index * @returns {*} */ - function renderItem({item, index}) { + const renderItem = useCallback(({item, index}) => { const {code, header, types} = item; if (item.spacer) { return null; @@ -472,7 +468,7 @@ function EmojiPickerMenu(props) { isUsingKeyboardMovement={isUsingKeyboardMovement} /> ); - } + }, []); const isFiltered = emojis.current.length !== filteredEmojis.length; const listStyle = StyleUtils.getEmojiPickerListHeight(isFiltered, windowHeight); From 0c9c6d263b88c45572b66ad20081e65d6c2f2802 Mon Sep 17 00:00:00 2001 From: Ashutosh Khanduala Date: Thu, 12 Oct 2023 13:51:09 +0530 Subject: [PATCH 41/45] fix: eslint error: useCallback & useEffect deps array --- .../EmojiPicker/EmojiPickerMenu/index.js | 100 ++++++++++-------- 1 file changed, 53 insertions(+), 47 deletions(-) diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.js b/src/components/EmojiPicker/EmojiPickerMenu/index.js index b79880478ab4..5b7ce6d2642a 100755 --- a/src/components/EmojiPicker/EmojiPickerMenu/index.js +++ b/src/components/EmojiPicker/EmojiPickerMenu/index.js @@ -345,7 +345,7 @@ function EmojiPickerMenu(props) { return () => { cleanupEventHandlers(); }; - }, []); + }, [forwardedRef, shouldFocusInputOnScreenFocus]); const scrollToHeader = useCallback((headerIndex) => { if (!emojiListRef.current) { @@ -384,7 +384,7 @@ function EmojiPickerMenu(props) { setHighlightedIndex(0); updateFirstNonHeaderIndex(newFilteredEmojiList); }, 300), - [], + [preferredLocale], ); /** @@ -399,13 +399,16 @@ function EmojiPickerMenu(props) { /** * @param {Number} skinTone */ - const updatePreferredSkinTone = useCallback((skinTone) => { - if (Number(preferredSkinTone) === skinTone) { - return; - } + const updatePreferredSkinTone = useCallback( + (skinTone) => { + if (Number(preferredSkinTone) === Number(skinTone)) { + return; + } - User.updatePreferredSkinTone(skinTone); - }, []); + User.updatePreferredSkinTone(skinTone); + }, + [preferredSkinTone], + ); /** * Return a unique key for each emoji item @@ -425,50 +428,53 @@ function EmojiPickerMenu(props) { * @param {Number} index * @returns {*} */ - const renderItem = useCallback(({item, index}) => { - const {code, header, types} = item; - if (item.spacer) { - return null; - } + const renderItem = useCallback( + ({item, index}) => { + const {code, header, types} = item; + if (item.spacer) { + return null; + } - if (header) { - return ( - - {translate(`emojiPicker.headers.${code}`)} - - ); - } + if (header) { + return ( + + {translate(`emojiPicker.headers.${code}`)} + + ); + } - const emojiCode = types && types[preferredSkinTone] ? types[preferredSkinTone] : code; + const emojiCode = types && types[preferredSkinTone] ? types[preferredSkinTone] : code; - const isEmojiFocused = index === highlightedIndex && isUsingKeyboardMovement; + const isEmojiFocused = index === highlightedIndex && isUsingKeyboardMovement; - return ( - onEmojiSelected(emoji, item)} - onHoverIn={() => { - setHighlightedIndex(index); - setIsUsingKeyboardMovement(false); - }} - onHoverOut={() => { - if (arePointerEventsDisabled) { - return; + return ( + onEmojiSelected(emoji, item)} + onHoverIn={() => { + setHighlightedIndex(index); + setIsUsingKeyboardMovement(false); + }} + onHoverOut={() => { + if (arePointerEventsDisabled) { + return; + } + setHighlightedIndex(-1); + }} + emoji={emojiCode} + onFocus={() => setHighlightedIndex(index)} + onBlur={() => + // Only clear the highlighted index if the highlighted index is the same, + // meaning that the focus changed to an element that is not an emoji item. + setHighlightedIndex((prevState) => (prevState === index ? -1 : prevState)) } - setHighlightedIndex(-1); - }} - emoji={emojiCode} - onFocus={() => setHighlightedIndex(index)} - onBlur={() => - // Only clear the highlighted index if the highlighted index is the same, - // meaning that the focus changed to an element that is not an emoji item. - setHighlightedIndex((prevState) => (prevState === index ? -1 : prevState)) - } - isFocused={isEmojiFocused} - isHighlighted={index === highlightedIndex} - isUsingKeyboardMovement={isUsingKeyboardMovement} - /> - ); - }, []); + isFocused={isEmojiFocused} + isHighlighted={index === highlightedIndex} + isUsingKeyboardMovement={isUsingKeyboardMovement} + /> + ); + }, + [arePointerEventsDisabled, isUsingKeyboardMovement, highlightedIndex, onEmojiSelected, preferredSkinTone, translate], + ); const isFiltered = emojis.current.length !== filteredEmojis.length; const listStyle = StyleUtils.getEmojiPickerListHeight(isFiltered, windowHeight); From 3ccc9a87b6e2624cd5519f485871f3a7cd32fb7c Mon Sep 17 00:00:00 2001 From: Ashutosh Khanduala Date: Thu, 12 Oct 2023 13:55:13 +0530 Subject: [PATCH 42/45] fix: eslint error: usused variable error --- src/components/EmojiPicker/EmojiPickerMenu/index.js | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.js b/src/components/EmojiPicker/EmojiPickerMenu/index.js index 5b7ce6d2642a..25c137a3163a 100755 --- a/src/components/EmojiPicker/EmojiPickerMenu/index.js +++ b/src/components/EmojiPicker/EmojiPickerMenu/index.js @@ -49,7 +49,7 @@ const defaultProps = { }; function EmojiPickerMenu(props) { - const {forwardedRef, frequentlyUsedEmojis, preferredSkinTone, onEmojiSelected, preferredLocale, isSmallScreenWidth, windowWidth, windowHeight, translate} = props; + const {forwardedRef, frequentlyUsedEmojis, preferredSkinTone, onEmojiSelected, preferredLocale, isSmallScreenWidth, windowHeight, translate} = props; // Ref for the emoji search input const searchInputRef = useRef(null); @@ -387,15 +387,6 @@ function EmojiPickerMenu(props) { [preferredLocale], ); - /** - * Check if its a landscape mode of mobile device - * - * @returns {Boolean} - */ - function isMobileLandscape() { - return isSmallScreenWidth && windowWidth >= windowHeight; - } - /** * @param {Number} skinTone */ From 1c0320a9a0ed32dba4d6ca32abb941a47e574c0b Mon Sep 17 00:00:00 2001 From: Ashutosh Khanduala Date: Thu, 12 Oct 2023 14:14:49 +0530 Subject: [PATCH 43/45] fix: eslint "warnings" about useCallback & useEffect deps arr --- .../EmojiPicker/EmojiPickerMenu/index.js | 297 +++++++++--------- 1 file changed, 149 insertions(+), 148 deletions(-) diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.js b/src/components/EmojiPicker/EmojiPickerMenu/index.js index 25c137a3163a..9a407604f840 100755 --- a/src/components/EmojiPicker/EmojiPickerMenu/index.js +++ b/src/components/EmojiPicker/EmojiPickerMenu/index.js @@ -133,12 +133,12 @@ function EmojiPickerMenu(props) { firstNonHeaderIndex.current = _.findIndex(filteredEmojisArr, (item) => !item.spacer && !item.header); } - const mouseMoveHandler = () => { + const mouseMoveHandler = useCallback(() => { if (!arePointerEventsDisabled) { return; } setArePointerEventsDisabled(false); - }; + }, [arePointerEventsDisabled]); /** * This function will be used with FlatList getItemLayout property for optimization purpose that allows skipping @@ -164,148 +164,179 @@ function EmojiPickerMenu(props) { searchInputRef.current.focus(); } + const filterEmojis = useCallback(() => { + const debouncedFilterEmojis = _.debounce((searchTerm) => { + const normalizedSearchTerm = searchTerm.toLowerCase().trim().replaceAll(':', ''); + if (emojiListRef.current) { + emojiListRef.current.scrollToOffset({offset: 0, animated: false}); + } + if (normalizedSearchTerm === '') { + // There are no headers when searching, so we need to re-make them sticky when there is no search term + setFilteredEmojis(emojis.current); + setHeaderIndices(headerRowIndices.current); + setHighlightedIndex(-1); + updateFirstNonHeaderIndex(emojis.current); + return; + } + const newFilteredEmojiList = EmojiUtils.suggestEmojis(`:${normalizedSearchTerm}`, preferredLocale, emojis.current.length); + + // Remove sticky header indices. There are no headers while searching and we don't want to make emojis sticky + setFilteredEmojis(newFilteredEmojiList); + setHeaderIndices([]); + setHighlightedIndex(0); + updateFirstNonHeaderIndex(newFilteredEmojiList); + }, 300); + debouncedFilterEmojis(); + }, [preferredLocale]); + /** * Highlights emojis adjacent to the currently highlighted emoji depending on the arrowKey * @param {String} arrowKey */ - function highlightAdjacentEmoji(arrowKey) { - if (filteredEmojis.length === 0) { - return; - } - - // Arrow Down and Arrow Right enable arrow navigation when search is focused - if (searchInputRef.current && searchInputRef.current.isFocused()) { - if (arrowKey !== 'ArrowDown' && arrowKey !== 'ArrowRight') { + const highlightAdjacentEmoji = useCallback( + (arrowKey) => { + if (filteredEmojis.length === 0) { return; } - if (arrowKey === 'ArrowRight' && !(searchInputRef.current.value.length === selection.start && selection.start === selection.end)) { - return; - } + // Arrow Down and Arrow Right enable arrow navigation when search is focused + if (searchInputRef.current && searchInputRef.current.isFocused()) { + if (arrowKey !== 'ArrowDown' && arrowKey !== 'ArrowRight') { + return; + } - // Blur the input, change the highlight type to keyboard, and disable pointer events - searchInputRef.current.blur(); - setArePointerEventsDisabled(true); - setIsUsingKeyboardMovement(true); + if (arrowKey === 'ArrowRight' && !(searchInputRef.current.value.length === selection.start && selection.start === selection.end)) { + return; + } - // We only want to hightlight the Emoji if none was highlighted already - // If we already have a highlighted Emoji, lets just skip the first navigation - if (highlightedIndex !== -1) { - return; - } - } + // Blur the input, change the highlight type to keyboard, and disable pointer events + searchInputRef.current.blur(); + setArePointerEventsDisabled(true); + setIsUsingKeyboardMovement(true); - // If nothing is highlighted and an arrow key is pressed - // select the first emoji, apply keyboard movement styles, and disable pointer events - if (highlightedIndex === -1) { - setHighlightedIndex(firstNonHeaderIndex.current); - setArePointerEventsDisabled(true); - setIsUsingKeyboardMovement(true); - return; - } + // We only want to hightlight the Emoji if none was highlighted already + // If we already have a highlighted Emoji, lets just skip the first navigation + if (highlightedIndex !== -1) { + return; + } + } - let newIndex = highlightedIndex; - const move = (steps, boundsCheck, onBoundReached = () => {}) => { - if (boundsCheck()) { - onBoundReached(); + // If nothing is highlighted and an arrow key is pressed + // select the first emoji, apply keyboard movement styles, and disable pointer events + if (highlightedIndex === -1) { + setHighlightedIndex(firstNonHeaderIndex.current); + setArePointerEventsDisabled(true); + setIsUsingKeyboardMovement(true); return; } - // Move in the prescribed direction until we reach an element that isn't a header - const isHeader = (e) => e.header || e.spacer; - do { - newIndex += steps; - if (newIndex < 0) { - break; + let newIndex = highlightedIndex; + const move = (steps, boundsCheck, onBoundReached = () => {}) => { + if (boundsCheck()) { + onBoundReached(); + return; } - } while (isHeader(filteredEmojis[newIndex])); - }; - switch (arrowKey) { - case 'ArrowDown': - move(CONST.EMOJI_NUM_PER_ROW, () => highlightedIndex + CONST.EMOJI_NUM_PER_ROW > filteredEmojis.length - 1); - break; - case 'ArrowLeft': - move( - -1, - () => highlightedIndex - 1 < firstNonHeaderIndex.current, - () => { - // Reaching start of the list, arrow left set the focus to searchInput. - focusInputWithTextSelect(); - newIndex = -1; - }, - ); - break; - case 'ArrowRight': - move(1, () => highlightedIndex + 1 > filteredEmojis.length - 1); - break; - case 'ArrowUp': - move( - -CONST.EMOJI_NUM_PER_ROW, - () => highlightedIndex - CONST.EMOJI_NUM_PER_ROW < firstNonHeaderIndex.current, - () => { - // Reaching start of the list, arrow up set the focus to searchInput. - focusInputWithTextSelect(); - newIndex = -1; - }, - ); - break; - default: - break; - } + // Move in the prescribed direction until we reach an element that isn't a header + const isHeader = (e) => e.header || e.spacer; + do { + newIndex += steps; + if (newIndex < 0) { + break; + } + } while (isHeader(filteredEmojis[newIndex])); + }; - // Actually highlight the new emoji, apply keyboard movement styles, and disable pointer events - if (newIndex !== highlightedIndex) { - setHighlightedIndex(newIndex); - setArePointerEventsDisabled(true); - setIsUsingKeyboardMovement(true); - } - } + switch (arrowKey) { + case 'ArrowDown': + move(CONST.EMOJI_NUM_PER_ROW, () => highlightedIndex + CONST.EMOJI_NUM_PER_ROW > filteredEmojis.length - 1); + break; + case 'ArrowLeft': + move( + -1, + () => highlightedIndex - 1 < firstNonHeaderIndex.current, + () => { + // Reaching start of the list, arrow left set the focus to searchInput. + focusInputWithTextSelect(); + newIndex = -1; + }, + ); + break; + case 'ArrowRight': + move(1, () => highlightedIndex + 1 > filteredEmojis.length - 1); + break; + case 'ArrowUp': + move( + -CONST.EMOJI_NUM_PER_ROW, + () => highlightedIndex - CONST.EMOJI_NUM_PER_ROW < firstNonHeaderIndex.current, + () => { + // Reaching start of the list, arrow up set the focus to searchInput. + focusInputWithTextSelect(); + newIndex = -1; + }, + ); + break; + default: + break; + } - const keyDownHandler = (keyBoardEvent) => { - if (keyBoardEvent.key.startsWith('Arrow')) { - if (!isFocused || keyBoardEvent.key === 'ArrowUp' || keyBoardEvent.key === 'ArrowDown') { - keyBoardEvent.preventDefault(); + // Actually highlight the new emoji, apply keyboard movement styles, and disable pointer events + if (newIndex !== highlightedIndex) { + setHighlightedIndex(newIndex); + setArePointerEventsDisabled(true); + setIsUsingKeyboardMovement(true); } + }, + [filteredEmojis, highlightedIndex, selection.end, selection.start], + ); - // Move the highlight when arrow keys are pressed - highlightAdjacentEmoji(keyBoardEvent.key); - return; - } + const keyDownHandler = useCallback( + (keyBoardEvent) => { + if (keyBoardEvent.key.startsWith('Arrow')) { + if (!isFocused || keyBoardEvent.key === 'ArrowUp' || keyBoardEvent.key === 'ArrowDown') { + keyBoardEvent.preventDefault(); + } - // Select the currently highlighted emoji if enter is pressed - if (!isEnterWhileComposition(keyBoardEvent) && keyBoardEvent.key === CONST.KEYBOARD_SHORTCUTS.ENTER.shortcutKey && highlightedIndex !== -1) { - const item = filteredEmojis[highlightedIndex]; - if (!item) { + // Move the highlight when arrow keys are pressed + highlightAdjacentEmoji(keyBoardEvent.key); return; } - const emoji = lodashGet(item, ['types', preferredSkinTone], item.code); - onEmojiSelected(emoji, item); - return; - } - // Enable keyboard movement if tab or enter is pressed or if shift is pressed while the input - // is not focused, so that the navigation and tab cycling can be done using the keyboard without - // interfering with the input behaviour. - if (keyBoardEvent.key === 'Tab' || keyBoardEvent.key === 'Enter' || (keyBoardEvent.key === 'Shift' && searchInputRef.current && !searchInputRef.current.isFocused())) { - setIsUsingKeyboardMovement(true); - return; - } + // Select the currently highlighted emoji if enter is pressed + if (!isEnterWhileComposition(keyBoardEvent) && keyBoardEvent.key === CONST.KEYBOARD_SHORTCUTS.ENTER.shortcutKey && highlightedIndex !== -1) { + const item = filteredEmojis[highlightedIndex]; + if (!item) { + return; + } + const emoji = lodashGet(item, ['types', preferredSkinTone], item.code); + onEmojiSelected(emoji, item); + return; + } - // We allow typing in the search box if any key is pressed apart from Arrow keys. - if (searchInputRef.current && !searchInputRef.current.isFocused()) { - setSelectTextOnFocus(false); - searchInputRef.current.focus(); + // Enable keyboard movement if tab or enter is pressed or if shift is pressed while the input + // is not focused, so that the navigation and tab cycling can be done using the keyboard without + // interfering with the input behaviour. + if (keyBoardEvent.key === 'Tab' || keyBoardEvent.key === 'Enter' || (keyBoardEvent.key === 'Shift' && searchInputRef.current && !searchInputRef.current.isFocused())) { + setIsUsingKeyboardMovement(true); + return; + } - // Re-enable selection on the searchInput - setSelectTextOnFocus(true); - } - }; + // We allow typing in the search box if any key is pressed apart from Arrow keys. + if (searchInputRef.current && !searchInputRef.current.isFocused()) { + setSelectTextOnFocus(false); + searchInputRef.current.focus(); + + // Re-enable selection on the searchInput + setSelectTextOnFocus(true); + } + }, + [filteredEmojis, highlightAdjacentEmoji, highlightedIndex, isFocused, onEmojiSelected, preferredSkinTone], + ); /** * Setup and attach keypress/mouse handlers for highlight navigation. */ - function setupEventHandlers() { + const setupEventHandlers = useCallback(() => { if (!document) { return; } @@ -316,19 +347,19 @@ function EmojiPickerMenu(props) { // Re-enable pointer events and hovering over EmojiPickerItems when the mouse moves document.addEventListener('mousemove', mouseMoveHandler); - } + }, [keyDownHandler, mouseMoveHandler]); /** * Cleanup all mouse/keydown event listeners that we've set up */ - function cleanupEventHandlers() { + const cleanupEventHandlers = useCallback(() => { if (!document) { return; } document.removeEventListener('keydown', keyDownHandler, true); document.removeEventListener('mousemove', mouseMoveHandler); - } + }, [keyDownHandler, mouseMoveHandler]); useEffect(() => { // This callback prop is used by the parent component using the constructor to @@ -345,7 +376,7 @@ function EmojiPickerMenu(props) { return () => { cleanupEventHandlers(); }; - }, [forwardedRef, shouldFocusInputOnScreenFocus]); + }, [forwardedRef, shouldFocusInputOnScreenFocus, cleanupEventHandlers, setupEventHandlers]); const scrollToHeader = useCallback((headerIndex) => { if (!emojiListRef.current) { @@ -357,36 +388,6 @@ function EmojiPickerMenu(props) { emojiListRef.current.scrollToOffset({offset: calculatedOffset, animated: true}); }, []); - /** - * Filter the entire list of emojis to only emojis that have the search term in their keywords - * - * @param {String} searchTerm - */ - const filterEmojis = useCallback( - _.debounce((searchTerm) => { - const normalizedSearchTerm = searchTerm.toLowerCase().trim().replaceAll(':', ''); - if (emojiListRef.current) { - emojiListRef.current.scrollToOffset({offset: 0, animated: false}); - } - if (normalizedSearchTerm === '') { - // There are no headers when searching, so we need to re-make them sticky when there is no search term - setFilteredEmojis(emojis.current); - setHeaderIndices(headerRowIndices.current); - setHighlightedIndex(-1); - updateFirstNonHeaderIndex(emojis.current); - return; - } - const newFilteredEmojiList = EmojiUtils.suggestEmojis(`:${normalizedSearchTerm}`, preferredLocale, emojis.current.length); - - // Remove sticky header indices. There are no headers while searching and we don't want to make emojis sticky - setFilteredEmojis(newFilteredEmojiList); - setHeaderIndices([]); - setHighlightedIndex(0); - updateFirstNonHeaderIndex(newFilteredEmojiList); - }, 300), - [preferredLocale], - ); - /** * @param {Number} skinTone */ From 7098e80aef7d3399837286b512405ac525af4d22 Mon Sep 17 00:00:00 2001 From: Ashutosh Khanduala Date: Fri, 13 Oct 2023 05:44:19 +0530 Subject: [PATCH 44/45] fix: emoji filter not working --- .../EmojiPicker/EmojiPickerMenu/index.js | 45 +++++++++---------- 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.js b/src/components/EmojiPicker/EmojiPickerMenu/index.js index 9a407604f840..292e435a3e52 100755 --- a/src/components/EmojiPicker/EmojiPickerMenu/index.js +++ b/src/components/EmojiPicker/EmojiPickerMenu/index.js @@ -164,30 +164,27 @@ function EmojiPickerMenu(props) { searchInputRef.current.focus(); } - const filterEmojis = useCallback(() => { - const debouncedFilterEmojis = _.debounce((searchTerm) => { - const normalizedSearchTerm = searchTerm.toLowerCase().trim().replaceAll(':', ''); - if (emojiListRef.current) { - emojiListRef.current.scrollToOffset({offset: 0, animated: false}); - } - if (normalizedSearchTerm === '') { - // There are no headers when searching, so we need to re-make them sticky when there is no search term - setFilteredEmojis(emojis.current); - setHeaderIndices(headerRowIndices.current); - setHighlightedIndex(-1); - updateFirstNonHeaderIndex(emojis.current); - return; - } - const newFilteredEmojiList = EmojiUtils.suggestEmojis(`:${normalizedSearchTerm}`, preferredLocale, emojis.current.length); - - // Remove sticky header indices. There are no headers while searching and we don't want to make emojis sticky - setFilteredEmojis(newFilteredEmojiList); - setHeaderIndices([]); - setHighlightedIndex(0); - updateFirstNonHeaderIndex(newFilteredEmojiList); - }, 300); - debouncedFilterEmojis(); - }, [preferredLocale]); + const filterEmojis = _.debounce((searchTerm) => { + const normalizedSearchTerm = searchTerm.toLowerCase().trim().replaceAll(':', ''); + if (emojiListRef.current) { + emojiListRef.current.scrollToOffset({offset: 0, animated: false}); + } + if (normalizedSearchTerm === '') { + // There are no headers when searching, so we need to re-make them sticky when there is no search term + setFilteredEmojis(emojis.current); + setHeaderIndices(headerRowIndices.current); + setHighlightedIndex(-1); + updateFirstNonHeaderIndex(emojis.current); + return; + } + const newFilteredEmojiList = EmojiUtils.suggestEmojis(`:${normalizedSearchTerm}`, preferredLocale, emojis.current.length); + + // Remove sticky header indices. There are no headers while searching and we don't want to make emojis sticky + setFilteredEmojis(newFilteredEmojiList); + setHeaderIndices([]); + setHighlightedIndex(0); + updateFirstNonHeaderIndex(newFilteredEmojiList); + }, 300); /** * Highlights emojis adjacent to the currently highlighted emoji depending on the arrowKey From 66b49691a04c10041f3c3b503ff39b1b1a8f7a5a Mon Sep 17 00:00:00 2001 From: Ashutosh Khanduala Date: Tue, 17 Oct 2023 09:00:05 +0530 Subject: [PATCH 45/45] fix: eslint warn -- extra deps in useCallaback --- src/components/EmojiPicker/EmojiPickerMenu/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.js b/src/components/EmojiPicker/EmojiPickerMenu/index.js index cd2d7ca61b22..b5ebc09c03af 100755 --- a/src/components/EmojiPicker/EmojiPickerMenu/index.js +++ b/src/components/EmojiPicker/EmojiPickerMenu/index.js @@ -456,7 +456,7 @@ function EmojiPickerMenu(props) { /> ); }, - [arePointerEventsDisabled, isUsingKeyboardMovement, highlightedIndex, onEmojiSelected, preferredSkinTone, translate], + [isUsingKeyboardMovement, highlightedIndex, onEmojiSelected, preferredSkinTone, translate], ); const isFiltered = emojis.current.length !== filteredEmojis.length;