Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c60181f731 | |||
| d8026315bc | |||
| f0b3248422 | |||
| 55915322a4 | |||
| 5c730d7800 | |||
| 137e7e7848 | |||
| d16ca5083f | |||
| a2f1db3027 | |||
| 9eb2f88651 | |||
| f80479744e |
+6
-3
@@ -83,8 +83,9 @@
|
||||
"@emoji-mart/react": "^1.1.1",
|
||||
"@expo/html-elements": "^0.12.5",
|
||||
"@expo/webpack-config": "^19.0.1",
|
||||
"@floating-ui/dom": "^1.6.3",
|
||||
"@floating-ui/react-dom": "^2.0.8",
|
||||
"@floating-ui/dom": "^1.7.5",
|
||||
"@floating-ui/react": "^0.27.18",
|
||||
"@floating-ui/react-dom": "^2.1.7",
|
||||
"@formatjs/intl-displaynames": "^6.8.13",
|
||||
"@formatjs/intl-locale": "^4.2.13",
|
||||
"@formatjs/intl-numberformat": "^8.15.6",
|
||||
@@ -294,7 +295,9 @@
|
||||
"**/expo-device": "7.1.4",
|
||||
"**/multiformats": "9.9.0",
|
||||
"unicode-segmenter": "0.14.5",
|
||||
"@types/estree": "1.0.6"
|
||||
"@types/estree": "1.0.6",
|
||||
"@floating-ui/dom": "1.7.5",
|
||||
"@floating-ui/react-dom": "2.1.7"
|
||||
},
|
||||
"jest": {
|
||||
"preset": "jest-expo/ios",
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import React from 'react'
|
||||
import {type TextInput, View} from 'react-native'
|
||||
import {
|
||||
type NativeSyntheticEvent,
|
||||
type TextInput,
|
||||
type TextInputKeyPressEventData,
|
||||
View,
|
||||
} from 'react-native'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
@@ -12,72 +16,85 @@ import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
|
||||
import {IS_NATIVE} from '#/env'
|
||||
|
||||
type SearchInputProps = Omit<TextField.InputProps, 'label'> & {
|
||||
ref?: React.Ref<TextInput>
|
||||
label?: TextField.InputProps['label']
|
||||
/**
|
||||
* Called when the user presses the (X) button
|
||||
*/
|
||||
onClearText?: () => void
|
||||
/**
|
||||
* Called when the user presses the Escape key
|
||||
*/
|
||||
onEscape?: () => void
|
||||
}
|
||||
|
||||
export const SearchInput = React.forwardRef<TextInput, SearchInputProps>(
|
||||
function SearchInput({value, label, onClearText, ...rest}, ref) {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const showClear = value && value.length > 0
|
||||
export function SearchInput({
|
||||
ref,
|
||||
value,
|
||||
label,
|
||||
onClearText,
|
||||
onEscape,
|
||||
...rest
|
||||
}: SearchInputProps) {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const showClear = value && value.length > 0
|
||||
|
||||
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={IS_NATIVE}
|
||||
autoFocus={false}
|
||||
accessibilityRole="search"
|
||||
autoCorrect={false}
|
||||
autoComplete="off"
|
||||
autoCapitalize="none"
|
||||
style={[
|
||||
showClear
|
||||
? {
|
||||
paddingRight: 24,
|
||||
}
|
||||
: {},
|
||||
]}
|
||||
{...rest}
|
||||
/>
|
||||
</TextField.Root>
|
||||
const onKeyPress = (
|
||||
evt: NativeSyntheticEvent<TextInputKeyPressEventData>,
|
||||
) => {
|
||||
if (evt.nativeEvent.key === 'Escape') {
|
||||
onEscape?.()
|
||||
}
|
||||
}
|
||||
|
||||
{showClear && (
|
||||
<View
|
||||
style={[
|
||||
a.absolute,
|
||||
a.z_20,
|
||||
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>
|
||||
)
|
||||
},
|
||||
)
|
||||
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={IS_NATIVE}
|
||||
autoFocus={false}
|
||||
accessibilityRole="search"
|
||||
autoCorrect={false}
|
||||
autoComplete="off"
|
||||
autoCapitalize="none"
|
||||
onKeyPress={onKeyPress}
|
||||
style={[!!showClear && a.pr_2xl]}
|
||||
{...rest}
|
||||
/>
|
||||
</TextField.Root>
|
||||
|
||||
{showClear && (
|
||||
<View
|
||||
style={[
|
||||
a.absolute,
|
||||
a.z_20,
|
||||
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,6 +1,7 @@
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useDeferredValue,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
@@ -17,8 +18,7 @@ import {useLingui} from '@lingui/react'
|
||||
import {useFocusEffect, useNavigation, useRoute} from '@react-navigation/native'
|
||||
import {useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
import {HITSLOP_20} from '#/lib/constants'
|
||||
import {HITSLOP_10} from '#/lib/constants'
|
||||
import {HITSLOP_10, HITSLOP_20} from '#/lib/constants'
|
||||
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
|
||||
import {MagnifyingGlassIcon} from '#/lib/icons'
|
||||
import {type NavigationProp} from '#/lib/routes/types'
|
||||
@@ -75,9 +75,10 @@ export function SearchScreenShell({
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
// Query terms
|
||||
const [searchText, setSearchText] = useState<string>(queryParam)
|
||||
const [searchText, setSearchText] = useState(queryParam)
|
||||
const deferredSearchText = useDeferredValue(searchText)
|
||||
const {data: autocompleteData, isFetching: isAutocompleteFetching} =
|
||||
useActorAutocompleteQuery(searchText, true)
|
||||
useActorAutocompleteQuery(deferredSearchText, true)
|
||||
|
||||
const [showAutocomplete, setShowAutocomplete] = useState(false)
|
||||
|
||||
@@ -397,11 +398,11 @@ export function SearchScreenShell({
|
||||
display: showAutocomplete && !fixedParams ? 'flex' : 'none',
|
||||
flex: 1,
|
||||
}}>
|
||||
{searchText.length > 0 ? (
|
||||
{deferredSearchText.length > 0 ? (
|
||||
<AutocompleteResults
|
||||
isAutocompleteFetching={isAutocompleteFetching}
|
||||
autocompleteData={autocompleteData}
|
||||
searchText={searchText}
|
||||
searchText={deferredSearchText}
|
||||
onSubmit={onSubmit}
|
||||
onResultPress={onAutocompleteResultPress}
|
||||
onProfileClick={handleProfileClick}
|
||||
|
||||
@@ -5,11 +5,10 @@ import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {SearchLinkCard} from '#/view/shell/desktop/Search'
|
||||
import {SearchProfileCard} from '#/screens/Search/components/SearchProfileCard'
|
||||
import {atoms as a, native} from '#/alf'
|
||||
import {atoms as a} from '#/alf'
|
||||
import * as Layout from '#/components/Layout'
|
||||
import {IS_NATIVE} from '#/env'
|
||||
import {SearchLinkCard} from './SearchLinkCard'
|
||||
|
||||
let AutocompleteResults = ({
|
||||
isAutocompleteFetching,
|
||||
@@ -43,12 +42,8 @@ let AutocompleteResults = ({
|
||||
keyboardDismissMode="on-drag">
|
||||
<SearchLinkCard
|
||||
label={_(msg`Search for "${searchText}"`)}
|
||||
onPress={native(onSubmit)}
|
||||
to={
|
||||
IS_NATIVE
|
||||
? undefined
|
||||
: `/search?q=${encodeURIComponent(searchText)}`
|
||||
}
|
||||
onPress={onSubmit}
|
||||
to={{screen: 'Search', params: {q: searchText}}}
|
||||
style={a.border_b}
|
||||
/>
|
||||
{autocompleteData?.map(item => (
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import {type StyleProp, View, type ViewStyle} from 'react-native'
|
||||
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Button} from '#/components/Button'
|
||||
import {type LinkProps} from '#/components/Link'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
export function SearchLinkCard({
|
||||
label,
|
||||
onPress,
|
||||
style,
|
||||
}: {
|
||||
label: string
|
||||
/**
|
||||
* @platform web
|
||||
*/
|
||||
to: LinkProps['to']
|
||||
/**
|
||||
* @platform native
|
||||
*/
|
||||
onPress: () => void
|
||||
style?: StyleProp<ViewStyle>
|
||||
}) {
|
||||
const t = useTheme()
|
||||
|
||||
return (
|
||||
<Button label={label} onPress={onPress}>
|
||||
{({focused, hovered, pressed}) => (
|
||||
<View
|
||||
style={[
|
||||
a.w_full,
|
||||
t.atoms.border_contrast_low,
|
||||
a.p_lg,
|
||||
(focused || hovered || pressed) && t.atoms.bg_contrast_25,
|
||||
style,
|
||||
]}>
|
||||
<Text style={[a.text_sm, a.leading_snug]}>{label}</Text>
|
||||
</View>
|
||||
)}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import {type StyleProp, View, type ViewStyle} from 'react-native'
|
||||
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Link, type LinkProps} from '#/components/Link'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
export function SearchLinkCard({
|
||||
label,
|
||||
to,
|
||||
style,
|
||||
}: {
|
||||
label: string
|
||||
/**
|
||||
* @platform web
|
||||
*/
|
||||
to: LinkProps['to']
|
||||
/**
|
||||
* @platform native
|
||||
*/
|
||||
onPress: () => void
|
||||
style?: StyleProp<ViewStyle>
|
||||
}) {
|
||||
const t = useTheme()
|
||||
|
||||
return (
|
||||
<Link to={to} label={label}>
|
||||
{({focused, hovered, pressed}) => (
|
||||
<View
|
||||
style={[
|
||||
a.w_full,
|
||||
t.atoms.border_contrast_low,
|
||||
a.py_lg,
|
||||
a.px_md,
|
||||
(focused || hovered || pressed) && t.atoms.bg_contrast_25,
|
||||
style,
|
||||
]}>
|
||||
<Text style={[a.text_sm, a.leading_snug]}>{label}</Text>
|
||||
</View>
|
||||
)}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
+248
-137
@@ -1,167 +1,278 @@
|
||||
import React from 'react'
|
||||
import {useDeferredValue, useRef, useState} from 'react'
|
||||
import {type Role, type TextInput, View} from 'react-native'
|
||||
import {
|
||||
ActivityIndicator,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
type ViewStyle,
|
||||
} from 'react-native'
|
||||
import {msg} from '@lingui/macro'
|
||||
useDismiss,
|
||||
useFloating,
|
||||
useId,
|
||||
useInteractions,
|
||||
useListNavigation,
|
||||
useRole,
|
||||
} from '@floating-ui/react'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {StackActions, useNavigation} from '@react-navigation/native'
|
||||
import {useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
import {usePalette} from '#/lib/hooks/usePalette'
|
||||
import {type NavigationProp} from '#/lib/routes/types'
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete'
|
||||
import {Link} from '#/view/com/util/Link'
|
||||
import {Text} from '#/view/com/util/text/Text'
|
||||
import {SearchProfileCard} from '#/screens/Search/components/SearchProfileCard'
|
||||
import {atoms as a} from '#/alf'
|
||||
import {unstableCacheProfileView} from '#/state/queries/unstable-profile-cache'
|
||||
import {atoms as a, flatten, useTheme} from '#/alf'
|
||||
import {SearchInput} from '#/components/forms/SearchInput'
|
||||
|
||||
let SearchLinkCard = ({
|
||||
label,
|
||||
to,
|
||||
onPress,
|
||||
style,
|
||||
}: {
|
||||
label: string
|
||||
to?: string
|
||||
onPress?: () => void
|
||||
style?: ViewStyle
|
||||
}): React.ReactNode => {
|
||||
const pal = usePalette('default')
|
||||
|
||||
const inner = (
|
||||
<View
|
||||
style={[pal.border, {paddingVertical: 16, paddingHorizontal: 12}, style]}>
|
||||
<Text type="md" style={[pal.text]}>
|
||||
{label}
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
|
||||
if (onPress) {
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={onPress}
|
||||
accessibilityLabel={label}
|
||||
accessibilityHint="">
|
||||
{inner}
|
||||
</TouchableOpacity>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Link href={to} asAnchor anchorNoUnderline>
|
||||
<View
|
||||
style={[
|
||||
pal.border,
|
||||
{paddingVertical: 16, paddingHorizontal: 12},
|
||||
style,
|
||||
]}>
|
||||
<Text type="md" style={[pal.text]}>
|
||||
{label}
|
||||
</Text>
|
||||
</View>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
SearchLinkCard = React.memo(SearchLinkCard)
|
||||
export {SearchLinkCard}
|
||||
import {MagnifyingGlass_Stroke2_Corner0_Rounded as SearchIcon} from '#/components/icons/MagnifyingGlass'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import * as ProfileCard from '#/components/ProfileCard'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
export function DesktopSearch() {
|
||||
const {_} = useLingui()
|
||||
const pal = usePalette('default')
|
||||
const t = useTheme()
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
const [isActive, setIsActive] = React.useState<boolean>(false)
|
||||
const [query, setQuery] = React.useState<string>('')
|
||||
const qc = useQueryClient()
|
||||
|
||||
const searchInputRef = useRef<TextInput>(null)
|
||||
const [open, setOpen] = useState(false)
|
||||
const [query, setQuery] = useState('')
|
||||
const deferredQuery = useDeferredValue(query)
|
||||
const [activeIndex, setActiveIndex] = useState<number | null>(null)
|
||||
const listRef = useRef<Array<HTMLElement | null>>([])
|
||||
|
||||
const {data: autocompleteData, isFetching} = useActorAutocompleteQuery(
|
||||
query,
|
||||
deferredQuery,
|
||||
true,
|
||||
)
|
||||
|
||||
const moderationOpts = useModerationOpts()
|
||||
const profiles = autocompleteData ?? []
|
||||
const hasSearchLink = deferredQuery.length > 0
|
||||
|
||||
const onChangeText = React.useCallback((text: string) => {
|
||||
// Floating UI setup — used for interaction hooks (ARIA combobox pattern),
|
||||
// not for positioning (we keep the existing CSS absolute layout).
|
||||
const {refs, context} = useFloating({
|
||||
open,
|
||||
onOpenChange(nextOpen, _event, reason) {
|
||||
setOpen(nextOpen)
|
||||
if (!nextOpen && reason === 'escape-key') {
|
||||
setQuery('')
|
||||
searchInputRef.current?.blur()
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const role = useRole(context, {role: 'listbox'})
|
||||
const dismiss = useDismiss(context)
|
||||
const listNav = useListNavigation(context, {
|
||||
listRef,
|
||||
activeIndex,
|
||||
onNavigate: setActiveIndex,
|
||||
virtual: true,
|
||||
loop: true,
|
||||
})
|
||||
|
||||
const {getReferenceProps, getFloatingProps, getItemProps} = useInteractions([
|
||||
role,
|
||||
dismiss,
|
||||
listNav,
|
||||
])
|
||||
|
||||
const listboxId = useId()
|
||||
|
||||
const navigateToSearch = () => {
|
||||
if (!deferredQuery.length) return
|
||||
navigation.dispatch(StackActions.push('Search', {q: deferredQuery}))
|
||||
setQuery('')
|
||||
setOpen(false)
|
||||
searchInputRef.current?.blur()
|
||||
}
|
||||
|
||||
const navigateToProfile = (profileIndex: number) => {
|
||||
const profile = profiles[profileIndex]
|
||||
if (!profile) return
|
||||
unstableCacheProfileView(qc, profile)
|
||||
navigation.dispatch(StackActions.push('Profile', {name: profile.did}))
|
||||
setQuery('')
|
||||
setOpen(false)
|
||||
searchInputRef.current?.blur()
|
||||
}
|
||||
|
||||
const selectItem = (index: number) => {
|
||||
if (hasSearchLink && index === 0) {
|
||||
navigateToSearch()
|
||||
} else {
|
||||
navigateToProfile(hasSearchLink ? index - 1 : index)
|
||||
}
|
||||
}
|
||||
|
||||
const onChangeText = (text: string) => {
|
||||
setQuery(text)
|
||||
setIsActive(text.length > 0)
|
||||
}, [])
|
||||
if (!open) setOpen(true)
|
||||
setActiveIndex(text.length > 0 ? 0 : null)
|
||||
}
|
||||
|
||||
const onPressCancelSearch = React.useCallback(() => {
|
||||
const onPressCancelSearch = () => {
|
||||
setQuery('')
|
||||
setIsActive(false)
|
||||
}, [setQuery])
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
const onSubmit = React.useCallback(() => {
|
||||
setIsActive(false)
|
||||
if (!query.length) return
|
||||
navigation.dispatch(StackActions.push('Search', {q: query}))
|
||||
}, [query, navigation])
|
||||
// getReferenceProps produces the merged keyboard + ARIA props.
|
||||
// We must use onKeyDownCapture because RNW's TextInput internally calls
|
||||
// stopPropagation() on all keydown events, preventing normal bubbling.
|
||||
// Capture phase (parent→child) fires before the target's handler.
|
||||
const referenceProps = getReferenceProps({
|
||||
onKeyDown(e: React.KeyboardEvent) {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
if (activeIndex != null) {
|
||||
selectItem(activeIndex)
|
||||
} else {
|
||||
navigateToSearch()
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
const {onKeyDown: refOnKeyDown, ...refAriaProps} = referenceProps
|
||||
|
||||
const onSearchProfileCardPress = React.useCallback(() => {
|
||||
setQuery('')
|
||||
setIsActive(false)
|
||||
}, [])
|
||||
// Extract role/id from floating props for the listbox View
|
||||
const floatingProps = getFloatingProps()
|
||||
|
||||
return (
|
||||
<View style={[styles.container, pal.view]}>
|
||||
<SearchInput
|
||||
value={query}
|
||||
onChangeText={onChangeText}
|
||||
onClearText={onPressCancelSearch}
|
||||
onSubmitEditing={onSubmit}
|
||||
/>
|
||||
{query !== '' && isActive && moderationOpts && (
|
||||
<View
|
||||
style={[
|
||||
pal.view,
|
||||
pal.borderDark,
|
||||
styles.resultsContainer,
|
||||
a.overflow_hidden,
|
||||
]}>
|
||||
{isFetching && !autocompleteData?.length ? (
|
||||
<View style={{padding: 8}}>
|
||||
<ActivityIndicator />
|
||||
</View>
|
||||
) : (
|
||||
<>
|
||||
<SearchLinkCard
|
||||
label={_(msg`Search for "${query}"`)}
|
||||
to={`/search?q=${encodeURIComponent(query)}`}
|
||||
style={
|
||||
(autocompleteData?.length ?? 0) > 0
|
||||
? {borderBottomWidth: 1}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
{autocompleteData?.map(item => (
|
||||
<SearchProfileCard
|
||||
key={item.did}
|
||||
profile={item}
|
||||
moderationOpts={moderationOpts}
|
||||
onPress={onSearchProfileCardPress}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
<View style={[a.w_full, a.z_10]}>
|
||||
{/* Wrapper div receives floating-ui reference + ARIA props.
|
||||
onKeyDownCapture is needed because RNW's TextInput stops keydown
|
||||
propagation — capture phase fires before that happens. */}
|
||||
<div
|
||||
ref={refs.setReference}
|
||||
onKeyDownCapture={
|
||||
refOnKeyDown as React.KeyboardEventHandler<HTMLDivElement>
|
||||
}
|
||||
{...(refAriaProps as React.HTMLAttributes<HTMLDivElement>)}
|
||||
style={{width: '100%'}}>
|
||||
<SearchInput
|
||||
ref={searchInputRef}
|
||||
value={query}
|
||||
onChangeText={onChangeText}
|
||||
onClearText={onPressCancelSearch}
|
||||
onFocus={() => setOpen(true)}
|
||||
onBlur={(e: any) => {
|
||||
const relatedTarget = (e as React.FocusEvent)
|
||||
.relatedTarget as Node | null
|
||||
if (
|
||||
relatedTarget &&
|
||||
refs.floating.current?.contains(relatedTarget)
|
||||
) {
|
||||
return
|
||||
}
|
||||
setOpen(false)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{open && moderationOpts && (
|
||||
<View style={[a.w_full]}>
|
||||
<View
|
||||
ref={refs.setFloating}
|
||||
role={floatingProps.role as Role}
|
||||
id={floatingProps.id as string}
|
||||
style={[
|
||||
t.atoms.bg,
|
||||
t.atoms.border_contrast_low,
|
||||
a.w_full,
|
||||
a.border,
|
||||
a.mt_sm,
|
||||
a.rounded_sm,
|
||||
a.overflow_hidden,
|
||||
a.absolute,
|
||||
a.shadow_lg,
|
||||
a.zoom_fade_in,
|
||||
]}>
|
||||
{deferredQuery.length === 0 ? (
|
||||
<View style={[a.py_xl, a.gap_sm, a.align_center]}>
|
||||
<SearchIcon size="2xl" style={[t.atoms.text_contrast_low]} />
|
||||
<Text
|
||||
style={[a.text_sm, t.atoms.text_contrast_low, a.text_center]}>
|
||||
<Trans>Start typing to search</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
) : (
|
||||
<>
|
||||
{/* Search link option */}
|
||||
<div
|
||||
ref={node => {
|
||||
listRef.current[0] = node
|
||||
}}
|
||||
id={`${listboxId}-option-0`}
|
||||
role="option"
|
||||
aria-selected={activeIndex === 0}
|
||||
style={flatten([
|
||||
a.w_full,
|
||||
a.py_lg,
|
||||
a.px_md,
|
||||
a.pointer,
|
||||
(profiles.length > 0 || isFetching) && a.border_b,
|
||||
t.atoms.border_contrast_low,
|
||||
activeIndex === 0 && t.atoms.bg_contrast_25,
|
||||
])}
|
||||
{...getItemProps({
|
||||
onClick() {
|
||||
navigateToSearch()
|
||||
},
|
||||
})}>
|
||||
<Text style={[a.text_sm, a.leading_snug]}>
|
||||
{_(msg`Search for "${deferredQuery}"`)}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{/* Loading state */}
|
||||
{isFetching && !profiles.length ? (
|
||||
<View style={[a.p_xl, a.align_center]}>
|
||||
<Loader size="md" />
|
||||
</View>
|
||||
) : (
|
||||
profiles.map((profile, i) => {
|
||||
const itemIndex = 1 + i
|
||||
return (
|
||||
<div
|
||||
key={profile.did}
|
||||
ref={node => {
|
||||
listRef.current[itemIndex] = node
|
||||
}}
|
||||
id={`${listboxId}-option-${itemIndex}`}
|
||||
role="option"
|
||||
aria-selected={activeIndex === itemIndex}
|
||||
aria-label={_(msg`View ${profile.handle}'s profile`)}
|
||||
style={flatten([
|
||||
a.flex,
|
||||
a.flex_col,
|
||||
{paddingLeft: 6, paddingRight: 6},
|
||||
a.px_sm,
|
||||
a.pointer,
|
||||
activeIndex === itemIndex && t.atoms.bg_contrast_25,
|
||||
])}
|
||||
{...getItemProps({
|
||||
onClick() {
|
||||
navigateToProfile(i)
|
||||
},
|
||||
})}>
|
||||
<ProfileCard.Outer>
|
||||
<ProfileCard.Header>
|
||||
<ProfileCard.Avatar
|
||||
profile={profile}
|
||||
moderationOpts={moderationOpts}
|
||||
/>
|
||||
<ProfileCard.NameAndHandle
|
||||
profile={profile}
|
||||
moderationOpts={moderationOpts}
|
||||
/>
|
||||
</ProfileCard.Header>
|
||||
</ProfileCard.Outer>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
},
|
||||
resultsContainer: {
|
||||
marginTop: 10,
|
||||
flexDirection: 'column',
|
||||
width: '100%',
|
||||
borderWidth: 1,
|
||||
borderRadius: 6,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -4419,59 +4419,41 @@
|
||||
dependencies:
|
||||
dequal "^2.0.3"
|
||||
|
||||
"@floating-ui/core@^1.0.0":
|
||||
version "1.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.6.0.tgz#fa41b87812a16bf123122bf945946bae3fdf7fc1"
|
||||
integrity sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g==
|
||||
"@floating-ui/core@^1.7.4":
|
||||
version "1.7.4"
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.7.4.tgz#4a006a6e01565c0f87ba222c317b056a2cffd2f4"
|
||||
integrity sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==
|
||||
dependencies:
|
||||
"@floating-ui/utils" "^0.2.1"
|
||||
"@floating-ui/utils" "^0.2.10"
|
||||
|
||||
"@floating-ui/core@^1.4.1":
|
||||
version "1.4.1"
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.4.1.tgz#0d633f4b76052668afb932492ac452f7ebe97f17"
|
||||
integrity sha512-jk3WqquEJRlcyu7997NtR5PibI+y5bi+LS3hPmguVClypenMsCY3CBa3LAQnozRCtCrYWSEtAdiskpamuJRFOQ==
|
||||
"@floating-ui/dom@1.7.5", "@floating-ui/dom@^1.7.5":
|
||||
version "1.7.5"
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.7.5.tgz#60bfc83a4d1275b2a90db76bf42ca2a5f2c231c2"
|
||||
integrity sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg==
|
||||
dependencies:
|
||||
"@floating-ui/utils" "^0.1.1"
|
||||
"@floating-ui/core" "^1.7.4"
|
||||
"@floating-ui/utils" "^0.2.10"
|
||||
|
||||
"@floating-ui/dom@^1.3.0":
|
||||
version "1.5.1"
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.5.1.tgz#88b70defd002fe851f17b4a25efb2d3c04d7a8d7"
|
||||
integrity sha512-KwvVcPSXg6mQygvA1TjbN/gh///36kKtllIF8SUm0qpFj8+rvYrpvlYdL1JoA71SHpDqgSSdGOSoQ0Mp3uY5aw==
|
||||
"@floating-ui/react-dom@2.1.7", "@floating-ui/react-dom@^2.0.0", "@floating-ui/react-dom@^2.1.7":
|
||||
version "2.1.7"
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.1.7.tgz#529475cc16ee4976ba3387968117e773d9aa703e"
|
||||
integrity sha512-0tLRojf/1Go2JgEVm+3Frg9A3IW8bJgKgdO0BN5RkF//ufuz2joZM63Npau2ff3J6lUVYgDSNzNkR+aH3IVfjg==
|
||||
dependencies:
|
||||
"@floating-ui/core" "^1.4.1"
|
||||
"@floating-ui/utils" "^0.1.1"
|
||||
"@floating-ui/dom" "^1.7.5"
|
||||
|
||||
"@floating-ui/dom@^1.6.1", "@floating-ui/dom@^1.6.3":
|
||||
version "1.6.3"
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.6.3.tgz#954e46c1dd3ad48e49db9ada7218b0985cee75ef"
|
||||
integrity sha512-RnDthu3mzPlQ31Ss/BTwQ1zjzIhr3lk1gZB1OC56h/1vEtaXkESrOqL5fQVMfXpwGtRwX+YsZBdyHtJMQnkArw==
|
||||
"@floating-ui/react@^0.27.18":
|
||||
version "0.27.18"
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/react/-/react-0.27.18.tgz#a664a45a867ed5e2999a858b5236a53cf60cf412"
|
||||
integrity sha512-xJWJxvmy3a05j643gQt+pRbht5XnTlGpsEsAPnMi5F5YTOEEJymA90uZKBD8OvIv5XvZ1qi4GcccSlqT3Bq44Q==
|
||||
dependencies:
|
||||
"@floating-ui/core" "^1.0.0"
|
||||
"@floating-ui/utils" "^0.2.0"
|
||||
"@floating-ui/react-dom" "^2.1.7"
|
||||
"@floating-ui/utils" "^0.2.10"
|
||||
tabbable "^6.0.0"
|
||||
|
||||
"@floating-ui/react-dom@^2.0.0":
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.0.1.tgz#7972a4fc488a8c746cded3cfe603b6057c308a91"
|
||||
integrity sha512-rZtAmSht4Lry6gdhAJDrCp/6rKN7++JnL1/Anbr/DdeyYXQPxvg/ivrbYvJulbRf4vL8b212suwMM2lxbv+RQA==
|
||||
dependencies:
|
||||
"@floating-ui/dom" "^1.3.0"
|
||||
|
||||
"@floating-ui/react-dom@^2.0.8":
|
||||
version "2.0.8"
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.0.8.tgz#afc24f9756d1b433e1fe0d047c24bd4d9cefaa5d"
|
||||
integrity sha512-HOdqOt3R3OGeTKidaLvJKcgg75S6tibQ3Tif4eyd91QnIJWr0NLvoXFpJA/j8HqkFSL68GDca9AuyWEHlhyClw==
|
||||
dependencies:
|
||||
"@floating-ui/dom" "^1.6.1"
|
||||
|
||||
"@floating-ui/utils@^0.1.1":
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.1.1.tgz#1a5b1959a528e374e8037c4396c3e825d6cf4a83"
|
||||
integrity sha512-m0G6wlnhm/AX0H12IOWtK8gASEMffnX08RtKkCgTdHb9JpHKGloI7icFfLg9ZmQeavcvR0PKmzxClyuFPSjKWw==
|
||||
|
||||
"@floating-ui/utils@^0.2.0", "@floating-ui/utils@^0.2.1":
|
||||
version "0.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.1.tgz#16308cea045f0fc777b6ff20a9f25474dd8293d2"
|
||||
integrity sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==
|
||||
"@floating-ui/utils@^0.2.10":
|
||||
version "0.2.10"
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.10.tgz#a2a1e3812d14525f725d011a73eceb41fef5bc1c"
|
||||
integrity sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==
|
||||
|
||||
"@formatjs/ecma402-abstract@2.3.6":
|
||||
version "2.3.6"
|
||||
@@ -19051,6 +19033,11 @@ symbol-tree@^3.2.4:
|
||||
resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
|
||||
integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==
|
||||
|
||||
tabbable@^6.0.0:
|
||||
version "6.4.0"
|
||||
resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-6.4.0.tgz#36eb7a06d80b3924a22095daf45740dea3bf5581"
|
||||
integrity sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==
|
||||
|
||||
tapable@^2.0.0, tapable@^2.1.1, tapable@^2.2.0:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0"
|
||||
|
||||
Reference in New Issue
Block a user