Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Revert "feat: implement useArrowKeyFocusManager in EmojiPickerMenu" #35737

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
214 changes: 145 additions & 69 deletions src/components/EmojiPicker/EmojiPickerMenu/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ 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';
Expand Down Expand Up @@ -53,7 +52,6 @@ function EmojiPickerMenu({forwardedRef, onEmojiSelected}) {
preferredSkinTone,
listStyle,
emojiListRef,
spacersIndexes,
} = useEmojiPickerMenu();

// Ref for the emoji search input
Expand All @@ -63,11 +61,22 @@ 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 [highlightEmoji, setHighlightEmoji] = 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 mouseMoveHandler = useCallback(() => {
if (!arePointerEventsDisabled) {
Expand All @@ -76,39 +85,15 @@ function EmojiPickerMenu({forwardedRef, onEmojiSelected}) {
setArePointerEventsDisabled(false);
}, [arePointerEventsDisabled]);

const onFocusedIndexChange = useCallback(
(newIndex) => {
if (filteredEmojis.length === 0) {
return;
}

if (highlightFirstEmoji) {
setHighlightFirstEmoji(false);
}

if (!isUsingKeyboardMovement) {
setIsUsingKeyboardMovement(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();
}
},
[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,
itemsPerRow: CONST.EMOJI_NUM_PER_ROW,
initialFocusedIndex: -1,
disableCyclicTraversal: true,
onFocusedIndexChange,
});
/**
* 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);
Expand All @@ -120,35 +105,134 @@ 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);
setFocusedIndex(-1);
setHighlightEmoji(false);
setHighlightedIndex(-1);
setHighlightFirstEmoji(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);

/**
* 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) {
let indexToSelect = focusedIndex;
if (highlightFirstEmoji) {
indexToSelect = 0;
}

const item = filteredEmojis[indexToSelect];
if (!isEnterWhileComposition(keyBoardEvent) && keyBoardEvent.key === CONST.KEYBOARD_SHORTCUTS.ENTER.shortcutKey && highlightedIndex !== -1) {
const item = filteredEmojis[highlightedIndex];
if (!item) {
return;
}
Expand All @@ -166,14 +250,15 @@ function EmojiPickerMenu({forwardedRef, onEmojiSelected}) {
// 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, focusedIndex, highlightFirstEmoji, isFocused, onEmojiSelected, preferredSkinTone],
[filteredEmojis, highlightAdjacentEmoji, highlightedIndex, isFocused, onEmojiSelected, preferredSkinTone],
);

/**
Expand Down Expand Up @@ -258,42 +343,32 @@ function EmojiPickerMenu({forwardedRef, onEmojiSelected}) {

const emojiCode = types && types[preferredSkinTone] ? types[preferredSkinTone] : code;

const isEmojiFocused = index === focusedIndex && isUsingKeyboardMovement;
const shouldEmojiBeHighlighted = index === focusedIndex && highlightEmoji;
const shouldFirstEmojiBeHighlighted = index === 0 && highlightFirstEmoji;
const isEmojiFocused = index === highlightedIndex && isUsingKeyboardMovement;
const shouldEmojiBeHighlighted = index === highlightedIndex && highlightFirstEmoji;

return (
<EmojiPickerMenuItem
onPress={singleExecution((emoji) => onEmojiSelected(emoji, item))}
onHoverIn={() => {
setHighlightEmoji(false);
setHighlightFirstEmoji(false);
if (!isUsingKeyboardMovement) {
return;
}
setIsUsingKeyboardMovement(false);
}}
emoji={emojiCode}
onFocus={() => setFocusedIndex(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.
setHighlightedIndex((prevState) => (prevState === index ? -1 : prevState))
}
isFocused={isEmojiFocused}
isHighlighted={shouldFirstEmojiBeHighlighted || shouldEmojiBeHighlighted}
isHighlighted={shouldEmojiBeHighlighted}
/>
);
},
[
preferredSkinTone,
focusedIndex,
isUsingKeyboardMovement,
highlightEmoji,
highlightFirstEmoji,
singleExecution,
styles,
isSmallScreenWidth,
windowWidth,
translate,
onEmojiSelected,
setFocusedIndex,
],
[preferredSkinTone, highlightedIndex, isUsingKeyboardMovement, highlightFirstEmoji, singleExecution, translate, onEmojiSelected, isSmallScreenWidth, windowWidth, styles],
);

return (
Expand All @@ -314,8 +389,9 @@ function EmojiPickerMenu({forwardedRef, onEmojiSelected}) {
defaultValue=""
ref={searchInputRef}
autoFocus={shouldFocusInputOnScreenFocus}
onSelectionChange={onSelectionChange}
onFocus={() => {
setFocusedIndex(-1);
setHighlightedIndex(-1);
setIsFocused(true);
setIsUsingKeyboardMovement(false);
}}
Expand All @@ -337,7 +413,7 @@ function EmojiPickerMenu({forwardedRef, onEmojiSelected}) {
ref={emojiListRef}
data={filteredEmojis}
renderItem={renderItem}
extraData={[focusedIndex, preferredSkinTone]}
extraData={[highlightedIndex, preferredSkinTone]}
stickyHeaderIndices={headerIndices}
/>
</View>
Expand Down
4 changes: 2 additions & 2 deletions src/components/EmojiPicker/EmojiPickerMenu/index.native.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ function EmojiPickerMenu({onEmojiSelected}) {
const styles = useThemeStyles();
const {windowWidth, isSmallScreenWidth} = useWindowDimensions();
const {translate} = useLocalize();
const {singleExecution} = useSingleExecution();
const {
allEmojis,
headerEmojis,
Expand All @@ -36,6 +35,7 @@ function EmojiPickerMenu({onEmojiSelected}) {
listStyle,
emojiListRef,
} = useEmojiPickerMenu();
const {singleExecution} = useSingleExecution();
const StyleUtils = useStyleUtils();

/**
Expand Down Expand Up @@ -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 {*}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ 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;
Expand Down Expand Up @@ -62,7 +61,6 @@ const useEmojiPickerMenu = () => {
preferredSkinTone,
listStyle,
emojiListRef,
spacersIndexes,
};
};

Expand Down
Loading
Loading