From 6e944b925e4737d8dff679962b5c19182458428f Mon Sep 17 00:00:00 2001 From: aelassas Date: Wed, 18 Dec 2024 03:14:09 +0100 Subject: [PATCH] Fix AutocompleteDropdown on iOS --- mobile/App.tsx | 13 +- .../AutocompleteDropdownContext.tsx | 170 +++++ .../AutocompleteDropdown-v4.3.1/Dropdown.tsx | 82 +++ .../HOC/withFadeAnimation.tsx | 32 + .../NothingFound.tsx | 27 + .../RightButton.tsx | 107 ++++ .../ScrollViewListItem.tsx | 83 +++ .../diacriticless.ts | 527 ++++++++++++++++ .../AutocompleteDropdown-v4.3.1/helpers.ts | 21 + .../AutocompleteDropdown-v4.3.1/index.tsx | 587 ++++++++++++++++++ .../AutocompleteDropdown-v4.3.1/theme.tsx | 23 + .../types/global.d.ts | 8 + .../types/index.ts | 77 +++ .../useKeyboardHeight.ts | 17 + mobile/components/Layout.tsx | 4 +- mobile/components/LocationSelectList.tsx | 2 +- 16 files changed, 1772 insertions(+), 8 deletions(-) create mode 100644 mobile/components/AutocompleteDropdown-v4.3.1/AutocompleteDropdownContext.tsx create mode 100644 mobile/components/AutocompleteDropdown-v4.3.1/Dropdown.tsx create mode 100644 mobile/components/AutocompleteDropdown-v4.3.1/HOC/withFadeAnimation.tsx create mode 100644 mobile/components/AutocompleteDropdown-v4.3.1/NothingFound.tsx create mode 100644 mobile/components/AutocompleteDropdown-v4.3.1/RightButton.tsx create mode 100644 mobile/components/AutocompleteDropdown-v4.3.1/ScrollViewListItem.tsx create mode 100644 mobile/components/AutocompleteDropdown-v4.3.1/diacriticless.ts create mode 100644 mobile/components/AutocompleteDropdown-v4.3.1/helpers.ts create mode 100644 mobile/components/AutocompleteDropdown-v4.3.1/index.tsx create mode 100644 mobile/components/AutocompleteDropdown-v4.3.1/theme.tsx create mode 100644 mobile/components/AutocompleteDropdown-v4.3.1/types/global.d.ts create mode 100644 mobile/components/AutocompleteDropdown-v4.3.1/types/index.ts create mode 100644 mobile/components/AutocompleteDropdown-v4.3.1/useKeyboardHeight.ts diff --git a/mobile/App.tsx b/mobile/App.tsx index 099dc0e99..d8bd01a9b 100644 --- a/mobile/App.tsx +++ b/mobile/App.tsx @@ -14,6 +14,7 @@ import * as NotificationService from './services/NotificationService' import * as UserService from './services/UserService' import { GlobalProvider } from './context/GlobalContext' import * as env from './config/env.config' +import { AutocompleteDropdownContextProvider } from '@/components/AutocompleteDropdown-v4.3.1' Notifications.setNotificationHandler({ handleNotification: async () => ({ @@ -105,11 +106,13 @@ const App = () => { - - - - - + + + + + + + diff --git a/mobile/components/AutocompleteDropdown-v4.3.1/AutocompleteDropdownContext.tsx b/mobile/components/AutocompleteDropdown-v4.3.1/AutocompleteDropdownContext.tsx new file mode 100644 index 000000000..b4a74bd55 --- /dev/null +++ b/mobile/components/AutocompleteDropdown-v4.3.1/AutocompleteDropdownContext.tsx @@ -0,0 +1,170 @@ +import React, { useCallback, useRef, useState, useEffect } from 'react' +import type { SetStateAction, Dispatch, FC, ReactElement, MutableRefObject } from 'react' +import type { LayoutChangeEvent, ViewProps } from 'react-native' +import { StyleSheet, View } from 'react-native' +import type { IAutocompleteDropdownRef } from './types' + +export interface IAutocompleteDropdownContext { + content?: ReactElement + setContent: Dispatch> + direction?: 'up' | 'down' + setDirection: Dispatch> + activeInputContainerRef?: MutableRefObject + activeControllerRef?: MutableRefObject + controllerRefs?: MutableRefObject +} + +export interface IAutocompleteDropdownContextProviderProps { + headerOffset?: number + children: React.ReactNode +} + +export const AutocompleteDropdownContext = React.createContext({ + content: undefined, + setContent: () => null, + direction: undefined, + setDirection: () => null, + activeInputContainerRef: undefined, + activeControllerRef: undefined, + controllerRefs: undefined, +}) + +export const AutocompleteDropdownContextProvider: FC = ({ + headerOffset = 0, + children, +}) => { + const [content, setContent] = useState() + const [direction, setDirection] = useState(undefined) + const [show, setShow] = useState(false) + const [dropdownHeight, setDropdownHeight] = useState(0) + const [inputMeasurements, setInputMeasurements] = useState< + { x: number; topY: number; bottomY: number; width: number; height: number } | undefined + >() + const [opacity, setOpacity] = useState(0) + const [contentStyles, setContentStyles] = useState< + { top?: number; left: number; width?: number; bottom?: number } | undefined + >(undefined) + const activeInputContainerRef = useRef(null) + const wrapperRef = useRef(null) + const activeControllerRef = useRef(null) + const controllerRefs = useRef([]) + const positionTrackingIntervalRef = useRef() + + useEffect(() => { + if (!inputMeasurements?.height) { + setOpacity(0) + return + } + + if (dropdownHeight && direction === 'up') { + setContentStyles({ + bottom: inputMeasurements.bottomY + 5 + headerOffset, + top: undefined, + left: inputMeasurements.x, + width: inputMeasurements.width, + }) + setOpacity(1) + } else if (direction === 'down') { + setContentStyles({ + top: inputMeasurements.topY + inputMeasurements.height + 5 + headerOffset, + bottom: undefined, + left: inputMeasurements.x, + width: inputMeasurements.width, + }) + setOpacity(1) + } + }, [direction, dropdownHeight, headerOffset, inputMeasurements]) + + const recalculatePosition = useCallback((showAfterCalculation = false) => { + activeInputContainerRef?.current?.measure((x, y, width, height, inputPageX, inputPageY) => { + wrapperRef.current?.measure((wrapperX, wrapperY, wrapperW, wrapperH, wrapperPageX, wrapperPageY) => { + const currentMeasurement = { + width, + height, + x: inputPageX, + topY: inputPageY - wrapperPageY, + bottomY: wrapperH - inputPageY + wrapperPageY, + } + setInputMeasurements(prev => + JSON.stringify(prev) === JSON.stringify(currentMeasurement) ? prev : currentMeasurement, + ) + showAfterCalculation && setShow(true) + }) + }) + }, []) + + useEffect(() => { + if (content) { + recalculatePosition(true) + } else { + setInputMeasurements(undefined) + setDropdownHeight(0) + setOpacity(0) + setContentStyles(undefined) + setShow(false) + } + }, [content, recalculatePosition]) + + useEffect(() => { + if (show && !!opacity) { + positionTrackingIntervalRef.current = setInterval(() => { + requestAnimationFrame(() => { + recalculatePosition() + }) + }, 16) + } else { + clearInterval(positionTrackingIntervalRef.current) + } + + return () => { + clearInterval(positionTrackingIntervalRef.current) + } + }, [recalculatePosition, opacity, show]) + + const onLayout: ViewProps['onLayout'] = useCallback((e: LayoutChangeEvent) => { + setDropdownHeight(e.nativeEvent.layout.height) + }, []) + + return ( + + { + activeControllerRef.current?.close() + activeControllerRef.current?.blur() + }}> + {children} + + {!!content && show && ( + + {content} + + )} + + ) +} + +const styles = StyleSheet.create({ + clickOutsideHandlerArea: { + flex: 1, + }, + wrapper: { + position: 'absolute', + }, +}) diff --git a/mobile/components/AutocompleteDropdown-v4.3.1/Dropdown.tsx b/mobile/components/AutocompleteDropdown-v4.3.1/Dropdown.tsx new file mode 100644 index 000000000..e60945d97 --- /dev/null +++ b/mobile/components/AutocompleteDropdown-v4.3.1/Dropdown.tsx @@ -0,0 +1,82 @@ +/* eslint-disable react/display-name */ +import React, { memo, useMemo } from 'react' +import type { ListRenderItem } from 'react-native' +import { StyleSheet, FlatList, View, useColorScheme } from 'react-native' +import * as Animatable from 'react-native-animatable' +import { fadeInDownShort, fadeInUpShort } from './helpers' +import { theme } from './theme' +import type { AutocompleteDropdownItem, IAutocompleteDropdownProps } from './types' + +interface DropdownProps extends Omit { + ListEmptyComponent: JSX.Element + renderItem: ListRenderItem +} + +export const Dropdown = memo((props: DropdownProps) => { + const { + dataSet, + suggestionsListMaxHeight, + renderItem, + ListEmptyComponent, + ItemSeparatorComponent, + direction, + ...rest + } = props + const themeName = useColorScheme() + const styles = useMemo(() => getStyles(themeName || 'light'), [themeName]) + + const defaultItemSeparator = useMemo(() => function () { + return + }, [styles.itemSeparator]) + + return ( + + item.id} + ListEmptyComponent={ListEmptyComponent} + ItemSeparatorComponent={ItemSeparatorComponent ?? defaultItemSeparator} + {...rest.flatListProps} + /> + + ) +}) + +const getStyles = (themeName: 'light' | 'dark' = 'light') => + StyleSheet.create({ + container: {}, + listContainer: { + backgroundColor: theme[themeName].suggestionsListBackgroundColor, + width: '100%', + zIndex: 9, + borderRadius: 5, + shadowColor: theme[themeName || 'light'].shadowColor, + shadowOffset: { + width: 0, + height: 12, + }, + shadowOpacity: 0.3, + shadowRadius: 15.46, + + elevation: 20, + }, + itemSeparator: { + height: 1, + width: '100%', + backgroundColor: theme[themeName || 'light'].itemSeparatorColor, + }, + }) diff --git a/mobile/components/AutocompleteDropdown-v4.3.1/HOC/withFadeAnimation.tsx b/mobile/components/AutocompleteDropdown-v4.3.1/HOC/withFadeAnimation.tsx new file mode 100644 index 000000000..4897ec237 --- /dev/null +++ b/mobile/components/AutocompleteDropdown-v4.3.1/HOC/withFadeAnimation.tsx @@ -0,0 +1,32 @@ +import type { FC, ComponentType } from 'react' +import React, { useEffect, useRef } from 'react' +import type { ViewProps } from 'react-native' +import { Animated, Easing } from 'react-native' + +interface WithFadeAnimationProps { + containerStyle?: ViewProps['style'] +} + +export const withFadeAnimation =

( + WrappedComponent: ComponentType

, + { containerStyle }: WithFadeAnimationProps = {}, +): FC

=> { + return (props: P) => { + const opacityAnimationValue = useRef(new Animated.Value(0)).current + + useEffect(() => { + Animated.timing(opacityAnimationValue, { + duration: 800, + toValue: 1, + useNativeDriver: true, + easing: Easing.bezier(0.3, 0.58, 0.25, 0.99), + }).start() + }, [opacityAnimationValue]) + + return ( + + + + ) + } +} diff --git a/mobile/components/AutocompleteDropdown-v4.3.1/NothingFound.tsx b/mobile/components/AutocompleteDropdown-v4.3.1/NothingFound.tsx new file mode 100644 index 000000000..c128f15ee --- /dev/null +++ b/mobile/components/AutocompleteDropdown-v4.3.1/NothingFound.tsx @@ -0,0 +1,27 @@ +import type { FC } from 'react' +import React, { memo } from 'react' +import { StyleSheet, Text, View } from 'react-native' +import { withFadeAnimation } from './HOC/withFadeAnimation' + +interface NothingFoundProps { + emptyResultText?: string +} + +export const NothingFound: FC = memo(({ ...props }) => { + const EL = withFadeAnimation( + () => ( + + {props.emptyResultText || 'Nothing found'} + + ), + {}, + ) + return +}) + +const styles = StyleSheet.create({ + container: { + padding: 10, + }, + text: { textAlign: 'center' }, +}) diff --git a/mobile/components/AutocompleteDropdown-v4.3.1/RightButton.tsx b/mobile/components/AutocompleteDropdown-v4.3.1/RightButton.tsx new file mode 100644 index 000000000..9cb3e62f9 --- /dev/null +++ b/mobile/components/AutocompleteDropdown-v4.3.1/RightButton.tsx @@ -0,0 +1,107 @@ +/* eslint-disable react/display-name */ +import React, { memo, useEffect, useRef } from 'react' +import type { StyleProp, ViewStyle } from 'react-native' +import { ActivityIndicator, Animated, Easing, StyleSheet, TouchableOpacity, View } from 'react-native' +// import { ChevronDown, XCircle } from 'react-native-feather' +import { ChevronDown } from 'react-native-feather' +import { MaterialIcons } from '@expo/vector-icons' + +interface RightButtonProps { + inputHeight?: number + onClearPress?: () => void + onChevronPress?: () => void + isOpened?: boolean + showChevron?: boolean + showClear?: boolean + loading?: boolean + buttonsContainerStyle?: StyleProp + ChevronIconComponent?: React.ReactNode + ClearIconComponent?: React.ReactNode + RightIconComponent?: React.ReactNode + onRightIconComponentPress?: () => void +} + +export const RightButton: React.FC = memo( + ({ + inputHeight, + onClearPress, + onChevronPress, + isOpened, + showChevron, + showClear, + loading, + buttonsContainerStyle, + ChevronIconComponent, + ClearIconComponent, + RightIconComponent, + onRightIconComponentPress, + }) => { + const isOpenedAnimationValue = useRef(new Animated.Value(0)).current + + useEffect(() => { + Animated.timing(isOpenedAnimationValue, { + duration: 350, + toValue: isOpened ? 1 : 0, + useNativeDriver: true, + easing: Easing.bezier(0.3, 0.58, 0.25, 0.99), + }).start() + }, [isOpened, isOpenedAnimationValue]) + + const chevronSpin = isOpenedAnimationValue.interpolate({ + inputRange: [0, 1], + outputRange: ['0deg', '180deg'], + }) + + return ( + + {!loading && showClear && ( + + {/* {ClearIconComponent ?? } */} + {ClearIconComponent ?? } + + )} + {loading && } + {RightIconComponent && ( + + {RightIconComponent} + + )} + {showChevron && ( + + + {ChevronIconComponent ?? } + + + )} + + ) + }, +) + +const styles = StyleSheet.create({ + container: { + position: 'relative', + flex: 0, + flexDirection: 'row', + right: 8, + zIndex: 10, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: 'transparent', + }, + clearButton: { + width: 26, + alignItems: 'center', + }, + chevronButton: { + width: 26, + alignItems: 'center', + height: '100%', + justifyContent: 'center', + }, +}) diff --git a/mobile/components/AutocompleteDropdown-v4.3.1/ScrollViewListItem.tsx b/mobile/components/AutocompleteDropdown-v4.3.1/ScrollViewListItem.tsx new file mode 100644 index 000000000..fc4d93c28 --- /dev/null +++ b/mobile/components/AutocompleteDropdown-v4.3.1/ScrollViewListItem.tsx @@ -0,0 +1,83 @@ +import type { FC } from 'react' +import React, { memo, useMemo } from 'react' +import type { ViewProps } from 'react-native' +import { StyleSheet, Text, TouchableOpacity, View, useColorScheme } from 'react-native' +import diacriticless from './diacriticless' +import { theme } from './theme' + +interface ScrollViewListItemProps { + highlight: string + title: string + style?: ViewProps['style'] + onPress?: () => void + ignoreAccents?: boolean + numberOfLines?: number +} + +export const ScrollViewListItem: FC = memo( + ({ highlight, title, style, onPress, ignoreAccents, numberOfLines = 2 }) => { + const themeName = useColorScheme() + const styles = useMemo(() => getStyles(themeName || 'light'), [themeName]) + + const titleParts = useMemo(() => { + let titleHighlighted = '' + let titleStart = title + let titleEnd = '' + + if (typeof title === 'string' && title?.length > 0 && highlight?.length > 0) { + const highlightIn = ignoreAccents ? diacriticless(title?.toLowerCase()) : title?.toLowerCase() + const highlightWhat = ignoreAccents ? diacriticless(highlight?.toLowerCase()) : highlight?.toLowerCase() + + const substrIndex = highlightIn?.indexOf(highlightWhat) + if (substrIndex !== -1) { + titleStart = title?.slice(0, substrIndex) + titleHighlighted = title?.slice(substrIndex, substrIndex + highlight?.length) + titleEnd = title?.slice(substrIndex + highlight?.length) + } + } + + return { titleHighlighted, titleStart, titleEnd } + }, [highlight, ignoreAccents, title]) + + return ( + + + + + {titleParts.titleStart} + + + {titleParts.titleHighlighted} + + + {titleParts.titleEnd} + + + + + ) + }, +) + +const getStyles = (themeName: 'light' | 'dark' = 'light') => + StyleSheet.create({ + container: { + padding: 15, + flex: 1, + flexDirection: 'row', + alignItems: 'flex-start', + justifyContent: 'flex-start', + flexWrap: 'nowrap', + + width: '100%', + }, + text: { + color: theme[themeName].listItemTextColor, + fontSize: 16, + flexGrow: 1, + flexShrink: 0, + }, + textBold: { + fontWeight: 'bold', + }, + }) diff --git a/mobile/components/AutocompleteDropdown-v4.3.1/diacriticless.ts b/mobile/components/AutocompleteDropdown-v4.3.1/diacriticless.ts new file mode 100644 index 000000000..3f8448a7a --- /dev/null +++ b/mobile/components/AutocompleteDropdown-v4.3.1/diacriticless.ts @@ -0,0 +1,527 @@ +interface DiacriticsMap { + [key: string]: string[] +} + +// all diacritics +const diacritics: DiacriticsMap = { + a: [ + 'a', + 'à', + 'á', + 'â', + 'ã', + 'ä', + 'å', + 'æ', + 'ā', + 'ă', + 'ą', + 'ǎ', + 'ǟ', + 'ǡ', + 'ǻ', + 'ȁ', + 'ȃ', + 'ȧ', + 'ɐ', + 'ɑ', + 'ɒ', + 'ͣ', + 'а', + 'ӑ', + 'ӓ', + 'ᵃ', + 'ᵄ', + 'ᶏ', + 'ḁ', + 'ẚ', + 'ạ', + 'ả', + 'ấ', + 'ầ', + 'ẩ', + 'ẫ', + 'ậ', + 'ắ', + 'ằ', + 'ẳ', + 'ẵ', + 'ặ', + 'ₐ', + 'ⱥ', + 'a', + ], + A: [ + 'A', + 'À', + 'Á', + 'Â', + 'Ã', + 'Ä', + 'Å', + 'Ā', + 'Ă', + 'Ą', + 'Ǎ', + 'Ǟ', + 'Ǡ', + 'Ǻ', + 'Ȁ', + 'Ȃ', + 'Ȧ', + 'Ⱥ', + 'А', + 'Ӑ', + 'Ӓ', + 'ᴀ', + 'ᴬ', + 'Ḁ', + 'Ạ', + 'Ả', + 'Ấ', + 'Ầ', + 'Ẩ', + 'Ẫ', + 'Ậ', + 'Ắ', + 'Ằ', + 'Ẳ', + 'Ẵ', + 'Ặ', + 'A', + ], + + b: ['b', 'ƀ', 'ƃ', 'ɓ', 'ᖯ', 'ᵇ', 'ᵬ', 'ᶀ', 'ḃ', 'ḅ', 'ḇ', 'b'], + B: ['B', 'Ɓ', 'Ƃ', 'Ƀ', 'ʙ', 'ᛒ', 'ᴃ', 'ᴮ', 'ᴯ', 'Ḃ', 'Ḅ', 'Ḇ', 'B'], + + c: ['c', 'ç', 'ć', 'ĉ', 'ċ', 'č', 'ƈ', 'ȼ', 'ɕ', 'ͨ', 'ᴄ', 'ᶜ', 'ḉ', 'ↄ', 'c'], + C: ['C', 'Ç', 'Ć', 'Ĉ', 'Ċ', 'Č', 'Ƈ', 'Ȼ', 'ʗ', 'Ḉ', 'C'], + + d: ['d', 'ď', 'đ', 'Ƌ', 'ƌ', 'ȡ', 'ɖ', 'ɗ', 'ͩ', 'ᵈ', 'ᵭ', 'ᶁ', 'ᶑ', 'ḋ', 'ḍ', 'ḏ', 'ḑ', 'ḓ', 'd'], + D: ['D', 'Ď', 'Đ', 'Ɖ', 'Ɗ', 'ᴰ', 'Ḋ', 'Ḍ', 'Ḏ', 'Ḑ', 'Ḓ', 'D'], + + e: [ + 'e', + 'è', + 'é', + 'ê', + 'ë', + 'ē', + 'ĕ', + 'ė', + 'ę', + 'ě', + 'ǝ', + 'ȅ', + 'ȇ', + 'ȩ', + 'ɇ', + 'ɘ', + 'ͤ', + 'ᵉ', + 'ᶒ', + 'ḕ', + 'ḗ', + 'ḙ', + 'ḛ', + 'ḝ', + 'ẹ', + 'ẻ', + 'ẽ', + 'ế', + 'ề', + 'ể', + 'ễ', + 'ệ', + 'ₑ', + 'e', + ], + E: [ + 'E', + 'È', + 'É', + 'Ê', + 'Ë', + 'Ē', + 'Ĕ', + 'Ė', + 'Ę', + 'Ě', + 'Œ', + 'Ǝ', + 'Ɛ', + 'Ȅ', + 'Ȇ', + 'Ȩ', + 'Ɇ', + 'ɛ', + 'ɜ', + 'ɶ', + 'Є', + 'Э', + 'э', + 'є', + 'Ӭ', + 'ӭ', + 'ᴇ', + 'ᴈ', + 'ᴱ', + 'ᴲ', + 'ᵋ', + 'ᵌ', + 'ᶓ', + 'ᶔ', + 'ᶟ', + 'Ḕ', + 'Ḗ', + 'Ḙ', + 'Ḛ', + 'Ḝ', + 'Ẹ', + 'Ẻ', + 'Ẽ', + 'Ế', + 'Ề', + 'Ể', + 'Ễ', + 'Ệ', + 'E', + '𐐁', + '𐐩', + ], + + f: ['f', 'ƒ', 'ᵮ', 'ᶂ', 'ᶠ', 'ḟ', 'f'], + F: ['F', 'Ƒ', 'Ḟ', 'ⅎ', 'F'], + + g: ['g', 'ĝ', 'ğ', 'ġ', 'ģ', 'ǥ', 'ǧ', 'ǵ', 'ɠ', 'ɡ', 'ᵍ', 'ᵷ', 'ᵹ', 'ᶃ', 'ᶢ', 'ḡ', 'g'], + G: ['G', 'Ĝ', 'Ğ', 'Ġ', 'Ģ', 'Ɠ', 'Ǥ', 'Ǧ', 'Ǵ', 'ɢ', 'ʛ', 'ᴳ', 'Ḡ', 'G'], + + h: [ + 'h', + 'ĥ', + 'ħ', + 'ƕ', + 'ȟ', + 'ɥ', + 'ɦ', + 'ʮ', + 'ʯ', + 'ʰ', + 'ʱ', + 'ͪ', + 'Һ', + 'һ', + 'ᑋ', + 'ᶣ', + 'ḣ', + 'ḥ', + 'ḧ', + 'ḩ', + 'ḫ', + 'ⱨ', + 'h', + ], + H: ['H', 'Ĥ', 'Ħ', 'Ȟ', 'ʜ', 'ᕼ', 'ᚺ', 'ᚻ', 'ᴴ', 'Ḣ', 'Ḥ', 'Ḧ', 'Ḩ', 'Ḫ', 'Ⱨ', 'H'], + + i: [ + 'i', + 'ì', + 'í', + 'î', + 'ï', + 'ĩ', + 'ī', + 'ĭ', + 'į', + 'ǐ', + 'ȉ', + 'ȋ', + 'ɨ', + 'ͥ', + 'ᴉ', + 'ᵎ', + 'ᵢ', + 'ᶖ', + 'ᶤ', + 'ḭ', + 'ḯ', + 'ỉ', + 'ị', + 'i', + ], + I: [ + 'I', + 'Ì', + 'Í', + 'Î', + 'Ï', + 'Ĩ', + 'Ī', + 'Ĭ', + 'Į', + 'İ', + 'Ǐ', + 'Ȉ', + 'Ȋ', + 'ɪ', + 'І', + 'ᴵ', + 'ᵻ', + 'ᶦ', + 'ᶧ', + 'Ḭ', + 'Ḯ', + 'Ỉ', + 'Ị', + 'I', + ], + + j: ['j', 'ĵ', 'ǰ', 'ɉ', 'ʝ', 'ʲ', 'ᶡ', 'ᶨ', 'j'], + J: ['J', 'Ĵ', 'ᴊ', 'ᴶ', 'J'], + + k: ['k', 'ķ', 'ƙ', 'ǩ', 'ʞ', 'ᵏ', 'ᶄ', 'ḱ', 'ḳ', 'ḵ', 'ⱪ', 'k'], + K: ['K', 'Ķ', 'Ƙ', 'Ǩ', 'ᴷ', 'Ḱ', 'Ḳ', 'Ḵ', 'Ⱪ', 'K'], + + l: ['l', 'ĺ', 'ļ', 'ľ', 'ŀ', 'ł', 'ƚ', 'ȴ', 'ɫ', 'ɬ', 'ɭ', 'ˡ', 'ᶅ', 'ᶩ', 'ᶪ', 'ḷ', 'ḹ', 'ḻ', 'ḽ', 'ℓ', 'ⱡ'], + L: ['L', 'Ĺ', 'Ļ', 'Ľ', 'Ŀ', 'Ł', 'Ƚ', 'ʟ', 'ᴌ', 'ᴸ', 'ᶫ', 'Ḷ', 'Ḹ', 'Ḻ', 'Ḽ', 'Ⱡ', 'Ɫ'], + + m: ['m', 'ɯ', 'ɰ', 'ɱ', 'ͫ', 'ᴟ', 'ᵐ', 'ᵚ', 'ᵯ', 'ᶆ', 'ᶬ', 'ᶭ', 'ḿ', 'ṁ', 'ṃ', '㎡', '㎥', 'm'], + M: ['M', 'Ɯ', 'ᴍ', 'ᴹ', 'Ḿ', 'Ṁ', 'Ṃ', 'M'], + + n: ['n', 'ñ', 'ń', 'ņ', 'ň', 'ʼn', 'ƞ', 'ǹ', 'ȵ', 'ɲ', 'ɳ', 'ᵰ', 'ᶇ', 'ᶮ', 'ᶯ', 'ṅ', 'ṇ', 'ṉ', 'ṋ', 'ⁿ', 'n'], + N: ['N', 'Ñ', 'Ń', 'Ņ', 'Ň', 'Ɲ', 'Ǹ', 'Ƞ', 'ɴ', 'ᴎ', 'ᴺ', 'ᴻ', 'ᶰ', 'Ṅ', 'Ṇ', 'Ṉ', 'Ṋ', 'N'], + + o: [ + 'o', + 'ò', + 'ó', + 'ô', + 'õ', + 'ö', + 'ø', + 'ō', + 'ŏ', + 'ő', + 'ơ', + 'ǒ', + 'ǫ', + 'ǭ', + 'ǿ', + 'ȍ', + 'ȏ', + 'ȫ', + 'ȭ', + 'ȯ', + 'ȱ', + 'ɵ', + 'ͦ', + 'о', + 'ӧ', + 'ө', + 'ᴏ', + 'ᴑ', + 'ᴓ', + 'ᴼ', + 'ᵒ', + 'ᶱ', + 'ṍ', + 'ṏ', + 'ṑ', + 'ṓ', + 'ọ', + 'ỏ', + 'ố', + 'ồ', + 'ổ', + 'ỗ', + 'ộ', + 'ớ', + 'ờ', + 'ở', + 'ỡ', + 'ợ', + 'ₒ', + 'o', + '𐐬', + ], + O: [ + 'O', + 'Ò', + 'Ó', + 'Ô', + 'Õ', + 'Ö', + 'Ø', + 'Ō', + 'Ŏ', + 'Ő', + 'Ɵ', + 'Ơ', + 'Ǒ', + 'Ǫ', + 'Ǭ', + 'Ǿ', + 'Ȍ', + 'Ȏ', + 'Ȫ', + 'Ȭ', + 'Ȯ', + 'Ȱ', + 'О', + 'Ӧ', + 'Ө', + 'Ṍ', + 'Ṏ', + 'Ṑ', + 'Ṓ', + 'Ọ', + 'Ỏ', + 'Ố', + 'Ồ', + 'Ổ', + 'Ỗ', + 'Ộ', + 'Ớ', + 'Ờ', + 'Ở', + 'Ỡ', + 'Ợ', + 'O', + '𐐄', + ], + + p: ['p', 'ᵖ', 'ᵱ', 'ᵽ', 'ᶈ', 'ṕ', 'ṗ', 'p'], + P: ['P', 'Ƥ', 'ᴘ', 'ᴾ', 'Ṕ', 'Ṗ', 'Ᵽ', 'P'], + + q: ['q', 'ɋ', 'ʠ', 'ᛩ', 'q'], + Q: ['Q', 'Ɋ', 'Q'], + + r: ['r', 'ŕ', 'ŗ', 'ř', 'ȑ', 'ȓ', 'ɍ', 'ɹ', 'ɻ', 'ʳ', 'ʴ', 'ʵ', 'ͬ', 'ᵣ', 'ᵲ', 'ᶉ', 'ṙ', 'ṛ', 'ṝ', 'ṟ'], + R: ['R', 'Ŕ', 'Ŗ', 'Ř', 'Ʀ', 'Ȑ', 'Ȓ', 'Ɍ', 'ʀ', 'ʁ', 'ʶ', 'ᚱ', 'ᴙ', 'ᴚ', 'ᴿ', 'Ṙ', 'Ṛ', 'Ṝ', 'Ṟ', 'Ɽ'], + + s: ['s', 'ś', 'ŝ', 'ş', 'š', 'ș', 'ʂ', 'ᔆ', 'ᶊ', 'ṡ', 'ṣ', 'ṥ', 'ṧ', 'ṩ', 's'], + S: ['S', 'Ś', 'Ŝ', 'Ş', 'Š', 'Ș', 'ȿ', 'ˢ', 'ᵴ', 'Ṡ', 'Ṣ', 'Ṥ', 'Ṧ', 'Ṩ', 'S'], + + t: ['t', 'ţ', 'ť', 'ŧ', 'ƫ', 'ƭ', 'ț', 'ʇ', 'ͭ', 'ᵀ', 'ᵗ', 'ᵵ', 'ᶵ', 'ṫ', 'ṭ', 'ṯ', 'ṱ', 'ẗ', 't'], + T: ['T', 'Ţ', 'Ť', 'Ƭ', 'Ʈ', 'Ț', 'Ⱦ', 'ᴛ', 'ᵀ', 'Ṫ', 'Ṭ', 'Ṯ', 'Ṱ', 'T'], + + u: [ + 'u', + 'ù', + 'ú', + 'û', + 'ü', + 'ũ', + 'ū', + 'ŭ', + 'ů', + 'ű', + 'ų', + 'ư', + 'ǔ', + 'ǖ', + 'ǘ', + 'ǚ', + 'ǜ', + 'ȕ', + 'ȗ', + 'ͧ', + 'ߎ', + 'ᵘ', + 'ᵤ', + 'ṳ', + 'ṵ', + 'ṷ', + 'ṹ', + 'ṻ', + 'ụ', + 'ủ', + 'ứ', + 'ừ', + 'ử', + 'ữ', + 'ự', + 'u', + ], + U: [ + 'U', + 'Ù', + 'Ú', + 'Û', + 'Ü', + 'Ũ', + 'Ū', + 'Ŭ', + 'Ů', + 'Ű', + 'Ų', + 'Ư', + 'Ǔ', + 'Ǖ', + 'Ǘ', + 'Ǚ', + 'Ǜ', + 'Ȕ', + 'Ȗ', + 'Ʉ', + 'ᴜ', + 'ᵁ', + 'ᵾ', + 'Ṳ', + 'Ṵ', + 'Ṷ', + 'Ṹ', + 'Ṻ', + 'Ụ', + 'Ủ', + 'Ứ', + 'Ừ', + 'Ử', + 'Ữ', + 'Ự', + 'U', + ], + + v: ['v', 'ʋ', 'ͮ', 'ᵛ', 'ᵥ', 'ᶹ', 'ṽ', 'ṿ', 'ⱱ', 'v', 'ⱴ'], + V: ['V', 'Ʋ', 'Ʌ', 'ʌ', 'ᴠ', 'ᶌ', 'Ṽ', 'Ṿ', 'V'], + + w: ['w', 'ŵ', 'ʷ', 'ᵂ', 'ẁ', 'ẃ', 'ẅ', 'ẇ', 'ẉ', 'ẘ', 'ⱳ', 'w'], + W: ['W', 'Ŵ', 'ʍ', 'ᴡ', 'Ẁ', 'Ẃ', 'Ẅ', 'Ẇ', 'Ẉ', 'Ⱳ', 'W'], + + x: ['x', '̽', '͓', 'ᶍ', 'ͯ', 'ẋ', 'ẍ', 'ₓ', 'x'], + X: ['X', 'ˣ', 'ͯ', 'Ẋ', 'Ẍ', '☒', '✕', '✖', '✗', '✘', 'X'], + + y: ['y', 'ý', 'ÿ', 'ŷ', 'ȳ', 'ɏ', 'ʸ', 'ẏ', 'ỳ', 'ỵ', 'ỷ', 'ỹ', 'y'], + Y: ['Y', 'Ý', 'Ŷ', 'Ÿ', 'Ƴ', 'ƴ', 'Ȳ', 'Ɏ', 'ʎ', 'ʏ', 'Ẏ', 'Ỳ', 'Ỵ', 'Ỷ', 'Ỹ', 'Y'], + + z: ['z', 'ź', 'ż', 'ž', 'ƶ', 'ȥ', 'ɀ', 'ʐ', 'ʑ', 'ᙆ', 'ᙇ', 'ᶻ', 'ᶼ', 'ᶽ', 'ẑ', 'ẓ', 'ẕ', 'ⱬ', 'z'], + Z: ['Z', 'Ź', 'Ż', 'Ž', 'Ƶ', 'Ȥ', 'ᴢ', 'ᵶ', 'Ẑ', 'Ẓ', 'Ẕ', 'Ⱬ', 'Z'], +} + +/* + * Main function of the module which removes all diacritics from the received text + */ + +export default function removeDiacritics(text: string): string { + const result: string[] = [] + + for (let i = 0; i < text.length; i++) { + const searchChar = text.charAt(i) + let foundChar = false + + for (const key in diacritics) { + const index = diacritics[key]?.indexOf(searchChar) + if (index !== -1) { + result.push(key) + foundChar = true + break + } + } + + if (!foundChar) { + result.push(searchChar) + } + } + + return result.join('') +} diff --git a/mobile/components/AutocompleteDropdown-v4.3.1/helpers.ts b/mobile/components/AutocompleteDropdown-v4.3.1/helpers.ts new file mode 100644 index 000000000..b655af3f2 --- /dev/null +++ b/mobile/components/AutocompleteDropdown-v4.3.1/helpers.ts @@ -0,0 +1,21 @@ +export const fadeInDownShort = { + 0: { + opacity: 0, + transform: [{ translateY: -20 }], + }, + 1: { + opacity: 1, + transform: [{ translateY: 0 }], + }, +} + +export const fadeInUpShort = { + 0: { + opacity: 0, + transform: [{ translateY: 20 }], + }, + 1: { + opacity: 1, + transform: [{ translateY: 0 }], + }, +} diff --git a/mobile/components/AutocompleteDropdown-v4.3.1/index.tsx b/mobile/components/AutocompleteDropdown-v4.3.1/index.tsx new file mode 100644 index 000000000..8a7ddb9bb --- /dev/null +++ b/mobile/components/AutocompleteDropdown-v4.3.1/index.tsx @@ -0,0 +1,587 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions */ +import React, { + forwardRef, + memo, + useCallback, + useEffect, + useLayoutEffect, + useMemo, + useRef, + useState, + useContext, +} from 'react' +import debounce from 'lodash.debounce' +import type { + GestureResponderEvent, + ListRenderItem, + NativeSyntheticEvent, + TextInputFocusEventData, + TextInputSubmitEditingEventData, +} from 'react-native' +import { + Dimensions, + Keyboard, + Platform, + Pressable, + TextInput, + TouchableOpacity, + View, + useColorScheme, +} from 'react-native' +import { moderateScale, ScaledSheet } from 'react-native-size-matters' +import { Dropdown } from './Dropdown' +import { NothingFound } from './NothingFound' +import { RightButton } from './RightButton' +import { ScrollViewListItem } from './ScrollViewListItem' +import { AutocompleteDropdownContext, AutocompleteDropdownContextProvider } from './AutocompleteDropdownContext' +import diacriticless from './diacriticless' +import { theme } from './theme' +import type { IAutocompleteDropdownProps, AutocompleteDropdownItem } from './types' + +export * from './types' +export { AutocompleteDropdownContextProvider } + +export const AutocompleteDropdown = memo< + React.ForwardRefExoticComponent & React.RefAttributes> +>( + forwardRef((props: IAutocompleteDropdownProps, ref) => { + const { + dataSet: dataSetProp, + initialValue: initialValueProp, + clearOnFocus = true, + caseSensitive = false, + ignoreAccents = true, + trimSearchText = true, + editable = true, + matchFrom, + inputHeight = moderateScale(40, 0.2), + suggestionsListMaxHeight = moderateScale(200, 0.2), + // bottomOffset = 0, + direction: directionProp, + controller, + onSelectItem: onSelectItemProp, + onOpenSuggestionsList: onOpenSuggestionsListProp, + useFilter, + renderItem: customRenderItem, + EmptyResultComponent, + emptyResultText, + onClear, + onChangeText: onTextChange, + debounce: debounceDelay = 0, + onChevronPress: onChevronPressProp, + onFocus: onFocusProp, + onBlur: onBlurProp, + onSubmit: onSubmitProp, + closeOnSubmit, + loading: loadingProp, + LeftComponent, + textInputProps, + showChevron, + showClear, + rightButtonsContainerStyle, + ChevronIconComponent, + ClearIconComponent, + RightIconComponent, + onRightIconComponentPress, + containerStyle, + inputContainerStyle, + suggestionsListTextStyle, + } = props + const InputComponent = (props.InputComponent as typeof TextInput) || TextInput + const inputRef = useRef(null) + const containerRef = useRef(null) + const [searchText, setSearchText] = useState('') + const [inputValue, setInputValue] = useState('') + const [loading, setLoading] = useState(loadingProp) + const [selectedItem, setSelectedItem] = useState(null) + const [isOpened, setIsOpened] = useState(false) + const initialDataSetRef = useRef(dataSetProp) + // const initialValueRef = useRef(initialValueProp) + const [dataSet, setDataSet] = useState(dataSetProp) + const matchFromStart = matchFrom === 'start' + const { + content, + setContent, + activeInputContainerRef, + activeControllerRef, + direction = directionProp, + setDirection, + controllerRefs, + } = useContext(AutocompleteDropdownContext) + const themeName = useColorScheme() || 'light' + const styles = useMemo(() => getStyles(themeName), [themeName]) + + useEffect(() => { + setLoading(loadingProp) + }, [loadingProp]) + + const calculateDirection = useCallback( + async ({ waitForKeyboard }: { waitForKeyboard: boolean }) => { + const [, positionY] = await new Promise<[x: number, y: number, width: number, height: number]>((resolve) => { + containerRef.current?.measureInWindow((...rect) => resolve(rect)) + },) + + return new Promise((resolve) => { + setTimeout( + () => { + const kbHeight = Keyboard.metrics?.()?.height || 0 + const screenHeight = Dimensions.get('window').height + setDirection((screenHeight - kbHeight) / 2 > positionY ? 'down' : 'up') + resolve() + }, + waitForKeyboard ? Platform.select({ ios: 600, android: 250, default: 1 }) : 1, // wait for keyboard to show + ) + }) + }, + [setDirection], + ) + + const onClearPress = useCallback(() => { + setSearchText('') + setInputValue('') + setSelectedItem(null) + setIsOpened(false) + inputRef.current?.blur() + if (typeof onClear === 'function') { + onClear() + } + }, [onClear]) + + /** methods */ + const close = useCallback(() => { + setIsOpened(false) + setContent(undefined) + }, [setContent]) + + const blur = useCallback(() => { + inputRef.current?.blur() + }, []) + + const open = useCallback(async () => { + if (directionProp) { + setDirection(directionProp) + } else { + await calculateDirection({ waitForKeyboard: !!inputRef.current?.isFocused() }) + } + + setTimeout(() => { + setIsOpened(true) + }, 0) + }, [calculateDirection, directionProp, setDirection]) + + const toggle = useCallback(() => { + isOpened ? close() : open() + }, [close, isOpened, open]) + + const clear = useCallback(() => { + onClearPress() + }, [onClearPress]) + + useLayoutEffect(() => { + if (ref) { + if (typeof ref === 'function') { + ref(inputRef.current) + } else { + ref.current = inputRef.current + } + } + }, [ref]) + + /** Set initial value */ + // useEffect(() => { + // const initialDataSet = initialDataSetRef.current + // const initialValue = initialValueRef.current + + // let initialValueItem: AutocompleteDropdownItem | undefined + // if (typeof initialValue === 'string') { + // initialValueItem = initialDataSet?.find((el) => el.id === initialValue) + // } else if (typeof initialValue === 'object' && initialValue.id) { + // initialValueItem = initialDataSet?.find((el) => el.id === initialValue?.id) + // if (!initialValueItem) { + // // set the item as it is if it's not in the list + // initialValueItem = initialValue + // } + // } + + // if (initialValueItem) { + // setSelectedItem(initialValueItem) + // } + // }, []) + + // useEffect(() => () => { + // setContent(undefined) + // setIsOpened(false) + // }, [setContent]) + + /** Set initial value */ + useEffect(() => { + if (!Array.isArray(dataSet) || selectedItem) { + // nothing to set or already setted + return + } + + let dataSetItem + if (typeof initialValueProp === 'string') { + dataSetItem = dataSet.find((el) => el.id === initialValueProp) + } else if (typeof initialValueProp === 'object' && initialValueProp.id) { + dataSetItem = dataSet.find((el) => el.id === initialValueProp.id) + } + + if (dataSetItem) { + setSelectedItem(dataSetItem) + } + }, [initialValueProp, dataSet]) // eslint-disable-line react-hooks/exhaustive-deps + + useEffect(() => () => { + setContent(undefined) + setIsOpened(false) + }, [setContent]) + + const setInputText = useCallback((text: string) => { + setSearchText(text) + }, []) + + const setItem = useCallback((item: AutocompleteDropdownItem | null) => { + setSelectedItem(item) + }, []) + + useEffect(() => { + if (activeControllerRef?.current) { + controllerRefs?.current.push(activeControllerRef?.current) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + const closeAll = useCallback(() => { + controllerRefs?.current.forEach((c) => { + c?.blur?.() + c?.close?.() + }) + }, [controllerRefs]) + + /** expose controller methods */ + useEffect(() => { + const methods = activeControllerRef ? { close, blur, open, toggle, clear, setInputText, setItem } : null + if (activeControllerRef) { + activeControllerRef.current = methods + } + if (typeof controller === 'function') { + controller(methods) + } else if (controller) { + controller.current = methods + } + }, [blur, clear, close, controller, activeControllerRef, open, setInputText, setItem, toggle]) + + useEffect(() => { + if (selectedItem) { + setInputValue(selectedItem.title ?? '') + } else { + setInputValue('') + } + }, [selectedItem]) + + useEffect(() => { + setInputValue(searchText) + }, [searchText]) + + useEffect(() => { + if (typeof onSelectItemProp === 'function') { + onSelectItemProp(selectedItem) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedItem]) + + useEffect(() => { + if (typeof onOpenSuggestionsListProp === 'function') { + onOpenSuggestionsListProp(isOpened) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isOpened]) + + useEffect(() => { + // renew state on close + if (!isOpened && selectedItem && !loading && !inputRef.current?.isFocused()) { + setInputValue(selectedItem.title || '') + } + }, [isOpened, loading, searchText, selectedItem]) + + const _onSelectItem = useCallback((item: AutocompleteDropdownItem) => { + setSelectedItem(item) + inputRef.current?.blur() + setIsOpened(false) + }, []) + + useEffect(() => { + initialDataSetRef.current = dataSetProp + setDataSet(dataSetProp) + }, [dataSetProp]) + + useEffect(() => { + const initialDataSet = initialDataSetRef.current + if (!searchText?.length) { + setDataSet(initialDataSet) + return + } + + if (!Array.isArray(initialDataSet) || useFilter === false) { + return + } + + let findWhat = caseSensitive ? searchText : searchText.toLowerCase() + + if (ignoreAccents) { + findWhat = diacriticless(findWhat) + } + + if (trimSearchText) { + findWhat = findWhat.trim() + } + + const newSet = initialDataSet.filter((item: AutocompleteDropdownItem) => { + const titleStr = item.title || '' + const title = caseSensitive ? titleStr : titleStr.toLowerCase() + const findWhere = ignoreAccents ? diacriticless(title) : title + + if (matchFromStart) { + return typeof item.title === 'string' && findWhere.startsWith(findWhat) + } + return typeof item.title === 'string' && findWhere.indexOf(findWhat) !== -1 + }) + + setDataSet(newSet) + }, [ignoreAccents, matchFromStart, caseSensitive, searchText, trimSearchText, useFilter]) + + const renderItem: ListRenderItem = useCallback( + ({ item }) => { + if (typeof customRenderItem === 'function') { + const EL = customRenderItem(item, searchText) + return _onSelectItem(item)}>{EL} + } + + return ( + _onSelectItem(item)} + ignoreAccents={ignoreAccents} + /> + ) + }, + [_onSelectItem, customRenderItem, ignoreAccents, searchText, suggestionsListTextStyle], + ) + + const ListEmptyComponent = useMemo(() => EmptyResultComponent ?? , [EmptyResultComponent, emptyResultText]) + + const debouncedEvent = useMemo( + () => + debounce((text: string) => { + if (typeof onTextChange === 'function') { + onTextChange(text) + } + setLoading(false) + }, debounceDelay), + [debounceDelay, onTextChange], + ) + + const onChangeText = useCallback( + (text: string) => { + setSearchText(text) + setInputValue(text) + setLoading(true) + debouncedEvent(text) + }, + [debouncedEvent], + ) + + const onChevronPress = useCallback(() => { + toggle() + Keyboard.dismiss() + + if (typeof onChevronPressProp === 'function') { + onChevronPressProp() + } + }, [onChevronPressProp, toggle]) + + const onFocus = useCallback( + (e: NativeSyntheticEvent) => { + if (clearOnFocus) { + setSearchText('') + setInputValue('') + } + if (typeof onFocusProp === 'function') { + onFocusProp(e) + } + open() + }, + [clearOnFocus, onFocusProp, open], + ) + + const onBlur = useCallback( + (e: NativeSyntheticEvent) => { + if (typeof onBlurProp === 'function') { + onBlurProp(e) + } + }, + [onBlurProp], + ) + + const onSubmit = useCallback( + (e: NativeSyntheticEvent) => { + inputRef.current?.blur() + if (closeOnSubmit) { + close() + } + + if (typeof onSubmitProp === 'function') { + onSubmitProp(e) + } + }, + [close, closeOnSubmit, onSubmitProp], + ) + + const onPressOut = useCallback( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + (e: GestureResponderEvent) => { + closeAll() + if (editable) { + inputRef?.current?.focus() + } else { + toggle() + } + }, + [closeAll, editable, toggle], + ) + + // eslint-disable-next-line consistent-return + useEffect(() => { + if ((!content && !inputRef.current?.isFocused()) || loading) { + const db = debounce(() => { + setIsOpened(false) + }, 100) + db() + return () => { + db.cancel() + } + } + }, [content, loading]) + + useEffect(() => { + // searchTextRef + if (searchText && inputRef.current?.isFocused() && !loading) { + setIsOpened(true) + } + }, [loading, searchText]) + + useEffect(() => { + if (isOpened && Array.isArray(dataSet)) { + if (activeInputContainerRef) { + activeInputContainerRef.current = containerRef.current + } + + setContent( + , + ) + } else { + setContent(undefined) + } + }, [ + ListEmptyComponent, + activeInputContainerRef, + dataSet, + direction, + inputHeight, + isOpened, + props, + renderItem, + setContent, + suggestionsListMaxHeight, + ]) + + return ( + true} + onTouchEnd={(e) => { + e.stopPropagation() + }} + style={[styles.container, containerStyle]}> + { }} // it's necessary use onLayout here for Androd (bug?) + style={[styles.inputContainerStyle, inputContainerStyle]}> + {LeftComponent} + + + + + + + ) + }), +) + +const getStyles = (themeName: 'light' | 'dark' = 'light') => + ScaledSheet.create({ + container: { + marginVertical: 2, + }, + inputContainerStyle: { + display: 'flex', + flexDirection: 'row', + backgroundColor: theme[themeName].inputBackgroundColor, + borderRadius: 5, + overflow: 'hidden', + }, + input: { + flexGrow: 1, + flexShrink: 1, + overflow: 'hidden', + paddingHorizontal: 13, + fontSize: 16, + color: theme[themeName].inputTextColor, + }, + pressable: { + flexGrow: 1, + flexShrink: 1, + }, + }) diff --git a/mobile/components/AutocompleteDropdown-v4.3.1/theme.tsx b/mobile/components/AutocompleteDropdown-v4.3.1/theme.tsx new file mode 100644 index 000000000..3adfa76fd --- /dev/null +++ b/mobile/components/AutocompleteDropdown-v4.3.1/theme.tsx @@ -0,0 +1,23 @@ +export const light = { + inputBackgroundColor: '#e5ecf2', + inputPlaceholderColor: '#00000066', + inputTextColor: '#333', + suggestionsListBackgroundColor: '#fff', + itemSeparatorColor: '#ddd', + shadowColor: '#00000099', + listItemTextColor: '#333', +} + +type Theme = typeof light + +export const dark: Theme = { + inputBackgroundColor: '#1c1c1e', + inputPlaceholderColor: '#767680', + inputTextColor: '#fff', + suggestionsListBackgroundColor: '#151516', + itemSeparatorColor: '#3e3e41', + shadowColor: '#919aaa5d', + listItemTextColor: '#dbdddf', +} + +export const theme = { light, dark } diff --git a/mobile/components/AutocompleteDropdown-v4.3.1/types/global.d.ts b/mobile/components/AutocompleteDropdown-v4.3.1/types/global.d.ts new file mode 100644 index 000000000..4f6322192 --- /dev/null +++ b/mobile/components/AutocompleteDropdown-v4.3.1/types/global.d.ts @@ -0,0 +1,8 @@ +declare global { + namespace setInterval { + function setInterval(callback: () => void, ms?: number | undefined): NodeJS.Timeout + } + namespace setTimeout { + function setTimeout(callback: () => void, ms?: number | undefined): NodeJS.Timeout + } +} diff --git a/mobile/components/AutocompleteDropdown-v4.3.1/types/index.ts b/mobile/components/AutocompleteDropdown-v4.3.1/types/index.ts new file mode 100644 index 000000000..da696beef --- /dev/null +++ b/mobile/components/AutocompleteDropdown-v4.3.1/types/index.ts @@ -0,0 +1,77 @@ +import type { MutableRefObject } from 'react' +import type React from 'react' +import type { StyleProp, TextInputProps, TextStyle, ViewStyle, FlatListProps, TextInput } from 'react-native' + +export type AutocompleteDropdownItem = { + id: string + title?: string | null +} + +export interface IAutocompleteDropdownRef { + clear: () => void + close: () => void + blur: () => void + open: () => Promise + setInputText: (text: string) => void + toggle: () => void + setItem: (item: AutocompleteDropdownItem) => void +} + +export interface IAutocompleteDropdownProps { + /** + * @example [ + * { id: "1", title: "Alpha" }, + * { id: "2", title: "Beta" }, + * { id: "3", title: "Gamma" } + * ] + */ + dataSet: AutocompleteDropdownItem[] | null + inputHeight?: number + suggestionsListMaxHeight?: number + initialValue?: string | { id: string } | AutocompleteDropdownItem + loading?: boolean + useFilter?: boolean + showClear?: boolean + showChevron?: boolean + closeOnBlur?: boolean + closeOnSubmit?: boolean + clearOnFocus?: boolean + caseSensitive?: boolean + ignoreAccents?: boolean + trimSearchText?: boolean + editable?: boolean + matchFrom?: 'any' | 'start' + debounce?: number + direction?: 'down' | 'up' + position?: 'absolute' | 'relative' + bottomOffset?: number + textInputProps?: TextInputProps + onChangeText?: (text: string) => void + onSelectItem?: (item: AutocompleteDropdownItem | null) => void + renderItem?: (item: AutocompleteDropdownItem, searchText: string) => React.ReactElement | null + onOpenSuggestionsList?: (isOpened: boolean) => void + onClear?: () => void + onChevronPress?: () => void + onRightIconComponentPress?: () => void + onSubmit?: TextInputProps['onSubmitEditing'] + onBlur?: TextInputProps['onBlur'] + onFocus?: TextInputProps['onFocus'] + controller?: + | MutableRefObject + | ((controller: IAutocompleteDropdownRef | null) => void) + containerStyle?: StyleProp + inputContainerStyle?: StyleProp + rightButtonsContainerStyle?: StyleProp + suggestionsListContainerStyle?: StyleProp + suggestionsListTextStyle?: StyleProp + ChevronIconComponent?: React.ReactElement + RightIconComponent?: React.ReactElement + LeftComponent?: React.ReactElement + ClearIconComponent?: React.ReactElement + InputComponent?: React.ComponentType + ItemSeparatorComponent?: React.ComponentType | null + EmptyResultComponent?: React.ReactElement + emptyResultText?: string + flatListProps?: Partial> + ref?: React.LegacyRef | undefined +} diff --git a/mobile/components/AutocompleteDropdown-v4.3.1/useKeyboardHeight.ts b/mobile/components/AutocompleteDropdown-v4.3.1/useKeyboardHeight.ts new file mode 100644 index 000000000..0f5b3ab72 --- /dev/null +++ b/mobile/components/AutocompleteDropdown-v4.3.1/useKeyboardHeight.ts @@ -0,0 +1,17 @@ +import { useEffect, useState } from 'react' +import { Keyboard } from 'react-native' + +export function useKeyboardHeight() { + const [keyboardHeight, setKeyboardHeight] = useState(0) + + useEffect(() => { + const showSubscription = Keyboard.addListener('keyboardDidShow', e => setKeyboardHeight(e.endCoordinates.height)) + const hideSubscription = Keyboard.addListener('keyboardDidHide', () => setKeyboardHeight(0)) + return () => { + showSubscription.remove() + hideSubscription.remove() + } + }, []) + + return keyboardHeight +} diff --git a/mobile/components/Layout.tsx b/mobile/components/Layout.tsx index 984fb2a9f..53e6d263a 100644 --- a/mobile/components/Layout.tsx +++ b/mobile/components/Layout.tsx @@ -9,7 +9,7 @@ import Button from './Button' import i18n from '@/lang/i18n' import * as helper from '@/common/helper' import Header from './Header' -import { AutocompleteDropdownContextProvider } from './AutocompleteDropdown-v4' +// import { AutocompleteDropdownContextProvider } from './AutocompleteDropdown-v4.3.1' interface LayoutProps { navigation: NativeStackNavigationProp @@ -136,7 +136,7 @@ const Layout = ({

{(!loading && ((!user && !strict) || (user && user.verified) ? ( - {children} + children ) : ( {i18n.t('VALIDATE_EMAIL')} diff --git a/mobile/components/LocationSelectList.tsx b/mobile/components/LocationSelectList.tsx index 351f0f4d9..620760f34 100644 --- a/mobile/components/LocationSelectList.tsx +++ b/mobile/components/LocationSelectList.tsx @@ -4,7 +4,7 @@ import { MaterialIcons } from '@expo/vector-icons' import * as env from '@/config/env.config' import * as LocationService from '@/services/LocationService' import * as helper from '@/common/helper' -import { AutocompleteDropdown, AutocompleteDropdownItem } from './AutocompleteDropdown-v4' +import { AutocompleteDropdown, AutocompleteDropdownItem } from './AutocompleteDropdown-v4.3.1' interface LocationSelectListProps { selectedItem?: string