Bluesky app fork with some witchin' additions 💫

Add language filtering UI to search (#5459)

* Use new TextField for search bar

* Add lang dropdown

* Dialog

* Revert "Dialog"

This reverts commit 257573cd9c2a70d29df4ef5bdd503eea4ae411fe.

* Extract util, test, cleanup

* Fix formatting

* Pass through other params

* Fix sticky header

* Fix stale data, hide/show

* Improve query parsing

* Replace memo

* Couple tweaks

* Revert cancel change

* Remove unused placeholder

authored by

Eric Bailey and committed by
GitHub
b1ca2503 6bc001a3

+427 -149
+3
src/alf/themes.ts
··· 305 305 } as const 306 306 307 307 const light: Theme = { 308 + scheme: 'light', 308 309 name: 'light', 309 310 palette: lightPalette, 310 311 atoms: { ··· 390 391 } 391 392 392 393 const dark: Theme = { 394 + scheme: 'dark', 393 395 name: 'dark', 394 396 palette: darkPalette, 395 397 atoms: { ··· 479 481 480 482 const dim: Theme = { 481 483 ...dark, 484 + scheme: 'dark', 482 485 name: 'dim', 483 486 palette: dimPalette, 484 487 atoms: {
+1
src/alf/types.ts
··· 156 156 } 157 157 } 158 158 export type Theme = { 159 + scheme: 'light' | 'dark' // for library support 159 160 name: ThemeName 160 161 palette: Palette 161 162 atoms: ThemedAtoms
+16 -7
src/components/forms/TextField.tsx
··· 135 135 placeholder, 136 136 value, 137 137 onChangeText, 138 + onFocus, 139 + onBlur, 138 140 isInvalid, 139 141 inputRef, 140 142 style, ··· 173 175 ref={refs} 174 176 value={value} 175 177 onChangeText={onChangeText} 176 - onFocus={ctx.onFocus} 177 - onBlur={ctx.onBlur} 178 + onFocus={e => { 179 + ctx.onFocus() 180 + onFocus?.(e) 181 + }} 182 + onBlur={e => { 183 + ctx.onBlur() 184 + onBlur?.(e) 185 + }} 178 186 placeholder={placeholder || label} 179 187 placeholderTextColor={t.palette.contrast_500} 180 188 keyboardAppearance={t.name === 'light' ? 'light' : 'dark'} ··· 188 196 a.px_xs, 189 197 { 190 198 // paddingVertical doesn't work w/multiline - esb 191 - paddingTop: 14, 192 - paddingBottom: 14, 199 + paddingTop: 12, 200 + paddingBottom: 13, 193 201 lineHeight: a.text_md.fontSize * 1.1875, 194 202 textAlignVertical: rest.multiline ? 'top' : undefined, 195 203 minHeight: rest.multiline ? 80 : undefined, ··· 197 205 }, 198 206 // fix for autofill styles covering border 199 207 web({ 200 - paddingTop: 12, 201 - paddingBottom: 12, 208 + paddingTop: 10, 209 + paddingBottom: 11, 202 210 marginTop: 2, 203 211 marginBottom: 2, 204 212 }), 205 213 android({ 206 - paddingBottom: 16, 214 + paddingTop: 8, 215 + paddingBottom: 8, 207 216 }), 208 217 style, 209 218 ]}
+43
src/screens/Search/__tests__/utils.test.ts
··· 1 + import {describe, expect, it} from '@jest/globals' 2 + 3 + import {parseSearchQuery} from '#/screens/Search/utils' 4 + 5 + describe(`parseSearchQuery`, () => { 6 + const tests = [ 7 + { 8 + input: `bluesky`, 9 + output: {query: `bluesky`, params: {}}, 10 + }, 11 + { 12 + input: `bluesky from:esb.lol`, 13 + output: {query: `bluesky`, params: {from: `esb.lol`}}, 14 + }, 15 + { 16 + input: `bluesky "from:esb.lol"`, 17 + output: {query: `bluesky "from:esb.lol"`, params: {}}, 18 + }, 19 + { 20 + input: `bluesky mentions:@esb.lol`, 21 + output: {query: `bluesky`, params: {mentions: `@esb.lol`}}, 22 + }, 23 + { 24 + input: `bluesky since:2021-01-01:00:00:00`, 25 + output: {query: `bluesky`, params: {since: `2021-01-01:00:00:00`}}, 26 + }, 27 + { 28 + input: `bluesky lang:"en"`, 29 + output: {query: `bluesky`, params: {lang: `en`}}, 30 + }, 31 + { 32 + input: `bluesky "literal" lang:en "from:invalid"`, 33 + output: {query: `bluesky "literal" "from:invalid"`, params: {lang: `en`}}, 34 + }, 35 + ] 36 + 37 + it.each(tests)( 38 + `$input -> $output.query $output.params`, 39 + ({input, output}) => { 40 + expect(parseSearchQuery(input)).toEqual(output) 41 + }, 42 + ) 43 + })
+43
src/screens/Search/utils.ts
··· 1 + export type Params = Record<string, string> 2 + 3 + export function parseSearchQuery(rawQuery: string) { 4 + let base = rawQuery 5 + const rawLiterals = rawQuery.match(/[^:\w\d]".+?"/gi) || [] 6 + 7 + // remove literals from base 8 + for (const literal of rawLiterals) { 9 + base = base.replace(literal.trim(), '') 10 + } 11 + 12 + // find remaining params in base 13 + const rawParams = base.match(/[a-z]+:[a-z-\.@\d:"]+/gi) || [] 14 + 15 + for (const param of rawParams) { 16 + base = base.replace(param, '') 17 + } 18 + 19 + base = base.trim() 20 + 21 + const params = rawParams.reduce((params, param) => { 22 + const [name, ...value] = param.split(/:/) 23 + params[name] = value.join(':').replace(/"/g, '') // dates can contain additional colons 24 + return params 25 + }, {} as Params) 26 + const literals = rawLiterals.map(l => String(l).trim()) 27 + 28 + return { 29 + query: [base, literals.join(' ')].filter(Boolean).join(' '), 30 + params, 31 + } 32 + } 33 + 34 + export function makeSearchQuery(query: string, params: Params) { 35 + return [ 36 + query, 37 + Object.entries(params) 38 + .map(([name, value]) => `${name}:${value}`) 39 + .join(' '), 40 + ] 41 + .filter(Boolean) 42 + .join(' ') 43 + }
+320 -141
src/view/screens/Search/Search.tsx
··· 11 11 View, 12 12 } from 'react-native' 13 13 import {ScrollView as RNGHScrollView} from 'react-native-gesture-handler' 14 + import RNPickerSelect from 'react-native-picker-select' 14 15 import {AppBskyActorDefs, AppBskyFeedDefs, moderateProfile} from '@atproto/api' 15 16 import { 16 17 FontAwesomeIcon, ··· 21 22 import AsyncStorage from '@react-native-async-storage/async-storage' 22 23 import {useFocusEffect, useNavigation} from '@react-navigation/native' 23 24 25 + import {LANGUAGES} from '#/lib/../locale/languages' 24 26 import {useAnalytics} from '#/lib/analytics/analytics' 25 27 import {createHitslop} from '#/lib/constants' 26 28 import {HITSLOP_10} from '#/lib/constants' ··· 35 37 SearchTabNavigatorParams, 36 38 } from '#/lib/routes/types' 37 39 import {augmentSearchQuery} from '#/lib/strings/helpers' 38 - import {useTheme} from '#/lib/ThemeContext' 39 40 import {logger} from '#/logger' 40 41 import {isNative, isWeb} from '#/platform/detection' 41 42 import {listenSoftReset} from '#/state/events' 43 + import {useLanguagePrefs} from '#/state/preferences/languages' 42 44 import {useModerationOpts} from '#/state/preferences/moderation-opts' 43 45 import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete' 44 46 import {useActorSearch} from '#/state/queries/actor-search' ··· 57 59 import {CenteredView, ScrollView} from '#/view/com/util/Views' 58 60 import {Explore} from '#/view/screens/Search/Explore' 59 61 import {SearchLinkCard, SearchProfileCard} from '#/view/shell/desktop/Search' 60 - import {atoms as a, useTheme as useThemeNew} from '#/alf' 62 + import {makeSearchQuery, parseSearchQuery} from '#/screens/Search/utils' 63 + import {atoms as a, useBreakpoints, useTheme as useThemeNew, web} from '#/alf' 64 + import {Button, ButtonIcon, ButtonText} from '#/components/Button' 61 65 import * as FeedCard from '#/components/FeedCard' 66 + import * as TextField from '#/components/forms/TextField' 67 + import {ChevronBottom_Stroke2_Corner0_Rounded as ChevronDown} from '#/components/icons/Chevron' 68 + import {MagnifyingGlass2_Stroke2_Corner0_Rounded as MagnifyingGlass} from '#/components/icons/MagnifyingGlass2' 62 69 import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu' 70 + import {SettingsGear2_Stroke2_Corner0_Rounded as Gear} from '#/components/icons/SettingsGear2' 71 + import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' 63 72 64 73 function Loader() { 65 74 const pal = usePalette('default') ··· 251 260 const {_} = useLingui() 252 261 253 262 const {data: results, isFetched} = useActorSearch({ 254 - query: query, 263 + query, 255 264 enabled: active, 256 265 }) 257 266 ··· 324 333 } 325 334 SearchScreenFeedsResults = React.memo(SearchScreenFeedsResults) 326 335 327 - let SearchScreenInner = ({query}: {query?: string}): React.ReactNode => { 336 + function SearchLanguageDropdown({ 337 + value, 338 + onChange, 339 + }: { 340 + value: string 341 + onChange(value: string): void 342 + }) { 343 + const t = useThemeNew() 344 + const {contentLanguages} = useLanguagePrefs() 345 + 346 + const items = React.useMemo(() => { 347 + return LANGUAGES.filter(l => Boolean(l.code2)) 348 + .map(l => ({ 349 + label: l.name, 350 + inputLabel: l.name, 351 + value: l.code2, 352 + key: l.code2 + l.code3, 353 + })) 354 + .sort(a => (contentLanguages.includes(a.value) ? -1 : 1)) 355 + }, [contentLanguages]) 356 + 357 + const style = { 358 + backgroundColor: t.atoms.bg_contrast_25.backgroundColor, 359 + color: t.atoms.text.color, 360 + fontSize: a.text_xs.fontSize, 361 + fontFamily: 'inherit', 362 + fontWeight: a.font_bold.fontWeight, 363 + paddingHorizontal: 14, 364 + paddingRight: 32, 365 + paddingVertical: 8, 366 + borderRadius: a.rounded_full.borderRadius, 367 + borderWidth: a.border.borderWidth, 368 + borderColor: t.atoms.border_contrast_low.borderColor, 369 + } 370 + 371 + return ( 372 + <RNPickerSelect 373 + value={value} 374 + onValueChange={onChange} 375 + items={items} 376 + Icon={() => ( 377 + <ChevronDown fill={t.atoms.text_contrast_low.color} size="sm" /> 378 + )} 379 + useNativeAndroidPickerStyle={false} 380 + style={{ 381 + iconContainer: { 382 + pointerEvents: 'none', 383 + right: a.px_sm.paddingRight, 384 + top: 0, 385 + bottom: 0, 386 + display: 'flex', 387 + justifyContent: 'center', 388 + }, 389 + inputAndroid: { 390 + ...style, 391 + paddingVertical: 2, 392 + }, 393 + inputIOS: { 394 + ...style, 395 + }, 396 + inputWeb: web({ 397 + ...style, 398 + cursor: 'pointer', 399 + // @ts-ignore web only 400 + '-moz-appearance': 'none', 401 + '-webkit-appearance': 'none', 402 + appearance: 'none', 403 + outline: 0, 404 + borderWidth: 0, 405 + overflow: 'hidden', 406 + whiteSpace: 'nowrap', 407 + textOverflow: 'ellipsis', 408 + }), 409 + }} 410 + /> 411 + ) 412 + } 413 + 414 + function useQueryManager({initialQuery}: {initialQuery: string}) { 415 + const {contentLanguages} = useLanguagePrefs() 416 + const {query, params: initialParams} = React.useMemo(() => { 417 + return parseSearchQuery(initialQuery || '') 418 + }, [initialQuery]) 419 + const prevInitialQuery = React.useRef(initialQuery) 420 + const [lang, setLang] = React.useState( 421 + initialParams.lang || contentLanguages[0], 422 + ) 423 + 424 + if (initialQuery !== prevInitialQuery.current) { 425 + // handle new queryParam change (from manual search entry) 426 + prevInitialQuery.current = initialQuery 427 + setLang(initialParams.lang || contentLanguages[0]) 428 + } 429 + 430 + const params = React.useMemo( 431 + () => ({ 432 + // default stuff 433 + ...initialParams, 434 + // managed stuff 435 + lang, 436 + }), 437 + [lang, initialParams], 438 + ) 439 + const handlers = React.useMemo( 440 + () => ({ 441 + setLang, 442 + }), 443 + [setLang], 444 + ) 445 + 446 + return React.useMemo(() => { 447 + return { 448 + query, 449 + queryWithParams: makeSearchQuery(query, params), 450 + params: { 451 + ...params, 452 + ...handlers, 453 + }, 454 + } 455 + }, [query, params, handlers]) 456 + } 457 + 458 + let SearchScreenInner = ({ 459 + query, 460 + queryWithParams, 461 + headerHeight, 462 + }: { 463 + query: string 464 + queryWithParams: string 465 + headerHeight: number 466 + }): React.ReactNode => { 328 467 const pal = usePalette('default') 329 468 const setMinimalShellMode = useSetMinimalShellMode() 330 469 const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled() ··· 349 488 title: _(msg`Top`), 350 489 component: ( 351 490 <SearchScreenPostResults 352 - query={query} 491 + query={queryWithParams} 353 492 sort="top" 354 493 active={activeTab === 0} 355 494 /> ··· 359 498 title: _(msg`Latest`), 360 499 component: ( 361 500 <SearchScreenPostResults 362 - query={query} 501 + query={queryWithParams} 363 502 sort="latest" 364 503 active={activeTab === 1} 365 504 /> ··· 378 517 ), 379 518 }, 380 519 ] 381 - }, [_, query, activeTab]) 520 + }, [_, query, queryWithParams, activeTab]) 382 521 383 522 return query ? ( 384 523 <Pager ··· 386 525 renderTabBar={props => ( 387 526 <CenteredView 388 527 sideBorders 389 - style={[pal.border, pal.view, styles.tabBarContainer]}> 528 + style={[ 529 + pal.border, 530 + pal.view, 531 + web({ 532 + position: isWeb ? 'sticky' : '', 533 + zIndex: 1, 534 + }), 535 + {top: isWeb ? headerHeight : undefined}, 536 + ]}> 390 537 <TabBar items={sections.map(section => section.title)} {...props} /> 391 538 </CenteredView> 392 539 )} ··· 448 595 export function SearchScreen( 449 596 props: NativeStackScreenProps<SearchTabNavigatorParams, 'Search'>, 450 597 ) { 598 + const t = useThemeNew() 599 + const {gtMobile} = useBreakpoints() 451 600 const navigation = useNavigation<NavigationProp>() 452 601 const textInput = React.useRef<TextInput>(null) 453 602 const {_} = useLingui() 454 - const pal = usePalette('default') 455 603 const {track} = useAnalytics() 456 604 const setDrawerOpen = useSetDrawerOpen() 457 605 const setMinimalShellMode = useSetMinimalShellMode() 458 - const {isTabletOrDesktop, isTabletOrMobile} = useWebMediaQueries() 459 606 460 607 // Query terms 461 608 const queryParam = props.route?.params?.q ?? '' ··· 469 616 AppBskyActorDefs.ProfileViewBasic[] 470 617 >([]) 471 618 619 + const {params, query, queryWithParams} = useQueryManager({ 620 + initialQuery: queryParam, 621 + }) 622 + const showFiltersButton = Boolean(query && !showAutocomplete) 623 + const [showFilters, setShowFilters] = React.useState(false) 624 + /* 625 + * Arbitrary sizing, so guess and check, used for sticky header alignment and 626 + * sizing. 627 + */ 628 + const headerHeight = 56 + (showFilters ? 40 : 0) 629 + 472 630 useFocusEffect( 473 631 useNonReactiveCallback(() => { 474 632 if (isWeb) { ··· 506 664 setSearchText('') 507 665 textInput.current?.focus() 508 666 }, []) 509 - 510 - const onPressCancelSearch = React.useCallback(() => { 511 - scrollToTopWeb() 512 - textInput.current?.blur() 513 - setShowAutocomplete(false) 514 - setSearchText(queryParam) 515 - }, [queryParam]) 516 667 517 668 const onChangeText = React.useCallback(async (text: string) => { 518 669 scrollToTopWeb() ··· 586 737 [updateSearchHistory, navigation], 587 738 ) 588 739 740 + const onPressCancelSearch = React.useCallback(() => { 741 + scrollToTopWeb() 742 + textInput.current?.blur() 743 + setShowAutocomplete(false) 744 + setSearchText(queryParam) 745 + }, [setShowAutocomplete, setSearchText, queryParam]) 746 + 589 747 const onSubmit = React.useCallback(() => { 590 748 navigateToItem(searchText) 591 749 }, [navigateToItem, searchText]) ··· 624 782 setSearchText('') 625 783 navigation.setParams({q: ''}) 626 784 } 785 + setShowFilters(false) 627 786 }, [navigation]) 628 787 629 788 useFocusEffect( ··· 663 822 [selectedProfiles], 664 823 ) 665 824 825 + const onSearchInputFocus = React.useCallback(() => { 826 + if (isWeb) { 827 + // Prevent a jump on iPad by ensuring that 828 + // the initial focused render has no result list. 829 + requestAnimationFrame(() => { 830 + setShowAutocomplete(true) 831 + }) 832 + } else { 833 + setShowAutocomplete(true) 834 + } 835 + setShowFilters(false) 836 + }, [setShowAutocomplete]) 837 + 666 838 return ( 667 839 <View style={isWeb ? null : {flex: 1}}> 668 840 <CenteredView 669 841 style={[ 670 - styles.header, 671 - pal.border, 672 - pal.view, 673 - isTabletOrDesktop && {paddingTop: 10}, 842 + a.p_md, 843 + a.pb_0, 844 + a.gap_sm, 845 + t.atoms.bg, 846 + web({ 847 + height: headerHeight, 848 + position: 'sticky', 849 + top: 0, 850 + zIndex: 1, 851 + }), 674 852 ]} 675 - sideBorders={isTabletOrDesktop}> 676 - {isTabletOrMobile && ( 677 - <Pressable 678 - testID="viewHeaderBackOrMenuBtn" 679 - onPress={onPressMenu} 680 - hitSlop={HITSLOP_10} 681 - style={styles.headerMenuBtn} 682 - accessibilityRole="button" 683 - accessibilityLabel={_(msg`Menu`)} 684 - accessibilityHint={_(msg`Access navigation links and settings`)}> 685 - <Menu size="lg" fill={pal.colors.textLight} /> 686 - </Pressable> 687 - )} 688 - <SearchInputBox 689 - textInput={textInput} 690 - searchText={searchText} 691 - showAutocomplete={showAutocomplete} 692 - setShowAutocomplete={setShowAutocomplete} 693 - onChangeText={onChangeText} 694 - onSubmit={onSubmit} 695 - onPressClearQuery={onPressClearQuery} 696 - /> 697 - {showAutocomplete && ( 698 - <View style={[styles.headerCancelBtn]}> 699 - <Pressable 853 + sideBorders={gtMobile}> 854 + <View style={[a.flex_row, a.gap_sm]}> 855 + {!gtMobile && ( 856 + <Button 857 + testID="viewHeaderBackOrMenuBtn" 858 + onPress={onPressMenu} 859 + hitSlop={HITSLOP_10} 860 + label={_(msg`Menu`)} 861 + accessibilityHint={_(msg`Access navigation links and settings`)} 862 + size="large" 863 + variant="solid" 864 + color="secondary" 865 + shape="square"> 866 + <ButtonIcon icon={Menu} size="lg" /> 867 + </Button> 868 + )} 869 + <SearchInputBox 870 + textInput={textInput} 871 + searchText={searchText} 872 + showAutocomplete={showAutocomplete} 873 + onFocus={onSearchInputFocus} 874 + onChangeText={onChangeText} 875 + onSubmit={onSubmit} 876 + onPressClearQuery={onPressClearQuery} 877 + /> 878 + {showFiltersButton && ( 879 + <Button 880 + onPress={() => setShowFilters(!showFilters)} 881 + hitSlop={HITSLOP_10} 882 + label={_(msg`Show advanced filters`)} 883 + size="large" 884 + variant="solid" 885 + color="secondary" 886 + shape="square"> 887 + <Gear 888 + size="md" 889 + fill={ 890 + showFilters 891 + ? t.palette.primary_500 892 + : t.atoms.text_contrast_low.color 893 + } 894 + /> 895 + </Button> 896 + )} 897 + {showAutocomplete && ( 898 + <Button 899 + label={_(msg`Cancel search`)} 900 + size="large" 901 + variant="ghost" 902 + color="secondary" 903 + style={[a.px_sm]} 700 904 onPress={onPressCancelSearch} 701 - accessibilityRole="button" 702 905 hitSlop={HITSLOP_10}> 703 - <Text style={pal.text}> 906 + <ButtonText> 704 907 <Trans>Cancel</Trans> 705 - </Text> 706 - </Pressable> 908 + </ButtonText> 909 + </Button> 910 + )} 911 + </View> 912 + 913 + {showFilters && ( 914 + <View 915 + style={[a.flex_row, a.align_center, a.justify_between, a.gap_sm]}> 916 + <View style={[{width: 140}]}> 917 + <SearchLanguageDropdown 918 + value={params.lang} 919 + onChange={params.setLang} 920 + /> 921 + </View> 707 922 </View> 708 923 )} 709 924 </CenteredView> 925 + 710 926 <View 711 927 style={{ 712 928 display: showAutocomplete ? 'flex' : 'none', ··· 737 953 display: showAutocomplete ? 'none' : 'flex', 738 954 flex: 1, 739 955 }}> 740 - <SearchScreenInner query={queryParam} /> 956 + <SearchScreenInner 957 + query={query} 958 + queryWithParams={queryWithParams} 959 + headerHeight={headerHeight} 960 + /> 741 961 </View> 742 962 </View> 743 963 ) ··· 747 967 textInput, 748 968 searchText, 749 969 showAutocomplete, 750 - setShowAutocomplete, 970 + onFocus, 751 971 onChangeText, 752 972 onSubmit, 753 973 onPressClearQuery, ··· 755 975 textInput: React.RefObject<TextInput> 756 976 searchText: string 757 977 showAutocomplete: boolean 758 - setShowAutocomplete: (show: boolean) => void 978 + onFocus: () => void 759 979 onChangeText: (text: string) => void 760 980 onSubmit: () => void 761 981 onPressClearQuery: () => void 762 982 }): React.ReactNode => { 763 - const pal = usePalette('default') 764 983 const {_} = useLingui() 765 - const theme = useTheme() 984 + const t = useThemeNew() 985 + 766 986 return ( 767 - <Pressable 768 - // This only exists only for extra hitslop so don't expose it to the a11y tree. 769 - accessible={false} 770 - focusable={false} 771 - // @ts-ignore web-only 772 - tabIndex={-1} 773 - style={[ 774 - {backgroundColor: pal.colors.backgroundLight}, 775 - styles.headerSearchContainer, 776 - // @ts-expect-error web only 777 - isWeb && { 778 - cursor: 'default', 779 - }, 780 - ]} 781 - onPress={() => { 782 - textInput.current?.focus() 783 - }}> 784 - <MagnifyingGlassIcon 785 - style={[pal.icon, styles.headerSearchIcon]} 786 - size={20} 787 - /> 788 - <TextInput 789 - testID="searchTextInput" 790 - ref={textInput} 791 - placeholder={_(msg`Search`)} 792 - placeholderTextColor={pal.colors.textLight} 793 - returnKeyType="search" 794 - value={searchText} 795 - style={[pal.text, styles.headerSearchInput]} 796 - keyboardAppearance={theme.colorScheme} 797 - selectTextOnFocus={isNative} 798 - onFocus={() => { 799 - if (isWeb) { 800 - // Prevent a jump on iPad by ensuring that 801 - // the initial focused render has no result list. 802 - requestAnimationFrame(() => { 803 - setShowAutocomplete(true) 804 - }) 805 - } else { 806 - setShowAutocomplete(true) 807 - } 808 - }} 809 - onChangeText={onChangeText} 810 - onSubmitEditing={onSubmit} 811 - autoFocus={false} 812 - accessibilityRole="search" 813 - accessibilityLabel={_(msg`Search`)} 814 - accessibilityHint="" 815 - autoCorrect={false} 816 - autoComplete="off" 817 - autoCapitalize="none" 818 - /> 987 + <View style={[a.flex_1, a.relative]}> 988 + <TextField.Root> 989 + <TextField.Icon icon={MagnifyingGlass} /> 990 + <TextField.Input 991 + inputRef={textInput} 992 + label={_(msg`Search`)} 993 + value={searchText} 994 + placeholder={_(msg`Search`)} 995 + returnKeyType="search" 996 + onChangeText={onChangeText} 997 + onSubmitEditing={onSubmit} 998 + onFocus={onFocus} 999 + keyboardAppearance={t.scheme} 1000 + selectTextOnFocus={isNative} 1001 + autoFocus={false} 1002 + accessibilityRole="search" 1003 + autoCorrect={false} 1004 + autoComplete="off" 1005 + autoCapitalize="none" 1006 + /> 1007 + </TextField.Root> 1008 + 819 1009 {showAutocomplete && searchText.length > 0 && ( 820 - <Pressable 821 - testID="searchTextInputClearBtn" 822 - onPress={onPressClearQuery} 823 - accessibilityRole="button" 824 - accessibilityLabel={_(msg`Clear search query`)} 825 - accessibilityHint="" 826 - hitSlop={HITSLOP_10}> 827 - <FontAwesomeIcon 828 - icon="xmark" 829 - size={16} 830 - style={pal.textLight as FontAwesomeIconStyle} 831 - /> 832 - </Pressable> 1010 + <View 1011 + style={[ 1012 + a.absolute, 1013 + a.z_10, 1014 + a.my_auto, 1015 + a.inset_0, 1016 + a.justify_center, 1017 + a.pr_sm, 1018 + {left: 'auto'}, 1019 + ]}> 1020 + <Button 1021 + testID="searchTextInputClearBtn" 1022 + onPress={onPressClearQuery} 1023 + label={_(msg`Clear search query`)} 1024 + hitSlop={HITSLOP_10} 1025 + size="tiny" 1026 + shape="round" 1027 + variant="ghost" 1028 + color="secondary"> 1029 + <ButtonIcon icon={X} size="sm" /> 1030 + </Button> 1031 + </View> 833 1032 )} 834 - </Pressable> 1033 + </View> 835 1034 ) 836 1035 } 837 1036 SearchInputBox = React.memo(SearchInputBox) ··· 1029 1228 } 1030 1229 } 1031 1230 1032 - const HEADER_HEIGHT = 46 1033 - 1034 1231 const styles = StyleSheet.create({ 1035 - header: { 1036 - flexDirection: 'row', 1037 - alignItems: 'center', 1038 - paddingHorizontal: 12, 1039 - paddingLeft: 13, 1040 - paddingVertical: 4, 1041 - height: HEADER_HEIGHT, 1042 - // @ts-ignore web only 1043 - position: isWeb ? 'sticky' : '', 1044 - top: 0, 1045 - zIndex: 1, 1046 - }, 1047 1232 headerMenuBtn: { 1048 1233 width: 30, 1049 1234 height: 30, ··· 1074 1259 alignSelf: 'center', 1075 1260 zIndex: -1, 1076 1261 elevation: -1, // For Android 1077 - }, 1078 - tabBarContainer: { 1079 - // @ts-ignore web only 1080 - position: isWeb ? 'sticky' : '', 1081 - top: isWeb ? HEADER_HEIGHT : 0, 1082 - zIndex: 1, 1083 1262 }, 1084 1263 searchHistoryContainer: { 1085 1264 width: '100%',
+1 -1
src/view/screens/Storybook/Forms.tsx
··· 32 32 label="Text field" 33 33 /> 34 34 35 - <View style={[a.flex_row, a.gap_sm]}> 35 + <View style={[a.flex_row, a.align_start, a.gap_sm]}> 36 36 <View 37 37 style={[ 38 38 {