Skip to content

Commit 46d5d85

Browse files
committed
Merge branch 'main' into ideal-nav-merge
2 parents d9165c5 + 5007450 commit 46d5d85

File tree

9 files changed

+215
-162
lines changed

9 files changed

+215
-162
lines changed

.github/workflows/e2ePerformanceTests.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ jobs:
4646
git fetch origin tag ${{ steps.getMostRecentRelease.outputs.VERSION }} --no-tags --depth=1
4747
git switch --detach ${{ steps.getMostRecentRelease.outputs.VERSION }}
4848
49-
- uses: ./.github/actions/composite/buildAndroidE2EAPK
49+
- uses: Expensify/App/.github/actions/composite/buildAndroidE2EAPK@main
5050
with:
5151
ARTIFACT_NAME: baseline-apk-${{ steps.getMostRecentRelease.outputs.VERSION }}
5252
PACKAGE_SCRIPT_NAME: android-build-e2e
@@ -114,7 +114,7 @@ jobs:
114114
- name: Checkout "delta ref"
115115
run: git checkout ${{ steps.getDeltaRef.outputs.DELTA_REF }}
116116

117-
- uses: ./.github/actions/composite/buildAndroidE2EAPK
117+
- uses: Expensify/App/.github/actions/composite/buildAndroidE2EAPK@main
118118
with:
119119
ARTIFACT_NAME: delta-apk-${{ steps.getDeltaRef.outputs.DELTA_REF }}
120120
PACKAGE_SCRIPT_NAME: android-build-e2edelta

package-lock.json

Lines changed: 0 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/components/EmojiPicker/EmojiPickerMenu/index.js

Lines changed: 69 additions & 145 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import _ from 'underscore';
77
import EmojiPickerMenuItem from '@components/EmojiPicker/EmojiPickerMenuItem';
88
import Text from '@components/Text';
99
import TextInput from '@components/TextInput';
10+
import useArrowKeyFocusManager from '@hooks/useArrowKeyFocusManager';
1011
import useLocalize from '@hooks/useLocalize';
1112
import useSingleExecution from '@hooks/useSingleExecution';
1213
import 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>

src/components/EmojiPicker/EmojiPickerMenu/index.native.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ function EmojiPickerMenu({onEmojiSelected}) {
2121
const styles = useThemeStyles();
2222
const {windowWidth, isSmallScreenWidth} = useWindowDimensions();
2323
const {translate} = useLocalize();
24+
const {singleExecution} = useSingleExecution();
2425
const {
2526
allEmojis,
2627
headerEmojis,
@@ -35,7 +36,6 @@ function EmojiPickerMenu({onEmojiSelected}) {
3536
listStyle,
3637
emojiListRef,
3738
} = useEmojiPickerMenu();
38-
const {singleExecution} = useSingleExecution();
3939
const StyleUtils = useStyleUtils();
4040

4141
/**
@@ -73,7 +73,7 @@ function EmojiPickerMenu({onEmojiSelected}) {
7373
/**
7474
* Given an emoji item object, render a component based on its type.
7575
* Items with the code "SPACER" return nothing and are used to fill rows up to 8
76-
* so that the sticky headers function properly
76+
* so that the sticky headers function properly.
7777
*
7878
* @param {Object} item
7979
* @returns {*}

src/components/EmojiPicker/EmojiPickerMenu/useEmojiPickerMenu.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ const useEmojiPickerMenu = () => {
1616
const allEmojis = useMemo(() => EmojiUtils.mergeEmojisWithFrequentlyUsedEmojis(emojis), [frequentlyUsedEmojis]);
1717
const headerEmojis = useMemo(() => EmojiUtils.getHeaderEmojis(allEmojis), [allEmojis]);
1818
const headerRowIndices = useMemo(() => _.map(headerEmojis, (headerEmoji) => headerEmoji.index), [headerEmojis]);
19+
const spacersIndexes = useMemo(() => EmojiUtils.getSpacersIndexes(allEmojis), [allEmojis]);
1920
const [filteredEmojis, setFilteredEmojis] = useState(allEmojis);
2021
const [headerIndices, setHeaderIndices] = useState(headerRowIndices);
2122
const isListFiltered = allEmojis.length !== filteredEmojis.length;
@@ -61,6 +62,7 @@ const useEmojiPickerMenu = () => {
6162
preferredSkinTone,
6263
listStyle,
6364
emojiListRef,
65+
spacersIndexes,
6466
};
6567
};
6668

0 commit comments

Comments
 (0)