Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useSafeAreaInsets from '@hooks/useSafeAreaInsets';
import useSingleExecution from '@hooks/useSingleExecution';
import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';

Expand All @@ -32,6 +33,7 @@ import useEmojiPickerMenu from './useEmojiPickerMenu';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function EmojiPickerMenu({onEmojiSelected, activeEmoji, ref}: EmojiPickerMenuProps) {
const styles = useThemeStyles();
const theme = useTheme();
const {windowWidth} = useWindowDimensions();
const {shouldUseNarrowLayout} = useResponsiveLayout();
const {translate} = useLocalize();
Expand Down Expand Up @@ -190,9 +192,13 @@ function EmojiPickerMenu({onEmojiSelected, activeEmoji, ref}: EmojiPickerMenuPro
<View style={[styles.emojiPickerContainer, StyleUtils.getEmojiPickerStyle(shouldUseNarrowLayout)]}>
<View style={[styles.p4, styles.pb3]}>
<TextInput
label={translate('common.search')}
placeholder={translate('common.search')}
placeholderTextColor={theme.textSupporting}
accessibilityLabel={translate('common.search')}
role={CONST.ROLE.PRESENTATION}
touchableInputWrapperStyle={styles.listSearchInputNarrowWrapper}
textInputContainerStyles={[styles.pb0, styles.ph3]}
inputStyle={[styles.lineHeightUndefined]}
onChangeText={(text: string) => {
setSearchText(text);
filterEmojis(text);
Expand Down
16 changes: 14 additions & 2 deletions src/components/EmojiPicker/EmojiPickerMenu/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import useLocalize from '@hooks/useLocalize';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useSingleExecution from '@hooks/useSingleExecution';
import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';

Expand Down Expand Up @@ -38,9 +39,14 @@ const throttleTime = isMobile() ? 200 : 50;

function EmojiPickerMenu({onEmojiSelected, activeEmoji, ref}: EmojiPickerMenuProps) {
const styles = useThemeStyles();
const theme = useTheme();
const StyleUtils = useStyleUtils();
const {windowWidth} = useWindowDimensions();
const {shouldUseNarrowLayout} = useResponsiveLayout();
// The compact search input must be sized by the physical device width, not by `shouldUseNarrowLayout`. Using
// `shouldUseNarrowLayout` would grow the input to the tall mobile size whenever it is rendered inside an
// RHP/narrow pane on web/desktop, so `isSmallScreenWidth` is intentionally used here to keep it compact.
// eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
const {shouldUseNarrowLayout, isSmallScreenWidth} = useResponsiveLayout();
const {translate} = useLocalize();
const {singleExecution} = useSingleExecution();
const {
Expand Down Expand Up @@ -400,9 +406,15 @@ function EmojiPickerMenu({onEmojiSelected, activeEmoji, ref}: EmojiPickerMenuPro
>
<View style={[styles.p4, styles.pb3]}>
<TextInput
label={translate('common.search')}
placeholder={translate('common.search')}
placeholderTextColor={theme.textSupporting}
accessibilityLabel={translate('common.search')}
role={CONST.ROLE.PRESENTATION}
// Size is based on device width (isSmallScreenWidth), not shouldUseNarrowLayout, so the search input
// stays the compact 34px size on web/desktop and only grows to 46px on mobile.
touchableInputWrapperStyle={isSmallScreenWidth ? styles.listSearchInputNarrowWrapper : styles.listSearchInputWideWrapper}
textInputContainerStyles={[styles.pb0, isSmallScreenWidth ? styles.ph3 : styles.ph2]}
inputStyle={[styles.w100, styles.lineHeightUndefined, isSmallScreenWidth ? undefined : styles.fontSizeLabel]}
onChangeText={(text: string) => {
setSearchText(text);
filterEmojis(text);
Expand Down
21 changes: 18 additions & 3 deletions src/components/SelectionList/components/TextInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types';
import useDebouncedAccessibilityAnnouncement from '@hooks/useDebouncedAccessibilityAnnouncement';
import useIsInLandscapeMode from '@hooks/useIsInLandscapeMode';
import useLocalize from '@hooks/useLocalize';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';

import Accessibility from '@libs/Accessibility';
Expand Down Expand Up @@ -72,7 +74,13 @@ function TextInput({
focusTextInput,
}: TextInputProps) {
const styles = useThemeStyles();
const theme = useTheme();
const {translate} = useLocalize();
// The compact search input must be sized by the physical device width, not by `shouldUseNarrowLayout`. Using
// `shouldUseNarrowLayout` would grow the input to the tall mobile size whenever it is rendered inside an
// RHP/narrow pane on web/desktop, so `isSmallScreenWidth` is intentionally used here to keep it compact.
// eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
const {isSmallScreenWidth} = useResponsiveLayout();
const {
label,
value,
Expand Down Expand Up @@ -155,12 +163,14 @@ function TextInput({
onKeyPress={onKeyPress}
onFocus={handleFocus}
onBlur={handleBlur}
label={label}
accessibilityLabel={accessibilityLabel}
// Use the smaller "above the table" search input size. The compact height cannot fit a
// floating label, so the label is rendered as a placeholder while a11y is preserved via accessibilityLabel.
accessibilityLabel={accessibilityLabel ?? label}
hint={hint}
role={CONST.ROLE.PRESENTATION}
value={value}
placeholder={placeholder}
placeholder={placeholder ?? label}
placeholderTextColor={theme.textSupporting}
maxLength={maxLength}
onChangeText={handleTextInputChange}
inputMode={inputMode}
Expand All @@ -173,6 +183,11 @@ function TextInput({
errorText={errorText}
autoCorrect={!disableAutoCorrect}
shouldInterceptSwipe={shouldInterceptSwipe ?? false}
// Size is based on device width (isSmallScreenWidth), not shouldUseNarrowLayout, so the input stays
// the compact 34px size on web/desktop even inside the RHP/narrow pane, and only grows to 46px on mobile.
touchableInputWrapperStyle={isSmallScreenWidth ? styles.listSearchInputNarrowWrapper : styles.listSearchInputWideWrapper}
textInputContainerStyles={[styles.pb0, isSmallScreenWidth ? styles.ph3 : styles.ph2]}
inputStyle={[styles.w100, styles.lineHeightUndefined, isSmallScreenWidth ? undefined : styles.fontSizeLabel]}
/>
</View>
{shouldShowHeaderMessage && (
Expand Down
5 changes: 5 additions & 0 deletions src/styles/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5220,6 +5220,11 @@ const staticStyles = (theme: ThemeColors) =>
searchPageInputWideTouchableWrapper: {height: 34, width: 202},
searchPageInputNarrowTouchableWrapper: {height: 46},

// Compact search inputs that appear above lists/popovers. Matches the smaller
// "above the table" search input heights (34 on web/desktop, 46 on mobile).
listSearchInputWideWrapper: {height: 34},
listSearchInputNarrowWrapper: {height: 46},

walletStaticIllustration: {
width: 262,
height: 152,
Expand Down
18 changes: 12 additions & 6 deletions tests/perf-test/SelectionList.perf-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type {KeyboardStateContextValue} from '@components/withKeyboardState';

import variables from '@styles/variables';

import type * as NativeNavigation from '@react-navigation/native';
import type {ComponentType} from 'react';
import type ReactNative from 'react-native';

Expand Down Expand Up @@ -64,12 +65,17 @@ jest.mock('@react-navigation/stack', () => ({
useCardAnimation: () => {},
}));

jest.mock('@react-navigation/native', () => ({
...jest.requireActual<Record<string, unknown>>('@react-navigation/native'),
useFocusEffect: () => {},
useIsFocused: () => true,
createNavigationContainerRef: jest.fn(),
}));
jest.mock('@react-navigation/native', () => {
// Spread the actual module so context objects like NavigationContainerRefContext and NavigationContext
// remain defined. useResponsiveLayout (native) reads them via useContext, which crashes if they are undefined.
const actualNav = jest.requireActual<typeof NativeNavigation>('@react-navigation/native');
return {
...actualNav,
useFocusEffect: () => {},
useIsFocused: () => true,
createNavigationContainerRef: jest.fn(),
};
});

jest.mock('../../src/hooks/useKeyboardState', () => ({
__esModule: true,
Expand Down
Loading