Increase search `TextInput` hit area and improve the related UI (#3748)

* improve hit area of search text input

use text cursor on web

use a pressable instead

use a vertical padding of 9

oops

move vertical padding to `TextInput` to increase hit area

* Hide it from a11y tree, change cursor

* Hide clear on empty text

* Render either Clear or Cancel

* Remove Clear button

* Animate it

* Better animation

---------

Co-authored-by: Dan Abramov <dan.abramov@gmail.com>
zio/stable
Hailey 2024-04-28 21:12:20 -07:00 committed by GitHub
parent dfce190cb6
commit 388c4f79cf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1 changed files with 63 additions and 41 deletions

View File

@ -7,6 +7,13 @@ import {
TextInput, TextInput,
View, View,
} from 'react-native' } from 'react-native'
import Animated, {
FadeIn,
FadeOut,
LinearTransition,
useAnimatedStyle,
withSpring,
} from 'react-native-reanimated'
import {AppBskyActorDefs, AppBskyFeedDefs, moderateProfile} from '@atproto/api' import {AppBskyActorDefs, AppBskyFeedDefs, moderateProfile} from '@atproto/api'
import { import {
FontAwesomeIcon, FontAwesomeIcon,
@ -56,6 +63,7 @@ import {
} from '#/view/shell/desktop/Search' } from '#/view/shell/desktop/Search'
import {ProfileCardFeedLoadingPlaceholder} from 'view/com/util/LoadingPlaceholder' import {ProfileCardFeedLoadingPlaceholder} from 'view/com/util/LoadingPlaceholder'
import {atoms as a} from '#/alf' import {atoms as a} from '#/alf'
const AnimatedPressable = Animated.createAnimatedComponent(Pressable)
function Loader() { function Loader() {
const pal = usePalette('default') const pal = usePalette('default')
@ -527,23 +535,10 @@ export function SearchScreen(
const onPressCancelSearch = React.useCallback(() => { const onPressCancelSearch = React.useCallback(() => {
scrollToTopWeb() scrollToTopWeb()
textInput.current?.blur()
if (showAutocomplete) { setShowAutocomplete(false)
textInput.current?.blur() setSearchText(queryParam)
setShowAutocomplete(false) }, [queryParam])
setSearchText(queryParam)
} else {
// If we just `setParams` and set `q` to an empty string, the URL still displays `q=`, which isn't pretty.
// However, `.replace()` on native has a "push" animation that we don't want. So we need to handle these
// differently.
if (isWeb) {
navigation.replace('Search', {})
} else {
setSearchText('')
navigation.setParams({q: ''})
}
}
}, [showAutocomplete, navigation, queryParam])
const onChangeText = React.useCallback(async (text: string) => { const onChangeText = React.useCallback(async (text: string) => {
scrollToTopWeb() scrollToTopWeb()
@ -629,6 +624,14 @@ export function SearchScreen(
) )
} }
const showClearButton = showAutocomplete && searchText.length > 0
const clearButtonStyle = useAnimatedStyle(() => ({
opacity: withSpring(showClearButton ? 1 : 0, {
overshootClamping: true,
duration: 50,
}),
}))
return ( return (
<View style={isWeb ? null : {flex: 1}}> <View style={isWeb ? null : {flex: 1}}>
<CenteredView <CenteredView
@ -656,11 +659,24 @@ export function SearchScreen(
</Pressable> </Pressable>
)} )}
<View <AnimatedPressable
// This only exists only for extra hitslop so don't expose it to the a11y tree.
accessible={false}
focusable={false}
// @ts-ignore web-only
tabIndex={-1}
layout={isNative ? LinearTransition.duration(200) : undefined}
style={[ style={[
{backgroundColor: pal.colors.backgroundLight}, {backgroundColor: pal.colors.backgroundLight},
styles.headerSearchContainer, styles.headerSearchContainer,
]}> isWeb && {
// @ts-ignore web only
cursor: 'default',
},
]}
onPress={() => {
textInput.current?.focus()
}}>
<MagnifyingGlassIcon <MagnifyingGlassIcon
style={[pal.icon, styles.headerSearchIcon]} style={[pal.icon, styles.headerSearchIcon]}
size={21} size={21}
@ -702,33 +718,36 @@ export function SearchScreen(
autoComplete="off" autoComplete="off"
autoCapitalize="none" autoCapitalize="none"
/> />
{showAutocomplete ? ( <AnimatedPressable
<Pressable layout={isNative ? LinearTransition.duration(200) : undefined}
testID="searchTextInputClearBtn" disabled={!showClearButton}
onPress={onPressClearQuery} style={clearButtonStyle}
accessibilityRole="button" testID="searchTextInputClearBtn"
accessibilityLabel={_(msg`Clear search query`)} onPress={onPressClearQuery}
accessibilityHint="" accessibilityRole="button"
hitSlop={HITSLOP_10}> accessibilityLabel={_(msg`Clear search query`)}
<FontAwesomeIcon accessibilityHint=""
icon="xmark" hitSlop={HITSLOP_10}>
size={16} <FontAwesomeIcon
style={pal.textLight as FontAwesomeIconStyle} icon="xmark"
/> size={16}
</Pressable> style={pal.textLight as FontAwesomeIconStyle}
) : undefined} />
</View> </AnimatedPressable>
</AnimatedPressable>
{(queryParam || showAutocomplete) && ( {showAutocomplete && (
<View style={styles.headerCancelBtn}> <View style={[styles.headerCancelBtn]}>
<Pressable <AnimatedPressable
entering={isNative ? FadeIn.duration(300) : undefined}
exiting={isNative ? FadeOut.duration(50) : undefined}
key="cancel"
onPress={onPressCancelSearch} onPress={onPressCancelSearch}
accessibilityRole="button" accessibilityRole="button"
hitSlop={HITSLOP_10}> hitSlop={HITSLOP_10}>
<Text style={[pal.text]}> <Text style={pal.text}>
<Trans>Cancel</Trans> <Trans>Cancel</Trans>
</Text> </Text>
</Pressable> </AnimatedPressable>
</View> </View>
)} )}
</CenteredView> </CenteredView>
@ -880,6 +899,9 @@ const styles = StyleSheet.create({
}, },
headerCancelBtn: { headerCancelBtn: {
paddingLeft: 10, paddingLeft: 10,
alignSelf: 'center',
zIndex: -1,
elevation: -1, // For Android
}, },
tabBarContainer: { tabBarContainer: {
// @ts-ignore web only // @ts-ignore web only