From 9fcd8edce5a9131c22bf7c151d40f1b9059e25ab Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Mon, 22 Jan 2024 15:27:02 +0100 Subject: [PATCH 01/19] add left and right arrow support in useArrowKeyFocusManager --- src/hooks/useArrowKeyFocusManager.ts | 108 +++++++++++++++++++++++++-- 1 file changed, 102 insertions(+), 6 deletions(-) diff --git a/src/hooks/useArrowKeyFocusManager.ts b/src/hooks/useArrowKeyFocusManager.ts index ecd494dfd9ea..5494d83865d6 100644 --- a/src/hooks/useArrowKeyFocusManager.ts +++ b/src/hooks/useArrowKeyFocusManager.ts @@ -9,6 +9,8 @@ type Config = { disabledIndexes?: readonly number[]; shouldExcludeTextAreaNodes?: boolean; isActive?: boolean; + itemsPerRow?: number; + disableCyclicTraversal?: boolean; }; type UseArrowKeyFocusManager = [number, (index: number) => void]; @@ -24,6 +26,9 @@ type UseArrowKeyFocusManager = [number, (index: number) => void]; * @param [config.disabledIndexes] – An array of indexes to disable + skip over * @param [config.shouldExcludeTextAreaNodes] – Whether arrow keys should have any effect when a TextArea node is focused * @param [config.isActive] – Whether the component is ready and should subscribe to KeyboardShortcut + * @param [config.allowHorizontalArrowKeys] – Whether to allow horizontal arrow keys to have any effect + * @param [config.itemsPerRow] – The number of items per row. If provided, the arrow keys will move focus horizontally as well as vertically + * @param [config.disableCyclicTraversal] – Whether to disable cyclic traversal of the list. If true, the arrow keys will have no effect when the first or last item is focused */ export default function useArrowKeyFocusManager({ maxIndex, @@ -35,7 +40,10 @@ export default function useArrowKeyFocusManager({ disabledIndexes = CONST.EMPTY_ARRAY, shouldExcludeTextAreaNodes = true, isActive, + itemsPerRow, + disableCyclicTraversal = false, }: Config): UseArrowKeyFocusManager { + const allowHorizontalArrowKeys = !!itemsPerRow; const [focusedIndex, setFocusedIndex] = useState(initialFocusedIndex); const arrowConfig = useMemo( () => ({ @@ -51,13 +59,23 @@ export default function useArrowKeyFocusManager({ if (maxIndex < 0) { return; } + const nextIndex = disableCyclicTraversal ? -1 : maxIndex; setFocusedIndex((actualIndex) => { - const currentFocusedIndex = actualIndex > 0 ? actualIndex - 1 : maxIndex; + let currentFocusedIndex = -1; + if (allowHorizontalArrowKeys) { + currentFocusedIndex = actualIndex > 0 ? actualIndex - itemsPerRow : nextIndex; + } else { + currentFocusedIndex = actualIndex > 0 ? actualIndex - 1 : nextIndex; + } let newFocusedIndex = currentFocusedIndex; while (disabledIndexes.includes(newFocusedIndex)) { - newFocusedIndex = newFocusedIndex > 0 ? newFocusedIndex - 1 : maxIndex; + if (allowHorizontalArrowKeys) { + newFocusedIndex = newFocusedIndex > 0 ? newFocusedIndex - itemsPerRow : nextIndex; + } else { + newFocusedIndex = newFocusedIndex > 0 ? newFocusedIndex - 1 : nextIndex; + } if (newFocusedIndex === currentFocusedIndex) { // all indexes are disabled return actualIndex; // no-op @@ -65,7 +83,7 @@ export default function useArrowKeyFocusManager({ } return newFocusedIndex; }); - }, [disabledIndexes, maxIndex]); + }, [allowHorizontalArrowKeys, disableCyclicTraversal, disabledIndexes, itemsPerRow, maxIndex]); useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ARROW_UP, arrowUpCallback, arrowConfig); const arrowDownCallback = useCallback(() => { @@ -73,12 +91,24 @@ export default function useArrowKeyFocusManager({ return; } + const nextIndex = disableCyclicTraversal ? maxIndex : 0; + setFocusedIndex((actualIndex) => { - const currentFocusedIndex = actualIndex < maxIndex ? actualIndex + 1 : 0; + let currentFocusedIndex = -1; + if (allowHorizontalArrowKeys) { + currentFocusedIndex = actualIndex < maxIndex ? actualIndex + itemsPerRow : nextIndex; + } else { + currentFocusedIndex = actualIndex < maxIndex ? actualIndex + 1 : nextIndex; + } + let newFocusedIndex = currentFocusedIndex; while (disabledIndexes.includes(newFocusedIndex)) { - newFocusedIndex = newFocusedIndex < maxIndex ? newFocusedIndex + 1 : 0; + if (allowHorizontalArrowKeys) { + newFocusedIndex = newFocusedIndex < maxIndex ? newFocusedIndex + itemsPerRow : nextIndex; + } else { + newFocusedIndex = newFocusedIndex < maxIndex ? newFocusedIndex + 1 : nextIndex; + } if (newFocusedIndex === currentFocusedIndex) { // all indexes are disabled return actualIndex; @@ -87,9 +117,75 @@ export default function useArrowKeyFocusManager({ return newFocusedIndex; }); - }, [disabledIndexes, maxIndex]); + }, [allowHorizontalArrowKeys, disableCyclicTraversal, disabledIndexes, itemsPerRow, maxIndex]); useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ARROW_DOWN, arrowDownCallback, arrowConfig); + const arrowLeftCallback = useCallback(() => { + if (maxIndex < 0) { + return; + } + + const nextIndex = disableCyclicTraversal ? -1 : maxIndex; + + setFocusedIndex((actualIndex) => { + let currentFocusedIndex = -1; + if (allowHorizontalArrowKeys) { + currentFocusedIndex = actualIndex > 0 ? actualIndex - 1 : nextIndex; + } else { + currentFocusedIndex = actualIndex > 0 ? actualIndex - 1 : nextIndex; + } + let newFocusedIndex = currentFocusedIndex; + + while (disabledIndexes.includes(newFocusedIndex)) { + if (allowHorizontalArrowKeys) { + newFocusedIndex = newFocusedIndex > 0 ? newFocusedIndex - 1 : nextIndex; + } else { + newFocusedIndex = newFocusedIndex > 0 ? newFocusedIndex - 1 : nextIndex; + } + if (newFocusedIndex === currentFocusedIndex) { + // all indexes are disabled + return actualIndex; // no-op + } + } + return newFocusedIndex; + }); + }, [allowHorizontalArrowKeys, disableCyclicTraversal, disabledIndexes, maxIndex]); + useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ARROW_LEFT, allowHorizontalArrowKeys ? arrowLeftCallback : () => {}, arrowConfig); + + const arrowRightCallback = useCallback(() => { + if (maxIndex < 0) { + return; + } + + const nextIndex = disableCyclicTraversal ? maxIndex : 0; + + setFocusedIndex((actualIndex) => { + let currentFocusedIndex = -1; + if (allowHorizontalArrowKeys) { + currentFocusedIndex = actualIndex < maxIndex ? actualIndex + 1 : nextIndex; + } else { + currentFocusedIndex = actualIndex < maxIndex ? actualIndex + 1 : nextIndex; + } + + let newFocusedIndex = currentFocusedIndex; + + while (disabledIndexes.includes(newFocusedIndex)) { + if (allowHorizontalArrowKeys) { + newFocusedIndex = newFocusedIndex < maxIndex ? newFocusedIndex + 1 : nextIndex; + } else { + newFocusedIndex = newFocusedIndex < maxIndex ? newFocusedIndex + 1 : nextIndex; + } + if (newFocusedIndex === currentFocusedIndex) { + // all indexes are disabled + return actualIndex; + } + } + + return newFocusedIndex; + }); + }, [allowHorizontalArrowKeys, disableCyclicTraversal, disabledIndexes, maxIndex]); + useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ARROW_RIGHT, allowHorizontalArrowKeys ? arrowRightCallback : () => {}, arrowConfig); + // Note: you don't need to manually manage focusedIndex in the parent. setFocusedIndex is only exposed in case you want to reset focusedIndex or focus a specific item return [focusedIndex, setFocusedIndex]; } From b2f6d98746bd38d3770e46afc79d0bb4ff40a4bd Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Mon, 22 Jan 2024 15:27:03 +0100 Subject: [PATCH 02/19] update useArrowKeyFocusManager down & up behavior --- src/hooks/useArrowKeyFocusManager.ts | 25 +++++-------------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/src/hooks/useArrowKeyFocusManager.ts b/src/hooks/useArrowKeyFocusManager.ts index 5494d83865d6..b5dd7189a33d 100644 --- a/src/hooks/useArrowKeyFocusManager.ts +++ b/src/hooks/useArrowKeyFocusManager.ts @@ -71,11 +71,7 @@ export default function useArrowKeyFocusManager({ let newFocusedIndex = currentFocusedIndex; while (disabledIndexes.includes(newFocusedIndex)) { - if (allowHorizontalArrowKeys) { - newFocusedIndex = newFocusedIndex > 0 ? newFocusedIndex - itemsPerRow : nextIndex; - } else { - newFocusedIndex = newFocusedIndex > 0 ? newFocusedIndex - 1 : nextIndex; - } + newFocusedIndex = newFocusedIndex > 0 ? newFocusedIndex - 1 : nextIndex; if (newFocusedIndex === currentFocusedIndex) { // all indexes are disabled return actualIndex; // no-op @@ -104,11 +100,7 @@ export default function useArrowKeyFocusManager({ let newFocusedIndex = currentFocusedIndex; while (disabledIndexes.includes(newFocusedIndex)) { - if (allowHorizontalArrowKeys) { - newFocusedIndex = newFocusedIndex < maxIndex ? newFocusedIndex + itemsPerRow : nextIndex; - } else { - newFocusedIndex = newFocusedIndex < maxIndex ? newFocusedIndex + 1 : nextIndex; - } + newFocusedIndex = newFocusedIndex < maxIndex ? newFocusedIndex + 1 : nextIndex; if (newFocusedIndex === currentFocusedIndex) { // all indexes are disabled return actualIndex; @@ -129,11 +121,8 @@ export default function useArrowKeyFocusManager({ setFocusedIndex((actualIndex) => { let currentFocusedIndex = -1; - if (allowHorizontalArrowKeys) { - currentFocusedIndex = actualIndex > 0 ? actualIndex - 1 : nextIndex; - } else { - currentFocusedIndex = actualIndex > 0 ? actualIndex - 1 : nextIndex; - } + currentFocusedIndex = actualIndex > 0 ? actualIndex - 1 : nextIndex; + let newFocusedIndex = currentFocusedIndex; while (disabledIndexes.includes(newFocusedIndex)) { @@ -161,11 +150,7 @@ export default function useArrowKeyFocusManager({ setFocusedIndex((actualIndex) => { let currentFocusedIndex = -1; - if (allowHorizontalArrowKeys) { - currentFocusedIndex = actualIndex < maxIndex ? actualIndex + 1 : nextIndex; - } else { - currentFocusedIndex = actualIndex < maxIndex ? actualIndex + 1 : nextIndex; - } + currentFocusedIndex = actualIndex < maxIndex ? actualIndex + 1 : nextIndex; let newFocusedIndex = currentFocusedIndex; From 718b67b01d976a2aea7694e6afb9ca379b9c8b49 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Mon, 22 Jan 2024 15:27:03 +0100 Subject: [PATCH 03/19] add arrow key to hook --- src/hooks/useArrowKeyFocusManager.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/hooks/useArrowKeyFocusManager.ts b/src/hooks/useArrowKeyFocusManager.ts index b5dd7189a33d..759b50b7805c 100644 --- a/src/hooks/useArrowKeyFocusManager.ts +++ b/src/hooks/useArrowKeyFocusManager.ts @@ -4,7 +4,7 @@ import useKeyboardShortcut from './useKeyboardShortcut'; type Config = { maxIndex: number; - onFocusedIndexChange?: (index: number) => void; + onFocusedIndexChange?: (index: number, keyboardShortcut?: 'ArrowLeft' | 'ArrowRight' | 'ArrowUp' | 'ArrowDown') => void; initialFocusedIndex?: number; disabledIndexes?: readonly number[]; shouldExcludeTextAreaNodes?: boolean; @@ -45,6 +45,7 @@ export default function useArrowKeyFocusManager({ }: Config): UseArrowKeyFocusManager { const allowHorizontalArrowKeys = !!itemsPerRow; const [focusedIndex, setFocusedIndex] = useState(initialFocusedIndex); + const [lastUsedArrowKey, setLastUsedArrowKey] = useState<'ArrowLeft' | 'ArrowRight' | 'ArrowUp' | 'ArrowDown'>(); const arrowConfig = useMemo( () => ({ excludedNodes: shouldExcludeTextAreaNodes ? ['TEXTAREA'] : [], @@ -53,7 +54,8 @@ export default function useArrowKeyFocusManager({ [isActive, shouldExcludeTextAreaNodes], ); - useEffect(() => onFocusedIndexChange(focusedIndex), [focusedIndex, onFocusedIndexChange]); + // eslint-disable-next-line react-hooks/exhaustive-deps + useEffect(() => onFocusedIndexChange(focusedIndex, lastUsedArrowKey), [focusedIndex, onFocusedIndexChange]); const arrowUpCallback = useCallback(() => { if (maxIndex < 0) { @@ -77,6 +79,7 @@ export default function useArrowKeyFocusManager({ return actualIndex; // no-op } } + setLastUsedArrowKey('ArrowUp'); return newFocusedIndex; }); }, [allowHorizontalArrowKeys, disableCyclicTraversal, disabledIndexes, itemsPerRow, maxIndex]); @@ -98,7 +101,6 @@ export default function useArrowKeyFocusManager({ } let newFocusedIndex = currentFocusedIndex; - while (disabledIndexes.includes(newFocusedIndex)) { newFocusedIndex = newFocusedIndex < maxIndex ? newFocusedIndex + 1 : nextIndex; if (newFocusedIndex === currentFocusedIndex) { @@ -106,7 +108,7 @@ export default function useArrowKeyFocusManager({ return actualIndex; } } - + setLastUsedArrowKey('ArrowDown'); return newFocusedIndex; }); }, [allowHorizontalArrowKeys, disableCyclicTraversal, disabledIndexes, itemsPerRow, maxIndex]); @@ -136,6 +138,7 @@ export default function useArrowKeyFocusManager({ return actualIndex; // no-op } } + setLastUsedArrowKey('ArrowLeft'); return newFocusedIndex; }); }, [allowHorizontalArrowKeys, disableCyclicTraversal, disabledIndexes, maxIndex]); @@ -165,7 +168,7 @@ export default function useArrowKeyFocusManager({ return actualIndex; } } - + setLastUsedArrowKey('ArrowRight'); return newFocusedIndex; }); }, [allowHorizontalArrowKeys, disableCyclicTraversal, disabledIndexes, maxIndex]); From fcc45b8a15300c9530dc66a9011f01a4545a728d Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Mon, 22 Jan 2024 15:27:03 +0100 Subject: [PATCH 04/19] update useArrowKeyFocusManager to correctly find next index --- src/hooks/useArrowKeyFocusManager.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/hooks/useArrowKeyFocusManager.ts b/src/hooks/useArrowKeyFocusManager.ts index 759b50b7805c..95c998ec10cc 100644 --- a/src/hooks/useArrowKeyFocusManager.ts +++ b/src/hooks/useArrowKeyFocusManager.ts @@ -4,7 +4,7 @@ import useKeyboardShortcut from './useKeyboardShortcut'; type Config = { maxIndex: number; - onFocusedIndexChange?: (index: number, keyboardShortcut?: 'ArrowLeft' | 'ArrowRight' | 'ArrowUp' | 'ArrowDown') => void; + onFocusedIndexChange?: (index: number) => void; initialFocusedIndex?: number; disabledIndexes?: readonly number[]; shouldExcludeTextAreaNodes?: boolean; @@ -45,7 +45,6 @@ export default function useArrowKeyFocusManager({ }: Config): UseArrowKeyFocusManager { const allowHorizontalArrowKeys = !!itemsPerRow; const [focusedIndex, setFocusedIndex] = useState(initialFocusedIndex); - const [lastUsedArrowKey, setLastUsedArrowKey] = useState<'ArrowLeft' | 'ArrowRight' | 'ArrowUp' | 'ArrowDown'>(); const arrowConfig = useMemo( () => ({ excludedNodes: shouldExcludeTextAreaNodes ? ['TEXTAREA'] : [], @@ -55,7 +54,7 @@ export default function useArrowKeyFocusManager({ ); // eslint-disable-next-line react-hooks/exhaustive-deps - useEffect(() => onFocusedIndexChange(focusedIndex, lastUsedArrowKey), [focusedIndex, onFocusedIndexChange]); + useEffect(() => onFocusedIndexChange(focusedIndex), [focusedIndex, onFocusedIndexChange]); const arrowUpCallback = useCallback(() => { if (maxIndex < 0) { @@ -71,15 +70,16 @@ export default function useArrowKeyFocusManager({ currentFocusedIndex = actualIndex > 0 ? actualIndex - 1 : nextIndex; } let newFocusedIndex = currentFocusedIndex; - while (disabledIndexes.includes(newFocusedIndex)) { - newFocusedIndex = newFocusedIndex > 0 ? newFocusedIndex - 1 : nextIndex; + newFocusedIndex -= allowHorizontalArrowKeys ? itemsPerRow : 1; + if (newFocusedIndex < 0) { + break; + } if (newFocusedIndex === currentFocusedIndex) { // all indexes are disabled return actualIndex; // no-op } } - setLastUsedArrowKey('ArrowUp'); return newFocusedIndex; }); }, [allowHorizontalArrowKeys, disableCyclicTraversal, disabledIndexes, itemsPerRow, maxIndex]); @@ -102,13 +102,15 @@ export default function useArrowKeyFocusManager({ let newFocusedIndex = currentFocusedIndex; while (disabledIndexes.includes(newFocusedIndex)) { - newFocusedIndex = newFocusedIndex < maxIndex ? newFocusedIndex + 1 : nextIndex; + newFocusedIndex += allowHorizontalArrowKeys ? itemsPerRow : 1; + if (newFocusedIndex < 0) { + break; + } if (newFocusedIndex === currentFocusedIndex) { // all indexes are disabled return actualIndex; } } - setLastUsedArrowKey('ArrowDown'); return newFocusedIndex; }); }, [allowHorizontalArrowKeys, disableCyclicTraversal, disabledIndexes, itemsPerRow, maxIndex]); @@ -138,7 +140,6 @@ export default function useArrowKeyFocusManager({ return actualIndex; // no-op } } - setLastUsedArrowKey('ArrowLeft'); return newFocusedIndex; }); }, [allowHorizontalArrowKeys, disableCyclicTraversal, disabledIndexes, maxIndex]); @@ -168,7 +169,6 @@ export default function useArrowKeyFocusManager({ return actualIndex; } } - setLastUsedArrowKey('ArrowRight'); return newFocusedIndex; }); }, [allowHorizontalArrowKeys, disableCyclicTraversal, disabledIndexes, maxIndex]); From 37525aa3cbefd4842219d46fad7df16fa3214492 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Mon, 22 Jan 2024 15:27:04 +0100 Subject: [PATCH 05/19] use horizontal arrows with emoji picker menu --- .../EmojiPicker/EmojiPickerMenu/index.js | 232 ++++-------------- .../EmojiPickerMenu/useEmojiPickerMenu.js | 2 + src/hooks/useArrowKeyFocusManager.ts | 10 +- src/libs/EmojiUtils.ts | 13 + 4 files changed, 71 insertions(+), 186 deletions(-) diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.js b/src/components/EmojiPicker/EmojiPickerMenu/index.js index a723eed446a4..32b3ea64deef 100755 --- a/src/components/EmojiPicker/EmojiPickerMenu/index.js +++ b/src/components/EmojiPicker/EmojiPickerMenu/index.js @@ -1,12 +1,12 @@ -import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; -import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import React, {useCallback, useEffect, useRef, useState} from 'react'; import {View} from 'react-native'; import {scrollTo} from 'react-native-reanimated'; import _ from 'underscore'; import EmojiPickerMenuItem from '@components/EmojiPicker/EmojiPickerMenuItem'; import Text from '@components/Text'; import TextInput from '@components/TextInput'; +import useArrowKeyFocusManager from '@hooks/useArrowKeyFocusManager'; import useLocalize from '@hooks/useLocalize'; import useSingleExecution from '@hooks/useSingleExecution'; import useStyleUtils from '@hooks/useStyleUtils'; @@ -14,8 +14,6 @@ import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import * as Browser from '@libs/Browser'; import canFocusInputOnScreenFocus from '@libs/canFocusInputOnScreenFocus'; -import isEnterWhileComposition from '@libs/KeyboardShortcut/isEnterWhileComposition'; -import * as ReportUtils from '@libs/ReportUtils'; import CONST from '@src/CONST'; import BaseEmojiPickerMenu from './BaseEmojiPickerMenu'; import emojiPickerMenuPropTypes from './emojiPickerMenuPropTypes'; @@ -52,6 +50,7 @@ function EmojiPickerMenu({forwardedRef, onEmojiSelected}) { preferredSkinTone, listStyle, emojiListRef, + spacersIndexes, } = useEmojiPickerMenu(); // Ref for the emoji search input @@ -61,40 +60,48 @@ function EmojiPickerMenu({forwardedRef, onEmojiSelected}) { // prevent auto focus when open picker for mobile device const shouldFocusInputOnScreenFocus = canFocusInputOnScreenFocus(); - const [highlightedIndex, setHighlightedIndex] = useState(-1); const [arePointerEventsDisabled, setArePointerEventsDisabled] = useState(false); - const [selection, setSelection] = useState({start: 0, end: 0}); - const [isFocused, setIsFocused] = useState(false); const [isUsingKeyboardMovement, setIsUsingKeyboardMovement] = useState(false); const [highlightFirstEmoji, setHighlightFirstEmoji] = useState(false); - const firstNonHeaderIndex = useMemo(() => _.findIndex(filteredEmojis, (item) => !item.spacer && !item.header), [filteredEmojis]); - /** - * On text input selection change - * - * @param {Event} event - */ - const onSelectionChange = useCallback((event) => { - setSelection(event.nativeEvent.selection); - }, []); + const onFocusedIndexChange = useCallback( + (newIndex) => { + if (filteredEmojis.length === 0 || isListFiltered) { + return; + } + + if (!isUsingKeyboardMovement) { + setIsUsingKeyboardMovement(true); + } + + if (!arePointerEventsDisabled) { + setArePointerEventsDisabled(true); + } + // If the input is not focused and the highlighted index is -1, focus the input + if (newIndex < 0 && !searchInputRef.current.isFocused()) { + searchInputRef.current.focus(); + } + }, + [arePointerEventsDisabled, filteredEmojis.length, isListFiltered, isUsingKeyboardMovement], + ); + + const [highlightedIndex, setHighlightedIndex] = useArrowKeyFocusManager({ + maxIndex: filteredEmojis.length - 1, + disabledIndexes: isListFiltered ? [] : [...headerIndices, ...spacersIndexes], + itemsPerRow: CONST.EMOJI_NUM_PER_ROW, + initialFocusedIndex: -1, + disableCyclicTraversal: true, + onFocusedIndexChange, + }); const mouseMoveHandler = useCallback(() => { if (!arePointerEventsDisabled) { return; } + setArePointerEventsDisabled(false); }, [arePointerEventsDisabled]); - /** - * Focuses the search Input and has the text selected - */ - function focusInputWithTextSelect() { - if (!searchInputRef.current) { - return; - } - searchInputRef.current.focus(); - } - const filterEmojis = _.throttle((searchTerm) => { const [normalizedSearchTerm, newFilteredEmojiList] = suggestEmojis(searchTerm); @@ -116,151 +123,6 @@ function EmojiPickerMenu({forwardedRef, onEmojiSelected}) { setHighlightFirstEmoji(true); }, throttleTime); - /** - * Highlights emojis adjacent to the currently highlighted emoji depending on the arrowKey - * @param {String} arrowKey - */ - const highlightAdjacentEmoji = useCallback( - (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') { - return; - } - - if (arrowKey === 'ArrowRight' && !(searchInputRef.current.value.length === selection.start && selection.start === selection.end)) { - return; - } - - // Blur the input, change the highlight type to keyboard, and disable pointer events - searchInputRef.current.blur(); - setArePointerEventsDisabled(true); - setIsUsingKeyboardMovement(true); - setHighlightFirstEmoji(false); - - // 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; - } - } - - // 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); - setArePointerEventsDisabled(true); - setIsUsingKeyboardMovement(true); - return; - } - - let newIndex = 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, () => highlightedIndex + CONST.EMOJI_NUM_PER_ROW > filteredEmojis.length - 1); - break; - case 'ArrowLeft': - move( - -1, - () => highlightedIndex - 1 < firstNonHeaderIndex, - () => { - // 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, - () => { - // Reaching start of the list, arrow up set the focus to searchInput. - focusInputWithTextSelect(); - newIndex = -1; - }, - ); - break; - default: - break; - } - - // Actually highlight the new emoji, apply keyboard movement styles, and disable pointer events - if (newIndex !== highlightedIndex) { - setHighlightedIndex(newIndex); - setArePointerEventsDisabled(true); - setIsUsingKeyboardMovement(true); - } - }, - [filteredEmojis, firstNonHeaderIndex, highlightedIndex, selection.end, selection.start], - ); - - const keyDownHandler = useCallback( - (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; - } - - // 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); - // On web, avoid this Enter default input action; otherwise, it will add a new line in the subsequently focused composer. - keyBoardEvent.preventDefault(); - // On mWeb, avoid propagating this Enter keystroke to Pressable child component; otherwise, it will trigger the onEmojiSelected callback again. - keyBoardEvent.stopPropagation(); - 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; - } - - // We allow typing in the search box if any key is pressed apart from Arrow keys. - if (searchInputRef.current && !searchInputRef.current.isFocused() && ReportUtils.shouldAutoFocusOnKeyPress(keyBoardEvent)) { - searchInputRef.current.focus(); - } - }, - [filteredEmojis, highlightAdjacentEmoji, highlightedIndex, isFocused, onEmojiSelected, preferredSkinTone], - ); - /** * Setup and attach keypress/mouse handlers for highlight navigation. */ @@ -269,13 +131,9 @@ function EmojiPickerMenu({forwardedRef, onEmojiSelected}) { 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', keyDownHandler, true); - // Re-enable pointer events and hovering over EmojiPickerItems when the mouse moves document.addEventListener('mousemove', mouseMoveHandler); - }, [keyDownHandler, mouseMoveHandler]); + }, [mouseMoveHandler]); /** * Cleanup all mouse/keydown event listeners that we've set up @@ -285,9 +143,8 @@ function EmojiPickerMenu({forwardedRef, onEmojiSelected}) { return; } - document.removeEventListener('keydown', keyDownHandler, true); document.removeEventListener('mousemove', mouseMoveHandler); - }, [keyDownHandler, mouseMoveHandler]); + }, [mouseMoveHandler]); useEffect(() => { // This callback prop is used by the parent component using the constructor to @@ -350,7 +207,7 @@ function EmojiPickerMenu({forwardedRef, onEmojiSelected}) { onEmojiSelected(emoji, item))} onHoverIn={() => { - setHighlightFirstEmoji(false); + // setHighlightFirstEmoji(false); if (!isUsingKeyboardMovement) { return; } @@ -368,7 +225,19 @@ function EmojiPickerMenu({forwardedRef, onEmojiSelected}) { /> ); }, - [preferredSkinTone, highlightedIndex, isUsingKeyboardMovement, highlightFirstEmoji, singleExecution, translate, onEmojiSelected, isSmallScreenWidth, windowWidth, styles], + [ + preferredSkinTone, + highlightedIndex, + isUsingKeyboardMovement, + highlightFirstEmoji, + singleExecution, + styles, + isSmallScreenWidth, + windowWidth, + translate, + onEmojiSelected, + setHighlightedIndex, + ], ); return ( @@ -389,13 +258,10 @@ function EmojiPickerMenu({forwardedRef, onEmojiSelected}) { defaultValue="" ref={searchInputRef} autoFocus={shouldFocusInputOnScreenFocus} - onSelectionChange={onSelectionChange} onFocus={() => { setHighlightedIndex(-1); - setIsFocused(true); setIsUsingKeyboardMovement(false); }} - onBlur={() => setIsFocused(false)} autoCorrect={false} blurOnSubmit={filteredEmojis.length > 0} /> diff --git a/src/components/EmojiPicker/EmojiPickerMenu/useEmojiPickerMenu.js b/src/components/EmojiPicker/EmojiPickerMenu/useEmojiPickerMenu.js index 2d895193ec68..7caab5e6fb55 100644 --- a/src/components/EmojiPicker/EmojiPickerMenu/useEmojiPickerMenu.js +++ b/src/components/EmojiPicker/EmojiPickerMenu/useEmojiPickerMenu.js @@ -16,6 +16,7 @@ const useEmojiPickerMenu = () => { const allEmojis = useMemo(() => EmojiUtils.mergeEmojisWithFrequentlyUsedEmojis(emojis), [frequentlyUsedEmojis]); const headerEmojis = useMemo(() => EmojiUtils.getHeaderEmojis(allEmojis), [allEmojis]); const headerRowIndices = useMemo(() => _.map(headerEmojis, (headerEmoji) => headerEmoji.index), [headerEmojis]); + const spacersIndexes = useMemo(() => EmojiUtils.getSpacersIndexes(allEmojis), [allEmojis]); const [filteredEmojis, setFilteredEmojis] = useState(allEmojis); const [headerIndices, setHeaderIndices] = useState(headerRowIndices); const isListFiltered = allEmojis.length !== filteredEmojis.length; @@ -61,6 +62,7 @@ const useEmojiPickerMenu = () => { preferredSkinTone, listStyle, emojiListRef, + spacersIndexes, }; }; diff --git a/src/hooks/useArrowKeyFocusManager.ts b/src/hooks/useArrowKeyFocusManager.ts index 95c998ec10cc..b6c0e1aa4d2d 100644 --- a/src/hooks/useArrowKeyFocusManager.ts +++ b/src/hooks/useArrowKeyFocusManager.ts @@ -54,7 +54,7 @@ export default function useArrowKeyFocusManager({ ); // eslint-disable-next-line react-hooks/exhaustive-deps - useEffect(() => onFocusedIndexChange(focusedIndex), [focusedIndex, onFocusedIndexChange]); + useEffect(() => onFocusedIndexChange(focusedIndex), [focusedIndex]); const arrowUpCallback = useCallback(() => { if (maxIndex < 0) { @@ -99,10 +99,14 @@ export default function useArrowKeyFocusManager({ } else { currentFocusedIndex = actualIndex < maxIndex ? actualIndex + 1 : nextIndex; } - let newFocusedIndex = currentFocusedIndex; while (disabledIndexes.includes(newFocusedIndex)) { - newFocusedIndex += allowHorizontalArrowKeys ? itemsPerRow : 1; + if (actualIndex < 0) { + newFocusedIndex += 1; + } else { + newFocusedIndex += allowHorizontalArrowKeys ? itemsPerRow : 1; + } + if (newFocusedIndex < 0) { break; } diff --git a/src/libs/EmojiUtils.ts b/src/libs/EmojiUtils.ts index e34fa0b90fc6..3629f5a31f12 100644 --- a/src/libs/EmojiUtils.ts +++ b/src/libs/EmojiUtils.ts @@ -548,6 +548,18 @@ const getEmojiReactionDetails = (emojiName: string, reaction: ReportActionReacti }; }; +function getSpacersIndexes(allEmojis: EmojiPickerList): number[] { + const spacersIndexes: number[] = []; + allEmojis.forEach((emoji, index) => { + if (!('spacer' in emoji)) { + return; + } + + spacersIndexes.push(index); + }); + return spacersIndexes; +} + export { findEmojiByName, findEmojiByCode, @@ -570,4 +582,5 @@ export { getAddedEmojis, isFirstLetterEmoji, hasAccountIDEmojiReacted, + getSpacersIndexes, }; From b143573a7541552f4e2f622222f1fc0385e49eeb Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Mon, 22 Jan 2024 15:27:04 +0100 Subject: [PATCH 06/19] remove unused condition --- src/components/EmojiPicker/EmojiPickerMenu/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.js b/src/components/EmojiPicker/EmojiPickerMenu/index.js index 32b3ea64deef..7a4d63e68bd4 100755 --- a/src/components/EmojiPicker/EmojiPickerMenu/index.js +++ b/src/components/EmojiPicker/EmojiPickerMenu/index.js @@ -66,7 +66,7 @@ function EmojiPickerMenu({forwardedRef, onEmojiSelected}) { const onFocusedIndexChange = useCallback( (newIndex) => { - if (filteredEmojis.length === 0 || isListFiltered) { + if (filteredEmojis.length === 0) { return; } @@ -82,7 +82,7 @@ function EmojiPickerMenu({forwardedRef, onEmojiSelected}) { searchInputRef.current.focus(); } }, - [arePointerEventsDisabled, filteredEmojis.length, isListFiltered, isUsingKeyboardMovement], + [arePointerEventsDisabled, filteredEmojis.length, isUsingKeyboardMovement], ); const [highlightedIndex, setHighlightedIndex] = useArrowKeyFocusManager({ From 222ba38175d242f7ce7d119db579eed27736f426 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Mon, 22 Jan 2024 15:27:04 +0100 Subject: [PATCH 07/19] cleanup emoji picker menu --- .../EmojiPicker/EmojiPickerMenu/index.js | 95 ++++++++++++++----- src/hooks/useArrowKeyFocusManager.ts | 10 +- 2 files changed, 82 insertions(+), 23 deletions(-) diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.js b/src/components/EmojiPicker/EmojiPickerMenu/index.js index 7a4d63e68bd4..99f356ff3c21 100755 --- a/src/components/EmojiPicker/EmojiPickerMenu/index.js +++ b/src/components/EmojiPicker/EmojiPickerMenu/index.js @@ -1,3 +1,4 @@ +import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useRef, useState} from 'react'; import {View} from 'react-native'; @@ -14,6 +15,8 @@ import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import * as Browser from '@libs/Browser'; import canFocusInputOnScreenFocus from '@libs/canFocusInputOnScreenFocus'; +import isEnterWhileComposition from '@libs/KeyboardShortcut/isEnterWhileComposition'; +import * as ReportUtils from '@libs/ReportUtils'; import CONST from '@src/CONST'; import BaseEmojiPickerMenu from './BaseEmojiPickerMenu'; import emojiPickerMenuPropTypes from './emojiPickerMenuPropTypes'; @@ -62,22 +65,34 @@ function EmojiPickerMenu({forwardedRef, onEmojiSelected}) { const [arePointerEventsDisabled, setArePointerEventsDisabled] = useState(false); const [isUsingKeyboardMovement, setIsUsingKeyboardMovement] = useState(false); + const [highlightEmoji, setHighlightEmoji] = useState(false); const [highlightFirstEmoji, setHighlightFirstEmoji] = useState(false); + const mouseMoveHandler = useCallback(() => { + if (!arePointerEventsDisabled) { + return; + } + + setArePointerEventsDisabled(false); + }, [arePointerEventsDisabled]); + const onFocusedIndexChange = useCallback( (newIndex) => { if (filteredEmojis.length === 0) { return; } + setHighlightFirstEmoji(false); + setIsUsingKeyboardMovement(true); + if (!isUsingKeyboardMovement) { setIsUsingKeyboardMovement(true); } - if (!arePointerEventsDisabled) { setArePointerEventsDisabled(true); } - // If the input is not focused and the highlighted index is -1, focus the input + + // If the input is not focused and the new index is out of range, focus the input if (newIndex < 0 && !searchInputRef.current.isFocused()) { searchInputRef.current.focus(); } @@ -87,6 +102,7 @@ function EmojiPickerMenu({forwardedRef, onEmojiSelected}) { const [highlightedIndex, setHighlightedIndex] = useArrowKeyFocusManager({ maxIndex: filteredEmojis.length - 1, + // Spacers indexes need to be disabled so that the arrow keys don't focus them disabledIndexes: isListFiltered ? [] : [...headerIndices, ...spacersIndexes], itemsPerRow: CONST.EMOJI_NUM_PER_ROW, initialFocusedIndex: -1, @@ -94,14 +110,6 @@ function EmojiPickerMenu({forwardedRef, onEmojiSelected}) { onFocusedIndexChange, }); - const mouseMoveHandler = useCallback(() => { - if (!arePointerEventsDisabled) { - return; - } - - setArePointerEventsDisabled(false); - }, [arePointerEventsDisabled]); - const filterEmojis = _.throttle((searchTerm) => { const [normalizedSearchTerm, newFilteredEmojiList] = suggestEmojis(searchTerm); @@ -113,16 +121,56 @@ function EmojiPickerMenu({forwardedRef, onEmojiSelected}) { setFilteredEmojis(allEmojis); setHeaderIndices(headerRowIndices); setHighlightedIndex(-1); - setHighlightFirstEmoji(false); + setHighlightEmoji(false); return; } // Remove sticky header indices. There are no headers while searching and we don't want to make emojis sticky setFilteredEmojis(newFilteredEmojiList); setHeaderIndices([]); - setHighlightedIndex(0); setHighlightFirstEmoji(true); + setIsUsingKeyboardMovement(false); }, throttleTime); + const keyDownHandler = useCallback( + (keyBoardEvent) => { + if (keyBoardEvent.key.startsWith('Arrow')) { + if (keyBoardEvent.key === 'ArrowUp' || keyBoardEvent.key === 'ArrowDown') { + keyBoardEvent.preventDefault(); + } + + 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); + // On web, avoid this Enter default input action; otherwise, it will add a new line in the subsequently focused composer. + keyBoardEvent.preventDefault(); + // On mWeb, avoid propagating this Enter keystroke to Pressable child component; otherwise, it will trigger the onEmojiSelected callback again. + keyBoardEvent.stopPropagation(); + 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); + } + + // We allow typing in the search box if any key is pressed apart from Arrow keys. + if (searchInputRef.current && !searchInputRef.current.isFocused() && ReportUtils.shouldAutoFocusOnKeyPress(keyBoardEvent)) { + searchInputRef.current.focus(); + } + }, + [filteredEmojis, highlightedIndex, onEmojiSelected, preferredSkinTone], + ); + /** * Setup and attach keypress/mouse handlers for highlight navigation. */ @@ -133,7 +181,11 @@ function EmojiPickerMenu({forwardedRef, onEmojiSelected}) { // Re-enable pointer events and hovering over EmojiPickerItems when the mouse moves document.addEventListener('mousemove', mouseMoveHandler); - }, [mouseMoveHandler]); + + // 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', keyDownHandler, true); + }, [keyDownHandler, mouseMoveHandler]); /** * Cleanup all mouse/keydown event listeners that we've set up @@ -143,8 +195,9 @@ function EmojiPickerMenu({forwardedRef, onEmojiSelected}) { return; } + document.removeEventListener('keydown', keyDownHandler, true); document.removeEventListener('mousemove', mouseMoveHandler); - }, [mouseMoveHandler]); + }, [keyDownHandler, mouseMoveHandler]); useEffect(() => { // This callback prop is used by the parent component using the constructor to @@ -201,13 +254,15 @@ function EmojiPickerMenu({forwardedRef, onEmojiSelected}) { const emojiCode = types && types[preferredSkinTone] ? types[preferredSkinTone] : code; const isEmojiFocused = index === highlightedIndex && isUsingKeyboardMovement; - const shouldEmojiBeHighlighted = index === highlightedIndex && highlightFirstEmoji; + const shouldEmojiBeHighlighted = index === highlightedIndex && highlightEmoji; + const shouldFirstEmojiBeHighlighted = index === 0 && highlightFirstEmoji; return ( onEmojiSelected(emoji, item))} onHoverIn={() => { - // setHighlightFirstEmoji(false); + setHighlightEmoji(false); + setHighlightFirstEmoji(false); if (!isUsingKeyboardMovement) { return; } @@ -215,13 +270,8 @@ function EmojiPickerMenu({forwardedRef, onEmojiSelected}) { }} 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={shouldEmojiBeHighlighted} + isHighlighted={shouldFirstEmojiBeHighlighted || shouldEmojiBeHighlighted} /> ); }, @@ -229,6 +279,7 @@ function EmojiPickerMenu({forwardedRef, onEmojiSelected}) { preferredSkinTone, highlightedIndex, isUsingKeyboardMovement, + highlightEmoji, highlightFirstEmoji, singleExecution, styles, diff --git a/src/hooks/useArrowKeyFocusManager.ts b/src/hooks/useArrowKeyFocusManager.ts index b6c0e1aa4d2d..a67bda5c04fb 100644 --- a/src/hooks/useArrowKeyFocusManager.ts +++ b/src/hooks/useArrowKeyFocusManager.ts @@ -94,11 +94,19 @@ export default function useArrowKeyFocusManager({ setFocusedIndex((actualIndex) => { let currentFocusedIndex = -1; - if (allowHorizontalArrowKeys) { + + if (actualIndex === -1) { + currentFocusedIndex = 0; + } else if (allowHorizontalArrowKeys) { currentFocusedIndex = actualIndex < maxIndex ? actualIndex + itemsPerRow : nextIndex; } else { currentFocusedIndex = actualIndex < maxIndex ? actualIndex + 1 : nextIndex; } + + if (disableCyclicTraversal && currentFocusedIndex > maxIndex) { + currentFocusedIndex = maxIndex; + } + let newFocusedIndex = currentFocusedIndex; while (disabledIndexes.includes(newFocusedIndex)) { if (actualIndex < 0) { From e6e324ba6be703bc7359b681a6fba400167b36a7 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Mon, 22 Jan 2024 15:27:05 +0100 Subject: [PATCH 08/19] more cleanup for horizontal arrows --- .../EmojiPicker/EmojiPickerMenu/index.js | 6 ++++-- src/hooks/useArrowKeyFocusManager.ts | 20 +++++++------------ 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.js b/src/components/EmojiPicker/EmojiPickerMenu/index.js index 99f356ff3c21..2e5095194a92 100755 --- a/src/components/EmojiPicker/EmojiPickerMenu/index.js +++ b/src/components/EmojiPicker/EmojiPickerMenu/index.js @@ -64,6 +64,7 @@ function EmojiPickerMenu({forwardedRef, onEmojiSelected}) { const shouldFocusInputOnScreenFocus = canFocusInputOnScreenFocus(); const [arePointerEventsDisabled, setArePointerEventsDisabled] = useState(false); + const [isFocused, setIsFocused] = useState(false); const [isUsingKeyboardMovement, setIsUsingKeyboardMovement] = useState(false); const [highlightEmoji, setHighlightEmoji] = useState(false); const [highlightFirstEmoji, setHighlightFirstEmoji] = useState(false); @@ -72,7 +73,6 @@ function EmojiPickerMenu({forwardedRef, onEmojiSelected}) { if (!arePointerEventsDisabled) { return; } - setArePointerEventsDisabled(false); }, [arePointerEventsDisabled]); @@ -134,7 +134,7 @@ function EmojiPickerMenu({forwardedRef, onEmojiSelected}) { const keyDownHandler = useCallback( (keyBoardEvent) => { if (keyBoardEvent.key.startsWith('Arrow')) { - if (keyBoardEvent.key === 'ArrowUp' || keyBoardEvent.key === 'ArrowDown') { + if (!isFocused || keyBoardEvent.key === 'ArrowUp' || keyBoardEvent.key === 'ArrowDown') { keyBoardEvent.preventDefault(); } @@ -311,8 +311,10 @@ function EmojiPickerMenu({forwardedRef, onEmojiSelected}) { autoFocus={shouldFocusInputOnScreenFocus} onFocus={() => { setHighlightedIndex(-1); + setIsFocused(true); setIsUsingKeyboardMovement(false); }} + onBlur={() => setIsFocused(false)} autoCorrect={false} blurOnSubmit={filteredEmojis.length > 0} /> diff --git a/src/hooks/useArrowKeyFocusManager.ts b/src/hooks/useArrowKeyFocusManager.ts index a67bda5c04fb..52e80147f157 100644 --- a/src/hooks/useArrowKeyFocusManager.ts +++ b/src/hooks/useArrowKeyFocusManager.ts @@ -26,7 +26,6 @@ type UseArrowKeyFocusManager = [number, (index: number) => void]; * @param [config.disabledIndexes] – An array of indexes to disable + skip over * @param [config.shouldExcludeTextAreaNodes] – Whether arrow keys should have any effect when a TextArea node is focused * @param [config.isActive] – Whether the component is ready and should subscribe to KeyboardShortcut - * @param [config.allowHorizontalArrowKeys] – Whether to allow horizontal arrow keys to have any effect * @param [config.itemsPerRow] – The number of items per row. If provided, the arrow keys will move focus horizontally as well as vertically * @param [config.disableCyclicTraversal] – Whether to disable cyclic traversal of the list. If true, the arrow keys will have no effect when the first or last item is focused */ @@ -70,6 +69,7 @@ export default function useArrowKeyFocusManager({ currentFocusedIndex = actualIndex > 0 ? actualIndex - 1 : nextIndex; } let newFocusedIndex = currentFocusedIndex; + while (disabledIndexes.includes(newFocusedIndex)) { newFocusedIndex -= allowHorizontalArrowKeys ? itemsPerRow : 1; if (newFocusedIndex < 0) { @@ -142,11 +142,8 @@ export default function useArrowKeyFocusManager({ let newFocusedIndex = currentFocusedIndex; while (disabledIndexes.includes(newFocusedIndex)) { - if (allowHorizontalArrowKeys) { - newFocusedIndex = newFocusedIndex > 0 ? newFocusedIndex - 1 : nextIndex; - } else { - newFocusedIndex = newFocusedIndex > 0 ? newFocusedIndex - 1 : nextIndex; - } + newFocusedIndex = newFocusedIndex > 0 ? newFocusedIndex - 1 : nextIndex; + if (newFocusedIndex === currentFocusedIndex) { // all indexes are disabled return actualIndex; // no-op @@ -154,7 +151,7 @@ export default function useArrowKeyFocusManager({ } return newFocusedIndex; }); - }, [allowHorizontalArrowKeys, disableCyclicTraversal, disabledIndexes, maxIndex]); + }, [disableCyclicTraversal, disabledIndexes, maxIndex]); useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ARROW_LEFT, allowHorizontalArrowKeys ? arrowLeftCallback : () => {}, arrowConfig); const arrowRightCallback = useCallback(() => { @@ -171,11 +168,8 @@ export default function useArrowKeyFocusManager({ let newFocusedIndex = currentFocusedIndex; while (disabledIndexes.includes(newFocusedIndex)) { - if (allowHorizontalArrowKeys) { - newFocusedIndex = newFocusedIndex < maxIndex ? newFocusedIndex + 1 : nextIndex; - } else { - newFocusedIndex = newFocusedIndex < maxIndex ? newFocusedIndex + 1 : nextIndex; - } + newFocusedIndex = newFocusedIndex < maxIndex ? newFocusedIndex + 1 : nextIndex; + if (newFocusedIndex === currentFocusedIndex) { // all indexes are disabled return actualIndex; @@ -183,7 +177,7 @@ export default function useArrowKeyFocusManager({ } return newFocusedIndex; }); - }, [allowHorizontalArrowKeys, disableCyclicTraversal, disabledIndexes, maxIndex]); + }, [disableCyclicTraversal, disabledIndexes, maxIndex]); useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ARROW_RIGHT, allowHorizontalArrowKeys ? arrowRightCallback : () => {}, arrowConfig); // Note: you don't need to manually manage focusedIndex in the parent. setFocusedIndex is only exposed in case you want to reset focusedIndex or focus a specific item From d053504e4a204aa768862ad9333567170bdf6774 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Mon, 22 Jan 2024 15:27:05 +0100 Subject: [PATCH 09/19] reduce diff between mobile and web --- src/components/EmojiPicker/EmojiPickerMenu/index.js | 2 +- src/components/EmojiPicker/EmojiPickerMenu/index.native.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.js b/src/components/EmojiPicker/EmojiPickerMenu/index.js index 2e5095194a92..5222540ffefe 100755 --- a/src/components/EmojiPicker/EmojiPickerMenu/index.js +++ b/src/components/EmojiPicker/EmojiPickerMenu/index.js @@ -168,7 +168,7 @@ function EmojiPickerMenu({forwardedRef, onEmojiSelected}) { searchInputRef.current.focus(); } }, - [filteredEmojis, highlightedIndex, onEmojiSelected, preferredSkinTone], + [filteredEmojis, highlightedIndex, isFocused, onEmojiSelected, preferredSkinTone], ); /** diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.native.js b/src/components/EmojiPicker/EmojiPickerMenu/index.native.js index 1463ce736699..27a49d360906 100644 --- a/src/components/EmojiPicker/EmojiPickerMenu/index.native.js +++ b/src/components/EmojiPicker/EmojiPickerMenu/index.native.js @@ -21,6 +21,7 @@ function EmojiPickerMenu({onEmojiSelected}) { const styles = useThemeStyles(); const {windowWidth, isSmallScreenWidth} = useWindowDimensions(); const {translate} = useLocalize(); + const {singleExecution} = useSingleExecution(); const { allEmojis, headerEmojis, @@ -35,7 +36,6 @@ function EmojiPickerMenu({onEmojiSelected}) { listStyle, emojiListRef, } = useEmojiPickerMenu(); - const {singleExecution} = useSingleExecution(); const StyleUtils = useStyleUtils(); /** @@ -73,7 +73,7 @@ function EmojiPickerMenu({onEmojiSelected}) { /** * 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 + * so that the sticky headers function properly. * * @param {Object} item * @returns {*} From 4e2d916655ced9b3f10f0360889a96a29eab20e8 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Mon, 22 Jan 2024 15:27:05 +0100 Subject: [PATCH 10/19] fix cyclic traversal focus --- src/components/EmojiPicker/EmojiPickerMenu/index.js | 2 +- src/hooks/useArrowKeyFocusManager.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.js b/src/components/EmojiPicker/EmojiPickerMenu/index.js index 5222540ffefe..22549e1c0866 100755 --- a/src/components/EmojiPicker/EmojiPickerMenu/index.js +++ b/src/components/EmojiPicker/EmojiPickerMenu/index.js @@ -102,7 +102,7 @@ function EmojiPickerMenu({forwardedRef, onEmojiSelected}) { const [highlightedIndex, setHighlightedIndex] = useArrowKeyFocusManager({ maxIndex: filteredEmojis.length - 1, - // Spacers indexes need to be disabled so that the arrow keys don't focus them + // Spacers indexes need to be disabled so that the arrow keys don't focus them. All headers are hidden when list is filtered disabledIndexes: isListFiltered ? [] : [...headerIndices, ...spacersIndexes], itemsPerRow: CONST.EMOJI_NUM_PER_ROW, initialFocusedIndex: -1, diff --git a/src/hooks/useArrowKeyFocusManager.ts b/src/hooks/useArrowKeyFocusManager.ts index 52e80147f157..934f65800939 100644 --- a/src/hooks/useArrowKeyFocusManager.ts +++ b/src/hooks/useArrowKeyFocusManager.ts @@ -104,7 +104,7 @@ export default function useArrowKeyFocusManager({ } if (disableCyclicTraversal && currentFocusedIndex > maxIndex) { - currentFocusedIndex = maxIndex; + return actualIndex; } let newFocusedIndex = currentFocusedIndex; From 613bc2804f5b62ac339c53708044feada7cbc60b Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Mon, 22 Jan 2024 15:27:06 +0100 Subject: [PATCH 11/19] update naming to focusedIndex --- .../EmojiPicker/EmojiPickerMenu/index.js | 31 ++++++++++--------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.js b/src/components/EmojiPicker/EmojiPickerMenu/index.js index 22549e1c0866..25800bf54fab 100755 --- a/src/components/EmojiPicker/EmojiPickerMenu/index.js +++ b/src/components/EmojiPicker/EmojiPickerMenu/index.js @@ -82,8 +82,9 @@ function EmojiPickerMenu({forwardedRef, onEmojiSelected}) { return; } - setHighlightFirstEmoji(false); - setIsUsingKeyboardMovement(true); + if (highlightFirstEmoji) { + setHighlightFirstEmoji(false); + } if (!isUsingKeyboardMovement) { setIsUsingKeyboardMovement(true); @@ -97,10 +98,10 @@ function EmojiPickerMenu({forwardedRef, onEmojiSelected}) { searchInputRef.current.focus(); } }, - [arePointerEventsDisabled, filteredEmojis.length, isUsingKeyboardMovement], + [arePointerEventsDisabled, filteredEmojis.length, highlightFirstEmoji, isUsingKeyboardMovement], ); - const [highlightedIndex, setHighlightedIndex] = useArrowKeyFocusManager({ + const [focusedIndex, setFocusedIndex] = useArrowKeyFocusManager({ maxIndex: filteredEmojis.length - 1, // Spacers indexes need to be disabled so that the arrow keys don't focus them. All headers are hidden when list is filtered disabledIndexes: isListFiltered ? [] : [...headerIndices, ...spacersIndexes], @@ -120,7 +121,7 @@ function EmojiPickerMenu({forwardedRef, onEmojiSelected}) { // There are no headers when searching, so we need to re-make them sticky when there is no search term setFilteredEmojis(allEmojis); setHeaderIndices(headerRowIndices); - setHighlightedIndex(-1); + setFocusedIndex(-1); setHighlightEmoji(false); return; } @@ -142,8 +143,8 @@ function EmojiPickerMenu({forwardedRef, onEmojiSelected}) { } // 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 (!isEnterWhileComposition(keyBoardEvent) && keyBoardEvent.key === CONST.KEYBOARD_SHORTCUTS.ENTER.shortcutKey && focusedIndex !== -1) { + const item = filteredEmojis[focusedIndex]; if (!item) { return; } @@ -168,7 +169,7 @@ function EmojiPickerMenu({forwardedRef, onEmojiSelected}) { searchInputRef.current.focus(); } }, - [filteredEmojis, highlightedIndex, isFocused, onEmojiSelected, preferredSkinTone], + [filteredEmojis, focusedIndex, isFocused, onEmojiSelected, preferredSkinTone], ); /** @@ -253,8 +254,8 @@ function EmojiPickerMenu({forwardedRef, onEmojiSelected}) { const emojiCode = types && types[preferredSkinTone] ? types[preferredSkinTone] : code; - const isEmojiFocused = index === highlightedIndex && isUsingKeyboardMovement; - const shouldEmojiBeHighlighted = index === highlightedIndex && highlightEmoji; + const isEmojiFocused = index === focusedIndex && isUsingKeyboardMovement; + const shouldEmojiBeHighlighted = index === focusedIndex && highlightEmoji; const shouldFirstEmojiBeHighlighted = index === 0 && highlightFirstEmoji; return ( @@ -269,7 +270,7 @@ function EmojiPickerMenu({forwardedRef, onEmojiSelected}) { setIsUsingKeyboardMovement(false); }} emoji={emojiCode} - onFocus={() => setHighlightedIndex(index)} + onFocus={() => setFocusedIndex(index)} isFocused={isEmojiFocused} isHighlighted={shouldFirstEmojiBeHighlighted || shouldEmojiBeHighlighted} /> @@ -277,7 +278,7 @@ function EmojiPickerMenu({forwardedRef, onEmojiSelected}) { }, [ preferredSkinTone, - highlightedIndex, + focusedIndex, isUsingKeyboardMovement, highlightEmoji, highlightFirstEmoji, @@ -287,7 +288,7 @@ function EmojiPickerMenu({forwardedRef, onEmojiSelected}) { windowWidth, translate, onEmojiSelected, - setHighlightedIndex, + setFocusedIndex, ], ); @@ -310,7 +311,7 @@ function EmojiPickerMenu({forwardedRef, onEmojiSelected}) { ref={searchInputRef} autoFocus={shouldFocusInputOnScreenFocus} onFocus={() => { - setHighlightedIndex(-1); + setFocusedIndex(-1); setIsFocused(true); setIsUsingKeyboardMovement(false); }} @@ -332,7 +333,7 @@ function EmojiPickerMenu({forwardedRef, onEmojiSelected}) { ref={emojiListRef} data={filteredEmojis} renderItem={renderItem} - extraData={[highlightedIndex, preferredSkinTone]} + extraData={[focusedIndex, preferredSkinTone]} stickyHeaderIndices={headerIndices} /> From e8d35df13a127233a345260d359fd4bf57c42b42 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Mon, 22 Jan 2024 15:27:06 +0100 Subject: [PATCH 12/19] reduce diff --- src/components/EmojiPicker/EmojiPickerMenu/index.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.js b/src/components/EmojiPicker/EmojiPickerMenu/index.js index 25800bf54fab..2c2746e32fac 100755 --- a/src/components/EmojiPicker/EmojiPickerMenu/index.js +++ b/src/components/EmojiPicker/EmojiPickerMenu/index.js @@ -180,12 +180,12 @@ function EmojiPickerMenu({forwardedRef, onEmojiSelected}) { return; } - // Re-enable pointer events and hovering over EmojiPickerItems when the mouse moves - document.addEventListener('mousemove', mouseMoveHandler); - // 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', keyDownHandler, true); + + // Re-enable pointer events and hovering over EmojiPickerItems when the mouse moves + document.addEventListener('mousemove', mouseMoveHandler); }, [keyDownHandler, mouseMoveHandler]); /** From 773787fe66212677d139219f601d7f20945e89b7 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Mon, 22 Jan 2024 15:35:58 +0100 Subject: [PATCH 13/19] code review updates --- src/hooks/useArrowKeyFocusManager.ts | 12 ++++++------ src/libs/EmojiUtils.ts | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/hooks/useArrowKeyFocusManager.ts b/src/hooks/useArrowKeyFocusManager.ts index 934f65800939..507adcb0df19 100644 --- a/src/hooks/useArrowKeyFocusManager.ts +++ b/src/hooks/useArrowKeyFocusManager.ts @@ -129,7 +129,7 @@ export default function useArrowKeyFocusManager({ useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ARROW_DOWN, arrowDownCallback, arrowConfig); const arrowLeftCallback = useCallback(() => { - if (maxIndex < 0) { + if (maxIndex < 0 || !allowHorizontalArrowKeys) { return; } @@ -151,11 +151,11 @@ export default function useArrowKeyFocusManager({ } return newFocusedIndex; }); - }, [disableCyclicTraversal, disabledIndexes, maxIndex]); - useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ARROW_LEFT, allowHorizontalArrowKeys ? arrowLeftCallback : () => {}, arrowConfig); + }, [allowHorizontalArrowKeys, disableCyclicTraversal, disabledIndexes, maxIndex]); + useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ARROW_LEFT, arrowLeftCallback, arrowConfig); const arrowRightCallback = useCallback(() => { - if (maxIndex < 0) { + if (maxIndex < 0 || !allowHorizontalArrowKeys) { return; } @@ -177,8 +177,8 @@ export default function useArrowKeyFocusManager({ } return newFocusedIndex; }); - }, [disableCyclicTraversal, disabledIndexes, maxIndex]); - useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ARROW_RIGHT, allowHorizontalArrowKeys ? arrowRightCallback : () => {}, arrowConfig); + }, [allowHorizontalArrowKeys, disableCyclicTraversal, disabledIndexes, maxIndex]); + useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ARROW_RIGHT, arrowRightCallback, arrowConfig); // Note: you don't need to manually manage focusedIndex in the parent. setFocusedIndex is only exposed in case you want to reset focusedIndex or focus a specific item return [focusedIndex, setFocusedIndex]; diff --git a/src/libs/EmojiUtils.ts b/src/libs/EmojiUtils.ts index 3629f5a31f12..fe79ea68a0b3 100644 --- a/src/libs/EmojiUtils.ts +++ b/src/libs/EmojiUtils.ts @@ -551,7 +551,7 @@ const getEmojiReactionDetails = (emojiName: string, reaction: ReportActionReacti function getSpacersIndexes(allEmojis: EmojiPickerList): number[] { const spacersIndexes: number[] = []; allEmojis.forEach((emoji, index) => { - if (!('spacer' in emoji)) { + if (!(CONST.EMOJI_PICKER_ITEM_TYPES.SPACER in emoji)) { return; } From 61287bc4e41c39f25a433bfbe2ba063d904dd111 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Wed, 24 Jan 2024 12:07:41 +0100 Subject: [PATCH 14/19] add newlines for better readability --- src/hooks/useArrowKeyFocusManager.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/hooks/useArrowKeyFocusManager.ts b/src/hooks/useArrowKeyFocusManager.ts index 507adcb0df19..352734c92e8d 100644 --- a/src/hooks/useArrowKeyFocusManager.ts +++ b/src/hooks/useArrowKeyFocusManager.ts @@ -83,6 +83,7 @@ export default function useArrowKeyFocusManager({ return newFocusedIndex; }); }, [allowHorizontalArrowKeys, disableCyclicTraversal, disabledIndexes, itemsPerRow, maxIndex]); + useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ARROW_UP, arrowUpCallback, arrowConfig); const arrowDownCallback = useCallback(() => { @@ -126,6 +127,7 @@ export default function useArrowKeyFocusManager({ return newFocusedIndex; }); }, [allowHorizontalArrowKeys, disableCyclicTraversal, disabledIndexes, itemsPerRow, maxIndex]); + useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ARROW_DOWN, arrowDownCallback, arrowConfig); const arrowLeftCallback = useCallback(() => { @@ -152,6 +154,7 @@ export default function useArrowKeyFocusManager({ return newFocusedIndex; }); }, [allowHorizontalArrowKeys, disableCyclicTraversal, disabledIndexes, maxIndex]); + useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ARROW_LEFT, arrowLeftCallback, arrowConfig); const arrowRightCallback = useCallback(() => { @@ -178,6 +181,7 @@ export default function useArrowKeyFocusManager({ return newFocusedIndex; }); }, [allowHorizontalArrowKeys, disableCyclicTraversal, disabledIndexes, maxIndex]); + useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ARROW_RIGHT, arrowRightCallback, arrowConfig); // Note: you don't need to manually manage focusedIndex in the parent. setFocusedIndex is only exposed in case you want to reset focusedIndex or focus a specific item From 54197c0a97140bbd851009245b6743d9b988f985 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Wed, 24 Jan 2024 12:23:55 +0100 Subject: [PATCH 15/19] memoize disabled indexes --- src/components/EmojiPicker/EmojiPickerMenu/index.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.js b/src/components/EmojiPicker/EmojiPickerMenu/index.js index 2c2746e32fac..24d15e9b1b6b 100755 --- a/src/components/EmojiPicker/EmojiPickerMenu/index.js +++ b/src/components/EmojiPicker/EmojiPickerMenu/index.js @@ -1,6 +1,6 @@ import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; -import React, {useCallback, useEffect, useRef, useState} from 'react'; +import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; import {scrollTo} from 'react-native-reanimated'; import _ from 'underscore'; @@ -101,10 +101,12 @@ function EmojiPickerMenu({forwardedRef, onEmojiSelected}) { [arePointerEventsDisabled, filteredEmojis.length, highlightFirstEmoji, isUsingKeyboardMovement], ); + const disabledIndexes = useMemo(() => (isListFiltered ? [] : [...headerIndices, ...spacersIndexes]), [headerIndices, isListFiltered, spacersIndexes]); + const [focusedIndex, setFocusedIndex] = useArrowKeyFocusManager({ maxIndex: filteredEmojis.length - 1, // Spacers indexes need to be disabled so that the arrow keys don't focus them. All headers are hidden when list is filtered - disabledIndexes: isListFiltered ? [] : [...headerIndices, ...spacersIndexes], + disabledIndexes, itemsPerRow: CONST.EMOJI_NUM_PER_ROW, initialFocusedIndex: -1, disableCyclicTraversal: true, From e916dabbb2cdfeb11448dd812c4891cceb38b65a Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Wed, 24 Jan 2024 13:34:24 +0100 Subject: [PATCH 16/19] remove unnecessary condition --- src/components/EmojiPicker/EmojiPickerMenu/index.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.js b/src/components/EmojiPicker/EmojiPickerMenu/index.js index 24d15e9b1b6b..c078c7217f79 100755 --- a/src/components/EmojiPicker/EmojiPickerMenu/index.js +++ b/src/components/EmojiPicker/EmojiPickerMenu/index.js @@ -89,16 +89,13 @@ function EmojiPickerMenu({forwardedRef, onEmojiSelected}) { if (!isUsingKeyboardMovement) { setIsUsingKeyboardMovement(true); } - if (!arePointerEventsDisabled) { - setArePointerEventsDisabled(true); - } // If the input is not focused and the new index is out of range, focus the input if (newIndex < 0 && !searchInputRef.current.isFocused()) { searchInputRef.current.focus(); } }, - [arePointerEventsDisabled, filteredEmojis.length, highlightFirstEmoji, isUsingKeyboardMovement], + [filteredEmojis.length, highlightFirstEmoji, isUsingKeyboardMovement], ); const disabledIndexes = useMemo(() => (isListFiltered ? [] : [...headerIndices, ...spacersIndexes]), [headerIndices, isListFiltered, spacersIndexes]); From 7ebc0f49b0fd9b5170d128bc711e73bd24b8542b Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Thu, 25 Jan 2024 15:33:30 +0100 Subject: [PATCH 17/19] fix selecting first emoji from filtered list --- src/components/EmojiPicker/EmojiPickerMenu/index.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.js b/src/components/EmojiPicker/EmojiPickerMenu/index.js index c078c7217f79..5428852d534f 100755 --- a/src/components/EmojiPicker/EmojiPickerMenu/index.js +++ b/src/components/EmojiPicker/EmojiPickerMenu/index.js @@ -142,8 +142,13 @@ function EmojiPickerMenu({forwardedRef, onEmojiSelected}) { } // Select the currently highlighted emoji if enter is pressed - if (!isEnterWhileComposition(keyBoardEvent) && keyBoardEvent.key === CONST.KEYBOARD_SHORTCUTS.ENTER.shortcutKey && focusedIndex !== -1) { - const item = filteredEmojis[focusedIndex]; + if (!isEnterWhileComposition(keyBoardEvent) && keyBoardEvent.key === CONST.KEYBOARD_SHORTCUTS.ENTER.shortcutKey) { + let indexToSelect = focusedIndex; + if(highlightFirstEmoji) { + indexToSelect = 0; + } + + const item = filteredEmojis[indexToSelect]; if (!item) { return; } From 7d61634e5553ff413385a9841e6da2a9e996c0f1 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Thu, 25 Jan 2024 16:07:36 +0100 Subject: [PATCH 18/19] fix linting --- 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 5428852d534f..21c439fa5291 100755 --- a/src/components/EmojiPicker/EmojiPickerMenu/index.js +++ b/src/components/EmojiPicker/EmojiPickerMenu/index.js @@ -144,7 +144,7 @@ function EmojiPickerMenu({forwardedRef, onEmojiSelected}) { // Select the currently highlighted emoji if enter is pressed if (!isEnterWhileComposition(keyBoardEvent) && keyBoardEvent.key === CONST.KEYBOARD_SHORTCUTS.ENTER.shortcutKey) { let indexToSelect = focusedIndex; - if(highlightFirstEmoji) { + if (highlightFirstEmoji) { indexToSelect = 0; } From d6789ed1f68406f73846a8efc81c343a77a9f7e0 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Fri, 26 Jan 2024 12:18:20 +0100 Subject: [PATCH 19/19] fix linting --- 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 21c439fa5291..cbf55b31c74d 100755 --- a/src/components/EmojiPicker/EmojiPickerMenu/index.js +++ b/src/components/EmojiPicker/EmojiPickerMenu/index.js @@ -173,7 +173,7 @@ function EmojiPickerMenu({forwardedRef, onEmojiSelected}) { searchInputRef.current.focus(); } }, - [filteredEmojis, focusedIndex, isFocused, onEmojiSelected, preferredSkinTone], + [filteredEmojis, focusedIndex, highlightFirstEmoji, isFocused, onEmojiSelected, preferredSkinTone], ); /**