@@ -7,6 +7,7 @@ import _ from 'underscore';
77import EmojiPickerMenuItem from '@components/EmojiPicker/EmojiPickerMenuItem' ;
88import Text from '@components/Text' ;
99import TextInput from '@components/TextInput' ;
10+ import useArrowKeyFocusManager from '@hooks/useArrowKeyFocusManager' ;
1011import useLocalize from '@hooks/useLocalize' ;
1112import useSingleExecution from '@hooks/useSingleExecution' ;
1213import useStyleUtils from '@hooks/useStyleUtils' ;
@@ -52,6 +53,7 @@ function EmojiPickerMenu({forwardedRef, onEmojiSelected}) {
5253 preferredSkinTone,
5354 listStyle,
5455 emojiListRef,
56+ spacersIndexes,
5557 } = useEmojiPickerMenu ( ) ;
5658
5759 // Ref for the emoji search input
@@ -61,22 +63,11 @@ function EmojiPickerMenu({forwardedRef, onEmojiSelected}) {
6163 // prevent auto focus when open picker for mobile device
6264 const shouldFocusInputOnScreenFocus = canFocusInputOnScreenFocus ( ) ;
6365
64- const [ highlightedIndex , setHighlightedIndex ] = useState ( - 1 ) ;
6566 const [ arePointerEventsDisabled , setArePointerEventsDisabled ] = useState ( false ) ;
66- const [ selection , setSelection ] = useState ( { start : 0 , end : 0 } ) ;
6767 const [ isFocused , setIsFocused ] = useState ( false ) ;
6868 const [ isUsingKeyboardMovement , setIsUsingKeyboardMovement ] = useState ( false ) ;
69+ const [ highlightEmoji , setHighlightEmoji ] = useState ( false ) ;
6970 const [ highlightFirstEmoji , setHighlightFirstEmoji ] = useState ( false ) ;
70- const firstNonHeaderIndex = useMemo ( ( ) => _ . findIndex ( filteredEmojis , ( item ) => ! item . spacer && ! item . header ) , [ filteredEmojis ] ) ;
71-
72- /**
73- * On text input selection change
74- *
75- * @param {Event } event
76- */
77- const onSelectionChange = useCallback ( ( event ) => {
78- setSelection ( event . nativeEvent . selection ) ;
79- } , [ ] ) ;
8071
8172 const mouseMoveHandler = useCallback ( ( ) => {
8273 if ( ! arePointerEventsDisabled ) {
@@ -85,15 +76,39 @@ function EmojiPickerMenu({forwardedRef, onEmojiSelected}) {
8576 setArePointerEventsDisabled ( false ) ;
8677 } , [ arePointerEventsDisabled ] ) ;
8778
88- /**
89- * Focuses the search Input and has the text selected
90- */
91- function focusInputWithTextSelect ( ) {
92- if ( ! searchInputRef . current ) {
93- return ;
94- }
95- searchInputRef . current . focus ( ) ;
96- }
79+ const onFocusedIndexChange = useCallback (
80+ ( newIndex ) => {
81+ if ( filteredEmojis . length === 0 ) {
82+ return ;
83+ }
84+
85+ if ( highlightFirstEmoji ) {
86+ setHighlightFirstEmoji ( false ) ;
87+ }
88+
89+ if ( ! isUsingKeyboardMovement ) {
90+ setIsUsingKeyboardMovement ( true ) ;
91+ }
92+
93+ // If the input is not focused and the new index is out of range, focus the input
94+ if ( newIndex < 0 && ! searchInputRef . current . isFocused ( ) ) {
95+ searchInputRef . current . focus ( ) ;
96+ }
97+ } ,
98+ [ filteredEmojis . length , highlightFirstEmoji , isUsingKeyboardMovement ] ,
99+ ) ;
100+
101+ const disabledIndexes = useMemo ( ( ) => ( isListFiltered ? [ ] : [ ...headerIndices , ...spacersIndexes ] ) , [ headerIndices , isListFiltered , spacersIndexes ] ) ;
102+
103+ const [ focusedIndex , setFocusedIndex ] = useArrowKeyFocusManager ( {
104+ maxIndex : filteredEmojis . length - 1 ,
105+ // Spacers indexes need to be disabled so that the arrow keys don't focus them. All headers are hidden when list is filtered
106+ disabledIndexes,
107+ itemsPerRow : CONST . EMOJI_NUM_PER_ROW ,
108+ initialFocusedIndex : - 1 ,
109+ disableCyclicTraversal : true ,
110+ onFocusedIndexChange,
111+ } ) ;
97112
98113 const filterEmojis = _ . throttle ( ( searchTerm ) => {
99114 const [ normalizedSearchTerm , newFilteredEmojiList ] = suggestEmojis ( searchTerm ) ;
@@ -105,134 +120,35 @@ function EmojiPickerMenu({forwardedRef, onEmojiSelected}) {
105120 // There are no headers when searching, so we need to re-make them sticky when there is no search term
106121 setFilteredEmojis ( allEmojis ) ;
107122 setHeaderIndices ( headerRowIndices ) ;
108- setHighlightedIndex ( - 1 ) ;
109- setHighlightFirstEmoji ( false ) ;
123+ setFocusedIndex ( - 1 ) ;
124+ setHighlightEmoji ( false ) ;
110125 return ;
111126 }
112127 // Remove sticky header indices. There are no headers while searching and we don't want to make emojis sticky
113128 setFilteredEmojis ( newFilteredEmojiList ) ;
114129 setHeaderIndices ( [ ] ) ;
115- setHighlightedIndex ( 0 ) ;
116130 setHighlightFirstEmoji ( true ) ;
131+ setIsUsingKeyboardMovement ( false ) ;
117132 } , throttleTime ) ;
118133
119- /**
120- * Highlights emojis adjacent to the currently highlighted emoji depending on the arrowKey
121- * @param {String } arrowKey
122- */
123- const highlightAdjacentEmoji = useCallback (
124- ( arrowKey ) => {
125- if ( filteredEmojis . length === 0 ) {
126- return ;
127- }
128-
129- // Arrow Down and Arrow Right enable arrow navigation when search is focused
130- if ( searchInputRef . current && searchInputRef . current . isFocused ( ) ) {
131- if ( arrowKey !== 'ArrowDown' && arrowKey !== 'ArrowRight' ) {
132- return ;
133- }
134-
135- if ( arrowKey === 'ArrowRight' && ! ( searchInputRef . current . value . length === selection . start && selection . start === selection . end ) ) {
136- return ;
137- }
138-
139- // Blur the input, change the highlight type to keyboard, and disable pointer events
140- searchInputRef . current . blur ( ) ;
141- setArePointerEventsDisabled ( true ) ;
142- setIsUsingKeyboardMovement ( true ) ;
143- setHighlightFirstEmoji ( false ) ;
144-
145- // We only want to hightlight the Emoji if none was highlighted already
146- // If we already have a highlighted Emoji, lets just skip the first navigation
147- if ( highlightedIndex !== - 1 ) {
148- return ;
149- }
150- }
151-
152- // If nothing is highlighted and an arrow key is pressed
153- // select the first emoji, apply keyboard movement styles, and disable pointer events
154- if ( highlightedIndex === - 1 ) {
155- setHighlightedIndex ( firstNonHeaderIndex ) ;
156- setArePointerEventsDisabled ( true ) ;
157- setIsUsingKeyboardMovement ( true ) ;
158- return ;
159- }
160-
161- let newIndex = highlightedIndex ;
162- const move = ( steps , boundsCheck , onBoundReached = ( ) => { } ) => {
163- if ( boundsCheck ( ) ) {
164- onBoundReached ( ) ;
165- return ;
166- }
167-
168- // Move in the prescribed direction until we reach an element that isn't a header
169- const isHeader = ( e ) => e . header || e . spacer ;
170- do {
171- newIndex += steps ;
172- if ( newIndex < 0 ) {
173- break ;
174- }
175- } while ( isHeader ( filteredEmojis [ newIndex ] ) ) ;
176- } ;
177-
178- switch ( arrowKey ) {
179- case 'ArrowDown' :
180- move ( CONST . EMOJI_NUM_PER_ROW , ( ) => highlightedIndex + CONST . EMOJI_NUM_PER_ROW > filteredEmojis . length - 1 ) ;
181- break ;
182- case 'ArrowLeft' :
183- move (
184- - 1 ,
185- ( ) => highlightedIndex - 1 < firstNonHeaderIndex ,
186- ( ) => {
187- // Reaching start of the list, arrow left set the focus to searchInput.
188- focusInputWithTextSelect ( ) ;
189- newIndex = - 1 ;
190- } ,
191- ) ;
192- break ;
193- case 'ArrowRight' :
194- move ( 1 , ( ) => highlightedIndex + 1 > filteredEmojis . length - 1 ) ;
195- break ;
196- case 'ArrowUp' :
197- move (
198- - CONST . EMOJI_NUM_PER_ROW ,
199- ( ) => highlightedIndex - CONST . EMOJI_NUM_PER_ROW < firstNonHeaderIndex ,
200- ( ) => {
201- // Reaching start of the list, arrow up set the focus to searchInput.
202- focusInputWithTextSelect ( ) ;
203- newIndex = - 1 ;
204- } ,
205- ) ;
206- break ;
207- default :
208- break ;
209- }
210-
211- // Actually highlight the new emoji, apply keyboard movement styles, and disable pointer events
212- if ( newIndex !== highlightedIndex ) {
213- setHighlightedIndex ( newIndex ) ;
214- setArePointerEventsDisabled ( true ) ;
215- setIsUsingKeyboardMovement ( true ) ;
216- }
217- } ,
218- [ filteredEmojis , firstNonHeaderIndex , highlightedIndex , selection . end , selection . start ] ,
219- ) ;
220-
221134 const keyDownHandler = useCallback (
222135 ( keyBoardEvent ) => {
223136 if ( keyBoardEvent . key . startsWith ( 'Arrow' ) ) {
224137 if ( ! isFocused || keyBoardEvent . key === 'ArrowUp' || keyBoardEvent . key === 'ArrowDown' ) {
225138 keyBoardEvent . preventDefault ( ) ;
226139 }
227140
228- // Move the highlight when arrow keys are pressed
229- highlightAdjacentEmoji ( keyBoardEvent . key ) ;
230141 return ;
231142 }
232143
233144 // Select the currently highlighted emoji if enter is pressed
234- if ( ! isEnterWhileComposition ( keyBoardEvent ) && keyBoardEvent . key === CONST . KEYBOARD_SHORTCUTS . ENTER . shortcutKey && highlightedIndex !== - 1 ) {
235- const item = filteredEmojis [ highlightedIndex ] ;
145+ if ( ! isEnterWhileComposition ( keyBoardEvent ) && keyBoardEvent . key === CONST . KEYBOARD_SHORTCUTS . ENTER . shortcutKey ) {
146+ let indexToSelect = focusedIndex ;
147+ if ( highlightFirstEmoji ) {
148+ indexToSelect = 0 ;
149+ }
150+
151+ const item = filteredEmojis [ indexToSelect ] ;
236152 if ( ! item ) {
237153 return ;
238154 }
@@ -250,15 +166,14 @@ function EmojiPickerMenu({forwardedRef, onEmojiSelected}) {
250166 // interfering with the input behaviour.
251167 if ( keyBoardEvent . key === 'Tab' || keyBoardEvent . key === 'Enter' || ( keyBoardEvent . key === 'Shift' && searchInputRef . current && ! searchInputRef . current . isFocused ( ) ) ) {
252168 setIsUsingKeyboardMovement ( true ) ;
253- return ;
254169 }
255170
256171 // We allow typing in the search box if any key is pressed apart from Arrow keys.
257172 if ( searchInputRef . current && ! searchInputRef . current . isFocused ( ) && ReportUtils . shouldAutoFocusOnKeyPress ( keyBoardEvent ) ) {
258173 searchInputRef . current . focus ( ) ;
259174 }
260175 } ,
261- [ filteredEmojis , highlightAdjacentEmoji , highlightedIndex , isFocused , onEmojiSelected , preferredSkinTone ] ,
176+ [ filteredEmojis , focusedIndex , highlightFirstEmoji , isFocused , onEmojiSelected , preferredSkinTone ] ,
262177 ) ;
263178
264179 /**
@@ -343,32 +258,42 @@ function EmojiPickerMenu({forwardedRef, onEmojiSelected}) {
343258
344259 const emojiCode = types && types [ preferredSkinTone ] ? types [ preferredSkinTone ] : code ;
345260
346- const isEmojiFocused = index === highlightedIndex && isUsingKeyboardMovement ;
347- const shouldEmojiBeHighlighted = index === highlightedIndex && highlightFirstEmoji ;
261+ const isEmojiFocused = index === focusedIndex && isUsingKeyboardMovement ;
262+ const shouldEmojiBeHighlighted = index === focusedIndex && highlightEmoji ;
263+ const shouldFirstEmojiBeHighlighted = index === 0 && highlightFirstEmoji ;
348264
349265 return (
350266 < EmojiPickerMenuItem
351267 onPress = { singleExecution ( ( emoji ) => onEmojiSelected ( emoji , item ) ) }
352268 onHoverIn = { ( ) => {
269+ setHighlightEmoji ( false ) ;
353270 setHighlightFirstEmoji ( false ) ;
354271 if ( ! isUsingKeyboardMovement ) {
355272 return ;
356273 }
357274 setIsUsingKeyboardMovement ( false ) ;
358275 } }
359276 emoji = { emojiCode }
360- onFocus = { ( ) => setHighlightedIndex ( index ) }
361- onBlur = { ( ) =>
362- // Only clear the highlighted index if the highlighted index is the same,
363- // meaning that the focus changed to an element that is not an emoji item.
364- setHighlightedIndex ( ( prevState ) => ( prevState === index ? - 1 : prevState ) )
365- }
277+ onFocus = { ( ) => setFocusedIndex ( index ) }
366278 isFocused = { isEmojiFocused }
367- isHighlighted = { shouldEmojiBeHighlighted }
279+ isHighlighted = { shouldFirstEmojiBeHighlighted || shouldEmojiBeHighlighted }
368280 />
369281 ) ;
370282 } ,
371- [ preferredSkinTone , highlightedIndex , isUsingKeyboardMovement , highlightFirstEmoji , singleExecution , translate , onEmojiSelected , isSmallScreenWidth , windowWidth , styles ] ,
283+ [
284+ preferredSkinTone ,
285+ focusedIndex ,
286+ isUsingKeyboardMovement ,
287+ highlightEmoji ,
288+ highlightFirstEmoji ,
289+ singleExecution ,
290+ styles ,
291+ isSmallScreenWidth ,
292+ windowWidth ,
293+ translate ,
294+ onEmojiSelected ,
295+ setFocusedIndex ,
296+ ] ,
372297 ) ;
373298
374299 return (
@@ -389,9 +314,8 @@ function EmojiPickerMenu({forwardedRef, onEmojiSelected}) {
389314 defaultValue = ""
390315 ref = { searchInputRef }
391316 autoFocus = { shouldFocusInputOnScreenFocus }
392- onSelectionChange = { onSelectionChange }
393317 onFocus = { ( ) => {
394- setHighlightedIndex ( - 1 ) ;
318+ setFocusedIndex ( - 1 ) ;
395319 setIsFocused ( true ) ;
396320 setIsUsingKeyboardMovement ( false ) ;
397321 } }
@@ -413,7 +337,7 @@ function EmojiPickerMenu({forwardedRef, onEmojiSelected}) {
413337 ref = { emojiListRef }
414338 data = { filteredEmojis }
415339 renderItem = { renderItem }
416- extraData = { [ highlightedIndex , preferredSkinTone ] }
340+ extraData = { [ focusedIndex , preferredSkinTone ] }
417341 stickyHeaderIndices = { headerIndices }
418342 />
419343 </ View >
0 commit comments