Improved search page (#7590)

* search input revamp

* fix web

* rm "useThemeNew"

* fix overlap during transition

* animate header properly

* reduce gap

* animate cancel button in

* don't move search bar when focused

* remove cancel button animation
This commit is contained in:
Samuel Newman
2025-02-12 22:48:29 +00:00
committed by GitHub
parent 1a3ecdf6ec
commit b37199a5a0
3 changed files with 101 additions and 93 deletions
+2 -1
View File
@@ -1,5 +1,5 @@
import {createContext, useCallback, useContext} from 'react' import {createContext, useCallback, useContext} from 'react'
import {GestureResponderEvent, View} from 'react-native' import {GestureResponderEvent, Keyboard, View} from 'react-native'
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/native' import {useNavigation} from '@react-navigation/native'
@@ -140,6 +140,7 @@ export function MenuButton() {
const {gtMobile} = useBreakpoints() const {gtMobile} = useBreakpoints()
const onPress = useCallback(() => { const onPress = useCallback(() => {
Keyboard.dismiss()
setDrawerOpen(true) setDrawerOpen(true)
}, [setDrawerOpen]) }, [setDrawerOpen])
+1 -1
View File
@@ -226,6 +226,7 @@ export function createInput(Component: typeof TextInput) {
<> <>
<Component <Component
accessibilityHint={undefined} accessibilityHint={undefined}
hitSlop={HITSLOP_20}
{...rest} {...rest}
accessibilityLabel={label} accessibilityLabel={label}
ref={refs} ref={refs}
@@ -242,7 +243,6 @@ export function createInput(Component: typeof TextInput) {
placeholder={placeholder || label} placeholder={placeholder || label}
placeholderTextColor={t.palette.contrast_500} placeholderTextColor={t.palette.contrast_500}
keyboardAppearance={t.name === 'light' ? 'light' : 'dark'} keyboardAppearance={t.name === 'light' ? 'light' : 'dark'}
hitSlop={HITSLOP_20}
style={flattened} style={flattened}
/> />
+98 -91
View File
@@ -1,4 +1,4 @@
import React, {useCallback} from 'react' import React, {useCallback, useLayoutEffect} from 'react'
import { import {
ActivityIndicator, ActivityIndicator,
Image, Image,
@@ -21,7 +21,7 @@ import {useLingui} from '@lingui/react'
import {useFocusEffect, useNavigation} from '@react-navigation/native' import {useFocusEffect, useNavigation} from '@react-navigation/native'
import {APP_LANGUAGES, LANGUAGES} from '#/lib/../locale/languages' import {APP_LANGUAGES, LANGUAGES} from '#/lib/../locale/languages'
import {createHitslop} from '#/lib/constants' import {createHitslop, HITSLOP_20} from '#/lib/constants'
import {HITSLOP_10} from '#/lib/constants' import {HITSLOP_10} from '#/lib/constants'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
import {usePalette} from '#/lib/hooks/usePalette' import {usePalette} from '#/lib/hooks/usePalette'
@@ -46,7 +46,6 @@ import {usePopularFeedsSearch} from '#/state/queries/feed'
import {useProfilesQuery} from '#/state/queries/profile' import {useProfilesQuery} from '#/state/queries/profile'
import {useSearchPostsQuery} from '#/state/queries/search-posts' import {useSearchPostsQuery} from '#/state/queries/search-posts'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
import {useSetDrawerOpen} from '#/state/shell'
import {useSetMinimalShellMode} from '#/state/shell' import {useSetMinimalShellMode} from '#/state/shell'
import {Pager} from '#/view/com/pager/Pager' import {Pager} from '#/view/com/pager/Pager'
import {TabBar} from '#/view/com/pager/TabBar' import {TabBar} from '#/view/com/pager/TabBar'
@@ -58,18 +57,11 @@ import {Text} from '#/view/com/util/text/Text'
import {Explore} from '#/view/screens/Search/Explore' import {Explore} from '#/view/screens/Search/Explore'
import {SearchLinkCard, SearchProfileCard} from '#/view/shell/desktop/Search' import {SearchLinkCard, SearchProfileCard} from '#/view/shell/desktop/Search'
import {makeSearchQuery, parseSearchQuery} from '#/screens/Search/utils' import {makeSearchQuery, parseSearchQuery} from '#/screens/Search/utils'
import { import {atoms as a, tokens, useBreakpoints, useTheme, web} from '#/alf'
atoms as a, import {Button, ButtonText} from '#/components/Button'
tokens,
useBreakpoints,
useTheme as useThemeNew,
web,
} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as FeedCard from '#/components/FeedCard' import * as FeedCard from '#/components/FeedCard'
import {SearchInput} from '#/components/forms/SearchInput' import {SearchInput} from '#/components/forms/SearchInput'
import {ChevronBottom_Stroke2_Corner0_Rounded as ChevronDown} from '#/components/icons/Chevron' import {ChevronBottom_Stroke2_Corner0_Rounded as ChevronDown} from '#/components/icons/Chevron'
import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu'
import * as Layout from '#/components/Layout' import * as Layout from '#/components/Layout'
import {account, useStorage} from '#/storage' import {account, useStorage} from '#/storage'
@@ -278,7 +270,7 @@ let SearchScreenFeedsResults = ({
query: string query: string
active: boolean active: boolean
}): React.ReactNode => { }): React.ReactNode => {
const t = useThemeNew() const t = useTheme()
const {_} = useLingui() const {_} = useLingui()
const {data: results, isFetched} = usePopularFeedsSearch({ const {data: results, isFetched} = usePopularFeedsSearch({
@@ -323,7 +315,7 @@ function SearchLanguageDropdown({
value: string value: string
onChange(value: string): void onChange(value: string): void
}) { }) {
const t = useThemeNew() const t = useTheme()
const {_} = useLingui() const {_} = useLingui()
const {appLanguage, contentLanguages} = useLanguagePrefs() const {appLanguage, contentLanguages} = useLanguagePrefs()
@@ -534,12 +526,7 @@ let SearchScreenInner = ({
<Pager <Pager
onPageSelected={onPageSelected} onPageSelected={onPageSelected}
renderTabBar={props => ( renderTabBar={props => (
<Layout.Center <Layout.Center style={[a.z_10, web([a.sticky, {top: headerHeight}])]}>
style={[
a.z_10,
web([a.sticky]),
{top: isWeb ? headerHeight : undefined},
]}>
<TabBar items={sections.map(section => section.title)} {...props} /> <TabBar items={sections.map(section => section.title)} {...props} />
</Layout.Center> </Layout.Center>
)} )}
@@ -597,12 +584,11 @@ SearchScreenInner = React.memo(SearchScreenInner)
export function SearchScreen( export function SearchScreen(
props: NativeStackScreenProps<SearchTabNavigatorParams, 'Search'>, props: NativeStackScreenProps<SearchTabNavigatorParams, 'Search'>,
) { ) {
const t = useThemeNew() const t = useTheme()
const {gtMobile} = useBreakpoints() const {gtMobile} = useBreakpoints()
const navigation = useNavigation<NavigationProp>() const navigation = useNavigation<NavigationProp>()
const textInput = React.useRef<TextInput>(null) const textInput = React.useRef<TextInput>(null)
const {_} = useLingui() const {_} = useLingui()
const setDrawerOpen = useSetDrawerOpen()
const setMinimalShellMode = useSetMinimalShellMode() const setMinimalShellMode = useSetMinimalShellMode()
const {currentAccount} = useSession() const {currentAccount} = useSession()
@@ -630,6 +616,7 @@ export function SearchScreen(
const updateSearchHistory = useCallback( const updateSearchHistory = useCallback(
async (item: string) => { async (item: string) => {
if (!item) return
const newSearchHistory = [ const newSearchHistory = [
item, item,
...termHistory.filter(search => search !== item), ...termHistory.filter(search => search !== item),
@@ -667,11 +654,17 @@ export function SearchScreen(
initialQuery: queryParam, initialQuery: queryParam,
}) })
const showFilters = Boolean(queryWithParams && !showAutocomplete) const showFilters = Boolean(queryWithParams && !showAutocomplete)
/*
* Arbitrary sizing, so guess and check, used for sticky header alignment and // web only - measure header height for sticky positioning
* sizing. const [headerHeight, setHeaderHeight] = React.useState(0)
*/ const headerRef = React.useRef(null)
const headerHeight = 60 + (showFilters ? 40 : 0) useLayoutEffect(() => {
if (isWeb) {
if (!headerRef.current) return
const measurement = (headerRef.current as Element).getBoundingClientRect()
setHeaderHeight(measurement.height)
}
}, [])
useFocusEffect( useFocusEffect(
useNonReactiveCallback(() => { useNonReactiveCallback(() => {
@@ -681,11 +674,6 @@ export function SearchScreen(
}), }),
) )
const onPressMenu = React.useCallback(() => {
textInput.current?.blur()
setDrawerOpen(true)
}, [setDrawerOpen])
const onPressClearQuery = React.useCallback(() => { const onPressClearQuery = React.useCallback(() => {
scrollToTopWeb() scrollToTopWeb()
setSearchText('') setSearchText('')
@@ -789,75 +777,94 @@ export function SearchScreen(
return ( return (
<Layout.Screen testID="searchScreen"> <Layout.Screen testID="searchScreen">
<View <View
ref={headerRef}
onLayout={evt => {
if (isWeb) setHeaderHeight(evt.nativeEvent.layout.height)
}}
style={[ style={[
a.relative,
a.z_10,
web({ web({
height: headerHeight,
position: 'sticky', position: 'sticky',
top: 0, top: 0,
zIndex: 1,
}), }),
]}> ]}>
<Layout.Center> <Layout.Center style={t.atoms.bg}>
<View style={[a.p_md, a.pb_sm, a.gap_sm, t.atoms.bg]}> {!gtMobile && (
<View style={[a.flex_row, a.gap_sm]}> <View
{!gtMobile && !showAutocomplete && ( // HACK: shift up search input. we can't remove the top padding
<Button // on the search input because it messes up the layout animation
testID="viewHeaderBackOrMenuBtn" // if we add it only when the header is hidden
onPress={onPressMenu} style={{marginBottom: tokens.space.sm * -1}}>
hitSlop={HITSLOP_10} <Layout.Header.Outer noBottomBorder>
label={_(msg`Menu`)} <Layout.Header.MenuButton />
accessibilityHint={_( <Layout.Header.Content
msg`Provides access to navigation links and settings`, align={showFilters ? 'left' : 'platform'}>
)} <Layout.Header.TitleText>
size="large" <Trans>Search</Trans>
variant="solid" </Layout.Header.TitleText>
color="secondary" </Layout.Header.Content>
shape="square"> {showFilters ? (
<ButtonIcon icon={Menu} size="lg" /> <View style={[{minWidth: 140}]}>
</Button> <SearchLanguageDropdown
)} value={params.lang}
<View style={[a.flex_1]}> onChange={params.setLang}
<SearchInput />
ref={textInput} </View>
value={searchText} ) : (
onFocus={onSearchInputFocus} <Layout.Header.Slot />
onChangeText={onChangeText} )}
onClearText={onPressClearQuery} </Layout.Header.Outer>
onSubmitEditing={onSubmit}
/>
</View>
{showAutocomplete && (
<Button
label={_(msg`Cancel search`)}
size="large"
variant="ghost"
color="secondary"
style={[a.px_sm]}
onPress={onPressCancelSearch}
hitSlop={HITSLOP_10}>
<ButtonText>
<Trans>Cancel</Trans>
</ButtonText>
</Button>
)}
</View> </View>
)}
{showFilters && ( <View style={[a.px_md, a.pt_sm, a.pb_sm, a.overflow_hidden]}>
<View <View style={[a.gap_sm]}>
style={[ <View style={[a.w_full, a.flex_row, a.align_stretch, a.gap_xs]}>
a.flex_row, <View style={[a.flex_1]}>
a.align_center, <SearchInput
a.justify_between, ref={textInput}
a.gap_sm, value={searchText}
]}> onFocus={onSearchInputFocus}
<View style={[{width: 140}]}> onChangeText={onChangeText}
<SearchLanguageDropdown onClearText={onPressClearQuery}
value={params.lang} onSubmitEditing={onSubmit}
onChange={params.setLang} placeholder={_(msg`Search for posts, users, or feeds`)}
hitSlop={{...HITSLOP_20, top: 0}}
/> />
</View> </View>
{showAutocomplete && (
<Button
label={_(msg`Cancel search`)}
size="large"
variant="ghost"
color="secondary"
style={[a.px_sm]}
onPress={onPressCancelSearch}
hitSlop={HITSLOP_10}>
<ButtonText>
<Trans>Cancel</Trans>
</ButtonText>
</Button>
)}
</View> </View>
)}
{showFilters && gtMobile && (
<View
style={[
a.flex_row,
a.align_center,
a.justify_between,
a.gap_sm,
]}>
<View style={[{width: 140}]}>
<SearchLanguageDropdown
value={params.lang}
onChange={params.setLang}
/>
</View>
</View>
)}
</View>
</View> </View>
</Layout.Center> </Layout.Center>
</View> </View>