diff options
Diffstat (limited to 'apps/mobile/components/ui/SearchInput')
| -rw-r--r-- | apps/mobile/components/ui/SearchInput/SearchInput.ios.tsx | 187 | ||||
| -rw-r--r-- | apps/mobile/components/ui/SearchInput/SearchInput.tsx | 114 | ||||
| -rw-r--r-- | apps/mobile/components/ui/SearchInput/index.ts | 1 | ||||
| -rw-r--r-- | apps/mobile/components/ui/SearchInput/types.ts | 13 |
4 files changed, 315 insertions, 0 deletions
diff --git a/apps/mobile/components/ui/SearchInput/SearchInput.ios.tsx b/apps/mobile/components/ui/SearchInput/SearchInput.ios.tsx new file mode 100644 index 00000000..969e48b2 --- /dev/null +++ b/apps/mobile/components/ui/SearchInput/SearchInput.ios.tsx @@ -0,0 +1,187 @@ +import type { + NativeSyntheticEvent, + TextInputFocusEventData, +} from "react-native"; +import * as React from "react"; +import { Pressable, TextInput, View, ViewStyle } from "react-native"; +import Animated, { + measure, + useAnimatedRef, + useAnimatedStyle, + useDerivedValue, + withTiming, +} from "react-native-reanimated"; +import { Text } from "@/components/ui/Text"; +import { useColorScheme } from "@/lib/useColorScheme"; +import { cn } from "@/lib/utils"; +import { useAugmentedRef, useControllableState } from "@rn-primitives/hooks"; +import { Icon } from "@roninoss/icons"; + +import type { SearchInputProps } from "./types"; + +// Add as class when possible: https://github.com/marklawlor/nativewind/issues/522 +const BORDER_CURVE: ViewStyle = { + borderCurve: "continuous", +}; + +const SearchInput = React.forwardRef< + React.ElementRef<typeof TextInput>, + SearchInputProps +>( + ( + { + value: valueProp, + onChangeText: onChangeTextProp, + onFocus: onFocusProp, + placeholder = "Search...", + cancelText = "Cancel", + containerClassName, + iconContainerClassName, + className, + iconColor, + onCancel, + ...props + }, + ref, + ) => { + const { colors } = useColorScheme(); + const inputRef = useAugmentedRef({ ref, methods: { focus, blur, clear } }); + const [showCancel, setShowCancel] = React.useState(false); + const showCancelDerivedValue = useDerivedValue( + () => showCancel, + [showCancel], + ); + const animatedRef = useAnimatedRef(); + + const [value = "", onChangeText] = useControllableState({ + prop: valueProp, + defaultProp: valueProp ?? "", + onChange: onChangeTextProp, + }); + + const rootStyle = useAnimatedStyle(() => { + if (_WORKLET) { + // safely use measure + const measurement = measure(animatedRef); + return { + paddingRight: showCancelDerivedValue.value + ? withTiming(measurement?.width ?? cancelText.length * 11.2) + : withTiming(0), + }; + } + return { + paddingRight: showCancelDerivedValue.value + ? withTiming(cancelText.length * 11.2) + : withTiming(0), + }; + }); + const buttonStyle3 = useAnimatedStyle(() => { + if (_WORKLET) { + // safely use measure + const measurement = measure(animatedRef); + return { + position: "absolute", + right: 0, + opacity: showCancelDerivedValue.value ? withTiming(1) : withTiming(0), + transform: [ + { + translateX: showCancelDerivedValue.value + ? withTiming(0) + : measurement?.width + ? withTiming(measurement.width) + : cancelText.length * 11.2, + }, + ], + }; + } + return { + position: "absolute", + right: 0, + opacity: showCancelDerivedValue.value ? withTiming(1) : withTiming(0), + transform: [ + { + translateX: showCancelDerivedValue.value + ? withTiming(0) + : withTiming(cancelText.length * 11.2), + }, + ], + }; + }); + + function focus() { + inputRef.current?.focus(); + } + + function blur() { + inputRef.current?.blur(); + } + + function clear() { + onChangeText(""); + } + + function onFocus(e: NativeSyntheticEvent<TextInputFocusEventData>) { + setShowCancel(true); + onFocusProp?.(e); + } + + return ( + <Animated.View className="flex-row items-center" style={rootStyle}> + <Animated.View + style={BORDER_CURVE} + className={cn( + "flex-1 flex-row rounded-lg bg-card", + containerClassName, + )} + > + <View + className={cn( + "absolute bottom-0 left-0 top-0 z-50 justify-center pl-1.5", + iconContainerClassName, + )} + > + <Icon color={iconColor ?? colors.grey3} name="magnify" size={22} /> + </View> + <TextInput + ref={inputRef} + placeholder={placeholder} + className={cn( + !showCancel && "active:bg-muted/5 dark:active:bg-muted/20", + "flex-1 rounded-lg py-2 pl-8 pr-1 text-[17px] text-foreground", + className, + )} + value={value} + onChangeText={onChangeText} + onFocus={onFocus} + clearButtonMode="while-editing" + role="searchbox" + {...props} + /> + </Animated.View> + <Animated.View + ref={animatedRef} + style={buttonStyle3} + pointerEvents={!showCancel ? "none" : "auto"} + > + <Pressable + onPress={() => { + onChangeText(""); + inputRef.current?.blur(); + setShowCancel(false); + onCancel?.(); + }} + disabled={!showCancel} + pointerEvents={!showCancel ? "none" : "auto"} + className="flex-1 justify-center active:opacity-50" + > + <Text className="px-2 text-primary">{cancelText}</Text> + </Pressable> + </Animated.View> + </Animated.View> + ); + }, +); + +SearchInput.displayName = "SearchInput"; + +export { SearchInput }; diff --git a/apps/mobile/components/ui/SearchInput/SearchInput.tsx b/apps/mobile/components/ui/SearchInput/SearchInput.tsx new file mode 100644 index 00000000..7e816ab6 --- /dev/null +++ b/apps/mobile/components/ui/SearchInput/SearchInput.tsx @@ -0,0 +1,114 @@ +import * as React from "react"; +import { Pressable, TextInput, View } from "react-native"; +import Animated, { FadeIn, FadeOut } from "react-native-reanimated"; +import { TailwindResolver } from "@/components/TailwindResolver"; +import { Button } from "@/components/ui/Button"; +import { useColorScheme } from "@/lib/useColorScheme"; +import { cn } from "@/lib/utils"; +import { useAugmentedRef, useControllableState } from "@rn-primitives/hooks"; +import { Icon } from "@roninoss/icons"; + +import type { SearchInputProps } from "./types"; + +const SearchInput = React.forwardRef< + React.ElementRef<typeof TextInput>, + SearchInputProps +>( + ( + { + value: valueProp, + onChangeText: onChangeTextProp, + placeholder = "Search...", + containerClassName, + iconContainerClassName, + className, + onCancel, + ...props + }, + ref, + ) => { + const { colors } = useColorScheme(); + const inputRef = useAugmentedRef({ ref, methods: { focus, blur, clear } }); + const [value = "", onChangeText] = useControllableState({ + prop: valueProp, + defaultProp: valueProp ?? "", + onChange: onChangeTextProp, + }); + + function focus() { + inputRef.current?.focus(); + } + + function blur() { + inputRef.current?.blur(); + } + + function clear() { + onCancel?.(); + onChangeText(""); + } + + return ( + <Button + variant="plain" + className={cn( + "android:gap-0 android:h-14 flex-row items-center rounded-full bg-card px-2", + containerClassName, + )} + onPress={focus} + > + <View + className={cn("p-2", iconContainerClassName)} + pointerEvents="none" + > + <TailwindResolver + className="text-muted" + comp={(styles) => ( + <Icon + color={styles?.color?.toString()} + name="magnify" + size={24} + /> + )} + /> + </View> + + <View className="flex-1" pointerEvents="none"> + <TextInput + ref={inputRef} + placeholder={placeholder} + className={cn( + "flex-1 rounded-r-full p-2 text-[17px] text-foreground placeholder:text-muted", + className, + )} + placeholderTextColor={colors.foreground} + value={value} + onChangeText={onChangeText} + role="searchbox" + {...props} + /> + </View> + {!!value && ( + <Animated.View entering={FadeIn} exiting={FadeOut.duration(150)}> + <Pressable className="p-2" onPress={clear}> + <TailwindResolver + className="text-muted" + comp={(styles) => ( + <Icon + name="close" + size={24} + color={styles?.color?.toString()} + /> + )} + /> + </Pressable> + </Animated.View> + )} + </Button> + ); + }, +); + +SearchInput.displayName = "SearchInput"; + +export { SearchInput }; diff --git a/apps/mobile/components/ui/SearchInput/index.ts b/apps/mobile/components/ui/SearchInput/index.ts new file mode 100644 index 00000000..e5150fe3 --- /dev/null +++ b/apps/mobile/components/ui/SearchInput/index.ts @@ -0,0 +1 @@ +export * from "./SearchInput"; diff --git a/apps/mobile/components/ui/SearchInput/types.ts b/apps/mobile/components/ui/SearchInput/types.ts new file mode 100644 index 00000000..e0be8a2c --- /dev/null +++ b/apps/mobile/components/ui/SearchInput/types.ts @@ -0,0 +1,13 @@ +import type { TextInput, TextInputProps } from "react-native"; + +interface SearchInputProps extends TextInputProps { + containerClassName?: string; + iconContainerClassName?: string; + cancelText?: string; + iconColor?: string; + onCancel?: () => void; +} + +type SearchInputRef = TextInput; + +export type { SearchInputProps, SearchInputRef }; |
