ALF text input for generic search input (#5511)

* alf text input for generic search input

* clearer ref naming

* Adjust props and definition

* Migrate props

* Migrate props

* Migrate props

* Replace on search screen

* rm old props

---------

Co-authored-by: Eric Bailey <git@esb.lol>
This commit is contained in:
Samuel Newman
2024-10-01 00:53:11 +03:00
committed by GitHub
parent b4941d8556
commit 2129f69fa0
8 changed files with 129 additions and 253 deletions
+75
View File
@@ -0,0 +1,75 @@
import React from 'react'
import {TextInput, View} from 'react-native'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {HITSLOP_10} from '#/lib/constants'
import {isNative} from '#/platform/detection'
import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonIcon} from '#/components/Button'
import * as TextField from '#/components/forms/TextField'
import {MagnifyingGlass2_Stroke2_Corner0_Rounded as MagnifyingGlassIcon} from '#/components/icons/MagnifyingGlass2'
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
type SearchInputProps = Omit<TextField.InputProps, 'label'> & {
label?: TextField.InputProps['label']
/**
* Called when the user presses the (X) button
*/
onClearText?: () => void
}
export const SearchInput = React.forwardRef<TextInput, SearchInputProps>(
function SearchInput({value, label, onClearText, ...rest}, ref) {
const t = useTheme()
const {_} = useLingui()
return (
<View style={[a.w_full, a.relative]}>
<TextField.Root>
<TextField.Icon icon={MagnifyingGlassIcon} />
<TextField.Input
inputRef={ref}
label={label || _(msg`Search`)}
value={value}
placeholder={_(msg`Search`)}
returnKeyType="search"
keyboardAppearance={t.scheme}
selectTextOnFocus={isNative}
autoFocus={false}
accessibilityRole="search"
autoCorrect={false}
autoComplete="off"
autoCapitalize="none"
{...rest}
/>
</TextField.Root>
{value && value.length > 0 && (
<View
style={[
a.absolute,
a.z_10,
a.my_auto,
a.inset_0,
a.justify_center,
a.pr_sm,
{left: 'auto'},
]}>
<Button
testID="searchTextInputClearBtn"
onPress={onClearText}
label={_(msg`Clear search query`)}
hitSlop={HITSLOP_10}
size="tiny"
shape="round"
variant="ghost"
color="secondary">
<ButtonIcon icon={X} size="xs" />
</Button>
</View>
)}
</View>
)
},
)
+1 -1
View File
@@ -126,7 +126,7 @@ export type InputProps = Omit<TextInputProps, 'value' | 'onChangeText'> & {
value?: string
onChangeText?: (value: string) => void
isInvalid?: boolean
inputRef?: React.RefObject<TextInput>
inputRef?: React.RefObject<TextInput> | React.ForwardedRef<TextInput>
}
export function createInput(Component: typeof TextInput) {
+8 -10
View File
@@ -4,17 +4,17 @@ import {KeyboardAwareScrollView} from 'react-native-keyboard-controller'
import {AppBskyFeedDefs, ModerationOpts} from '@atproto/api'
import {Trans} from '@lingui/macro'
import {DISCOVER_FEED_URI} from '#/lib/constants'
import {useA11y} from '#/state/a11y'
import {DISCOVER_FEED_URI} from 'lib/constants'
import {
useGetPopularFeedsQuery,
usePopularFeedsSearch,
useSavedFeeds,
} from 'state/queries/feed'
import {SearchInput} from 'view/com/util/forms/SearchInput'
import {List} from 'view/com/util/List'
} from '#/state/queries/feed'
import {List} from '#/view/com/util/List'
import {useWizardState} from '#/screens/StarterPack/Wizard/State'
import {atoms as a, useTheme} from '#/alf'
import {SearchInput} from '#/components/forms/SearchInput'
import {useThrottledValue} from '#/components/hooks/useThrottledValue'
import {Loader} from '#/components/Loader'
import {ScreenTransition} from '#/components/StarterPack/Wizard/ScreenTransition'
@@ -81,12 +81,11 @@ export function StepFeeds({moderationOpts}: {moderationOpts: ModerationOpts}) {
return (
<ScreenTransition style={[a.flex_1]} direction={state.transitionDirection}>
<View style={[a.border_b, t.atoms.border_contrast_medium]}>
<View style={[a.my_sm, a.px_md, {height: 40}]}>
<View style={[a.py_sm, a.px_md, {height: 60}]}>
<SearchInput
query={query}
onChangeQuery={t => setQuery(t)}
onPressCancelSearch={() => setQuery('')}
onSubmitQuery={() => {}}
value={query}
onChangeText={t => setQuery(t)}
onClearText={() => setQuery('')}
/>
</View>
</View>
@@ -94,7 +93,6 @@ export function StepFeeds({moderationOpts}: {moderationOpts: ModerationOpts}) {
data={query ? searchedFeeds : suggestedFeeds}
renderItem={renderItem}
keyExtractor={keyExtractor}
contentContainerStyle={{paddingTop: 6}}
onEndReached={
!query && !screenReaderEnabled ? () => fetchNextPage() : undefined
}
@@ -4,14 +4,14 @@ import {KeyboardAwareScrollView} from 'react-native-keyboard-controller'
import {AppBskyActorDefs, ModerationOpts} from '@atproto/api'
import {Trans} from '@lingui/macro'
import {isNative} from '#/platform/detection'
import {useA11y} from '#/state/a11y'
import {isNative} from 'platform/detection'
import {useActorAutocompleteQuery} from 'state/queries/actor-autocomplete'
import {useActorSearchPaginated} from 'state/queries/actor-search'
import {SearchInput} from 'view/com/util/forms/SearchInput'
import {List} from 'view/com/util/List'
import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete'
import {useActorSearchPaginated} from '#/state/queries/actor-search'
import {List} from '#/view/com/util/List'
import {useWizardState} from '#/screens/StarterPack/Wizard/State'
import {atoms as a, useTheme} from '#/alf'
import {SearchInput} from '#/components/forms/SearchInput'
import {Loader} from '#/components/Loader'
import {ScreenTransition} from '#/components/StarterPack/Wizard/ScreenTransition'
import {WizardProfileCard} from '#/components/StarterPack/Wizard/WizardListCard'
@@ -65,12 +65,11 @@ export function StepProfiles({
return (
<ScreenTransition style={[a.flex_1]} direction={state.transitionDirection}>
<View style={[a.border_b, t.atoms.border_contrast_medium]}>
<View style={[a.my_sm, a.px_md, {height: 40}]}>
<View style={[a.py_sm, a.px_md, {height: 60}]}>
<SearchInput
query={query}
onChangeQuery={setQuery}
onPressCancelSearch={() => setQuery('')}
onSubmitQuery={() => {}}
value={query}
onChangeText={setQuery}
onClearText={() => setQuery('')}
/>
</View>
</View>
-124
View File
@@ -1,124 +0,0 @@
import React from 'react'
import {
StyleProp,
StyleSheet,
TextInput,
TouchableOpacity,
View,
ViewStyle,
} from 'react-native'
import {
FontAwesomeIcon,
FontAwesomeIconStyle,
} from '@fortawesome/react-native-fontawesome'
import {HITSLOP_10} from 'lib/constants'
import {MagnifyingGlassIcon} from 'lib/icons'
import {useTheme} from 'lib/ThemeContext'
import {usePalette} from 'lib/hooks/usePalette'
import {useLingui} from '@lingui/react'
import {msg} from '@lingui/macro'
interface Props {
query: string
setIsInputFocused?: (v: boolean) => void
onChangeQuery: (v: string) => void
onPressCancelSearch: () => void
onSubmitQuery: () => void
style?: StyleProp<ViewStyle>
}
export interface SearchInputRef {
focus?: () => void
}
export const SearchInput = React.forwardRef<SearchInputRef, Props>(
function SearchInput(
{
query,
setIsInputFocused,
onChangeQuery,
onPressCancelSearch,
onSubmitQuery,
style,
},
ref,
) {
const theme = useTheme()
const pal = usePalette('default')
const {_} = useLingui()
const textInput = React.useRef<TextInput>(null)
const onPressCancelSearchInner = React.useCallback(() => {
onPressCancelSearch()
textInput.current?.blur()
}, [onPressCancelSearch, textInput])
React.useImperativeHandle(ref, () => ({
focus: () => textInput.current?.focus(),
blur: () => textInput.current?.blur(),
}))
return (
<View style={[pal.viewLight, styles.container, style]}>
<MagnifyingGlassIcon style={[pal.icon, styles.icon]} size={21} />
<TextInput
testID="searchTextInput"
ref={textInput}
placeholder={_(msg`Search`)}
placeholderTextColor={pal.colors.textLight}
selectTextOnFocus
returnKeyType="search"
value={query}
style={[pal.text, styles.input]}
keyboardAppearance={theme.colorScheme}
onFocus={() => setIsInputFocused?.(true)}
onBlur={() => setIsInputFocused?.(false)}
onChangeText={onChangeQuery}
onSubmitEditing={onSubmitQuery}
accessibilityRole="search"
accessibilityLabel={_(msg`Search`)}
accessibilityHint=""
autoCorrect={false}
autoCapitalize="none"
/>
{query ? (
<TouchableOpacity
onPress={onPressCancelSearchInner}
accessibilityRole="button"
accessibilityLabel={_(msg`Clear search query`)}
accessibilityHint=""
hitSlop={HITSLOP_10}>
<FontAwesomeIcon
icon="xmark"
size={16}
style={pal.textLight as FontAwesomeIconStyle}
/>
</TouchableOpacity>
) : undefined}
</View>
)
},
)
const styles = StyleSheet.create({
container: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
borderRadius: 30,
paddingHorizontal: 12,
paddingVertical: 8,
},
icon: {
marginRight: 6,
alignSelf: 'center',
},
input: {
flex: 1,
fontSize: 17,
minWidth: 0, // overflow mitigation for firefox
},
cancelBtn: {
paddingLeft: 10,
},
})
+20 -19
View File
@@ -6,6 +6,12 @@ import {useLingui} from '@lingui/react'
import {useFocusEffect} from '@react-navigation/native'
import debounce from 'lodash.debounce'
import {usePalette} from '#/lib/hooks/usePalette'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {ComposeIcon2} from '#/lib/icons'
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
import {cleanError} from '#/lib/strings/errors'
import {s} from '#/lib/styles'
import {isNative, isWeb} from '#/platform/detection'
import {
SavedFeedItem,
@@ -16,25 +22,19 @@ import {
import {useSession} from '#/state/session'
import {useSetMinimalShellMode} from '#/state/shell'
import {useComposerControls} from '#/state/shell/composer'
import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {ComposeIcon2} from 'lib/icons'
import {CommonNavigatorParams, NativeStackScreenProps} from 'lib/routes/types'
import {cleanError} from 'lib/strings/errors'
import {s} from 'lib/styles'
import {ErrorMessage} from 'view/com/util/error/ErrorMessage'
import {FAB} from 'view/com/util/fab/FAB'
import {SearchInput} from 'view/com/util/forms/SearchInput'
import {TextLink} from 'view/com/util/Link'
import {List} from 'view/com/util/List'
import {FeedFeedLoadingPlaceholder} from 'view/com/util/LoadingPlaceholder'
import {Text} from 'view/com/util/text/Text'
import {ViewHeader} from 'view/com/util/ViewHeader'
import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
import {FAB} from '#/view/com/util/fab/FAB'
import {TextLink} from '#/view/com/util/Link'
import {List} from '#/view/com/util/List'
import {FeedFeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
import {Text} from '#/view/com/util/text/Text'
import {ViewHeader} from '#/view/com/util/ViewHeader'
import {NoFollowingFeed} from '#/screens/Feeds/NoFollowingFeed'
import {NoSavedFeedsOfAnyType} from '#/screens/Feeds/NoSavedFeedsOfAnyType'
import {atoms as a, useTheme} from '#/alf'
import {Divider} from '#/components/Divider'
import * as FeedCard from '#/components/FeedCard'
import {SearchInput} from '#/components/forms/SearchInput'
import {IconCircle} from '#/components/IconCircle'
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron'
import {FilterTimeline_Stroke2_Corner0_Rounded as FilterTimeline} from '#/components/icons/FilterTimeline'
@@ -481,11 +481,12 @@ export function FeedsScreen(_props: Props) {
<FeedsAboutHeader />
<View style={{paddingHorizontal: 12, paddingBottom: 4}}>
<SearchInput
query={query}
onChangeQuery={onChangeQuery}
onPressCancelSearch={onPressCancelSearch}
onSubmitQuery={onSubmitQuery}
setIsInputFocused={onChangeSearchFocus}
value={query}
onChangeText={onChangeQuery}
onClearText={onPressCancelSearch}
onSubmitEditing={onSubmitQuery}
onFocus={() => onChangeSearchFocus(true)}
onBlur={() => onChangeSearchFocus(false)}
/>
</View>
</>
+11 -84
View File
@@ -62,12 +62,10 @@ import {makeSearchQuery, parseSearchQuery} from '#/screens/Search/utils'
import {atoms as a, useBreakpoints, useTheme as useThemeNew, web} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as FeedCard from '#/components/FeedCard'
import * as TextField from '#/components/forms/TextField'
import {SearchInput} from '#/components/forms/SearchInput'
import {ChevronBottom_Stroke2_Corner0_Rounded as ChevronDown} from '#/components/icons/Chevron'
import {MagnifyingGlass2_Stroke2_Corner0_Rounded as MagnifyingGlass} from '#/components/icons/MagnifyingGlass2'
import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu'
import {SettingsGear2_Stroke2_Corner0_Rounded as Gear} from '#/components/icons/SettingsGear2'
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
function Loader() {
const pal = usePalette('default')
@@ -864,15 +862,16 @@ export function SearchScreen(
<ButtonIcon icon={Menu} size="lg" />
</Button>
)}
<SearchInputBox
textInput={textInput}
searchText={searchText}
showAutocomplete={showAutocomplete}
onFocus={onSearchInputFocus}
onChangeText={onChangeText}
onSubmit={onSubmit}
onPressClearQuery={onPressClearQuery}
/>
<View style={[a.flex_1]}>
<SearchInput
ref={textInput}
value={searchText}
onFocus={onSearchInputFocus}
onChangeText={onChangeText}
onClearText={onPressClearQuery}
onSubmitEditing={onSubmit}
/>
</View>
{showFiltersButton && (
<Button
onPress={() => setShowFilters(!showFilters)}
@@ -961,78 +960,6 @@ export function SearchScreen(
)
}
let SearchInputBox = ({
textInput,
searchText,
showAutocomplete,
onFocus,
onChangeText,
onSubmit,
onPressClearQuery,
}: {
textInput: React.RefObject<TextInput>
searchText: string
showAutocomplete: boolean
onFocus: () => void
onChangeText: (text: string) => void
onSubmit: () => void
onPressClearQuery: () => void
}): React.ReactNode => {
const {_} = useLingui()
const t = useThemeNew()
return (
<View style={[a.flex_1]}>
<TextField.Root>
<TextField.Icon icon={MagnifyingGlass} />
<TextField.Input
inputRef={textInput}
label={_(msg`Search`)}
value={searchText}
placeholder={_(msg`Search`)}
returnKeyType="search"
onChangeText={onChangeText}
onSubmitEditing={onSubmit}
onFocus={onFocus}
keyboardAppearance={t.scheme}
selectTextOnFocus={isNative}
autoFocus={false}
accessibilityRole="search"
autoCorrect={false}
autoComplete="off"
autoCapitalize="none"
/>
</TextField.Root>
{showAutocomplete && searchText.length > 0 && (
<View
style={[
a.absolute,
a.z_10,
a.my_auto,
a.inset_0,
a.justify_center,
a.pr_sm,
{left: 'auto'},
]}>
<Button
testID="searchTextInputClearBtn"
onPress={onPressClearQuery}
label={_(msg`Clear search query`)}
hitSlop={HITSLOP_10}
size="tiny"
shape="round"
variant="ghost"
color="secondary">
<ButtonIcon icon={X} size="sm" />
</Button>
</View>
)}
</View>
)
}
SearchInputBox = React.memo(SearchInputBox)
let AutocompleteResults = ({
isAutocompleteFetching,
autocompleteData,
+5 -5
View File
@@ -25,11 +25,11 @@ import {s} from '#/lib/styles'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete'
import {precacheProfile} from '#/state/queries/profile'
import {SearchInput} from '#/view/com/util/forms/SearchInput'
import {Link} from '#/view/com/util/Link'
import {Text} from '#/view/com/util/text/Text'
import {UserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a} from '#/alf'
import {SearchInput} from '#/components/forms/SearchInput'
let SearchLinkCard = ({
label,
@@ -184,10 +184,10 @@ export function DesktopSearch() {
return (
<View style={[styles.container, pal.view]}>
<SearchInput
query={query}
onChangeQuery={onChangeText}
onPressCancelSearch={onPressCancelSearch}
onSubmitQuery={onSubmit}
value={query}
onChangeText={onChangeText}
onClearText={onPressCancelSearch}
onSubmitEditing={onSubmit}
/>
{query !== '' && isActive && moderationOpts && (
<View style={[pal.view, pal.borderDark, styles.resultsContainer]}>