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
parent
dfce190cb6
commit
388c4f79cf
|
@ -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()
|
||||||
|
|
||||||
if (showAutocomplete) {
|
|
||||||
textInput.current?.blur()
|
textInput.current?.blur()
|
||||||
setShowAutocomplete(false)
|
setShowAutocomplete(false)
|
||||||
setSearchText(queryParam)
|
setSearchText(queryParam)
|
||||||
} else {
|
}, [queryParam])
|
||||||
// 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,8 +718,10 @@ export function SearchScreen(
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
/>
|
/>
|
||||||
{showAutocomplete ? (
|
<AnimatedPressable
|
||||||
<Pressable
|
layout={isNative ? LinearTransition.duration(200) : undefined}
|
||||||
|
disabled={!showClearButton}
|
||||||
|
style={clearButtonStyle}
|
||||||
testID="searchTextInputClearBtn"
|
testID="searchTextInputClearBtn"
|
||||||
onPress={onPressClearQuery}
|
onPress={onPressClearQuery}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
|
@ -715,20 +733,21 @@ export function SearchScreen(
|
||||||
size={16}
|
size={16}
|
||||||
style={pal.textLight as FontAwesomeIconStyle}
|
style={pal.textLight as FontAwesomeIconStyle}
|
||||||
/>
|
/>
|
||||||
</Pressable>
|
</AnimatedPressable>
|
||||||
) : undefined}
|
</AnimatedPressable>
|
||||||
</View>
|
{showAutocomplete && (
|
||||||
|
<View style={[styles.headerCancelBtn]}>
|
||||||
{(queryParam || showAutocomplete) && (
|
<AnimatedPressable
|
||||||
<View style={styles.headerCancelBtn}>
|
entering={isNative ? FadeIn.duration(300) : undefined}
|
||||||
<Pressable
|
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
|
||||||
|
|
Loading…
Reference in New Issue