Skip to content

Commit

Permalink
Fix AutocompleteDropdown on iOS
Browse files Browse the repository at this point in the history
  • Loading branch information
aelassas committed Dec 18, 2024
1 parent a806d5c commit 6e944b9
Show file tree
Hide file tree
Showing 16 changed files with 1,772 additions and 8 deletions.
13 changes: 8 additions & 5 deletions mobile/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => ({
Expand Down Expand Up @@ -105,11 +106,13 @@ const App = () => {
<SafeAreaProvider>
<Provider>
<StripeProvider publishableKey={env.STRIPE_PUBLISHABLE_KEY} merchantIdentifier={env.STRIPE_MERCHANT_IDENTIFIER}>
<NavigationContainer ref={navigationRef} onReady={onReady}>
<ExpoStatusBar style="light" backgroundColor="rgba(0, 0, 0, .9)" />
<DrawerNavigator />
<Toast />
</NavigationContainer>
<AutocompleteDropdownContextProvider>
<NavigationContainer ref={navigationRef} onReady={onReady}>
<ExpoStatusBar style="light" backgroundColor="rgba(0, 0, 0, .9)" />
<DrawerNavigator />
<Toast />
</NavigationContainer>
</AutocompleteDropdownContextProvider>
</StripeProvider>
</Provider>
</SafeAreaProvider>
Expand Down
Original file line number Diff line number Diff line change
@@ -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<SetStateAction<ReactElement | undefined>>
direction?: 'up' | 'down'
setDirection: Dispatch<SetStateAction<IAutocompleteDropdownContext['direction']>>
activeInputContainerRef?: MutableRefObject<View | null>
activeControllerRef?: MutableRefObject<IAutocompleteDropdownRef | null>
controllerRefs?: MutableRefObject<IAutocompleteDropdownRef[]>
}

export interface IAutocompleteDropdownContextProviderProps {
headerOffset?: number
children: React.ReactNode
}

export const AutocompleteDropdownContext = React.createContext<IAutocompleteDropdownContext>({
content: undefined,
setContent: () => null,
direction: undefined,
setDirection: () => null,
activeInputContainerRef: undefined,
activeControllerRef: undefined,
controllerRefs: undefined,
})

export const AutocompleteDropdownContextProvider: FC<IAutocompleteDropdownContextProviderProps> = ({
headerOffset = 0,
children,
}) => {
const [content, setContent] = useState<IAutocompleteDropdownContext['content']>()
const [direction, setDirection] = useState<IAutocompleteDropdownContext['direction']>(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<View>(null)
const wrapperRef = useRef<View>(null)
const activeControllerRef = useRef<IAutocompleteDropdownRef | null>(null)
const controllerRefs = useRef<IAutocompleteDropdownRef[]>([])
const positionTrackingIntervalRef = useRef<NodeJS.Timeout>()

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 =>

Check failure on line 88 in mobile/components/AutocompleteDropdown-v4.3.1/AutocompleteDropdownContext.tsx

View workflow job for this annotation

GitHub Actions / build (lts/*)

Arrow function used ambiguously with a conditional expression

Check failure on line 88 in mobile/components/AutocompleteDropdown-v4.3.1/AutocompleteDropdownContext.tsx

View workflow job for this annotation

GitHub Actions / build (lts/*)

Expected parentheses around arrow function argument

Check failure on line 88 in mobile/components/AutocompleteDropdown-v4.3.1/AutocompleteDropdownContext.tsx

View workflow job for this annotation

GitHub Actions / build (lts/*)

Arrow function used ambiguously with a conditional expression

Check failure on line 88 in mobile/components/AutocompleteDropdown-v4.3.1/AutocompleteDropdownContext.tsx

View workflow job for this annotation

GitHub Actions / build (lts/*)

Expected parentheses around arrow function argument
JSON.stringify(prev) === JSON.stringify(currentMeasurement) ? prev : currentMeasurement,
)

Check failure on line 90 in mobile/components/AutocompleteDropdown-v4.3.1/AutocompleteDropdownContext.tsx

View workflow job for this annotation

GitHub Actions / build (lts/*)

Unexpected newline before ')'

Check failure on line 90 in mobile/components/AutocompleteDropdown-v4.3.1/AutocompleteDropdownContext.tsx

View workflow job for this annotation

GitHub Actions / build (lts/*)

Unexpected newline before ')'
showAfterCalculation && setShow(true)

Check failure on line 91 in mobile/components/AutocompleteDropdown-v4.3.1/AutocompleteDropdownContext.tsx

View workflow job for this annotation

GitHub Actions / build (lts/*)

Expected an assignment or function call and instead saw an expression

Check failure on line 91 in mobile/components/AutocompleteDropdown-v4.3.1/AutocompleteDropdownContext.tsx

View workflow job for this annotation

GitHub Actions / build (lts/*)

Expected an assignment or function call and instead saw an expression
})
})
}, [])

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 (
<AutocompleteDropdownContext.Provider
value={{
content,
setContent,
activeInputContainerRef,
direction,
setDirection,
activeControllerRef,
controllerRefs,
}}>
<View
ref={wrapperRef}
style={styles.clickOutsideHandlerArea}
onTouchEnd={() => {
activeControllerRef.current?.close()
activeControllerRef.current?.blur()
}}>
{children}
</View>
{!!content && show && (
<View
onLayout={onLayout}
style={{
...styles.wrapper,
opacity,
...contentStyles,
}}>
{content}
</View>
)}
</AutocompleteDropdownContext.Provider>
)
}

const styles = StyleSheet.create({
clickOutsideHandlerArea: {
flex: 1,
},
wrapper: {
position: 'absolute',
},
})
82 changes: 82 additions & 0 deletions mobile/components/AutocompleteDropdown-v4.3.1/Dropdown.tsx
Original file line number Diff line number Diff line change
@@ -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<IAutocompleteDropdownProps, 'renderItem' | 'ref'> {
ListEmptyComponent: JSX.Element
renderItem: ListRenderItem<AutocompleteDropdownItem>
}

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 <View style={styles.itemSeparator} />
}, [styles.itemSeparator])

return (
<Animatable.View
useNativeDriver
animation={direction === 'up' ? fadeInUpShort : fadeInDownShort}
easing="ease-out-quad"
delay={direction === 'up' ? 150 : 0}
duration={150}
style={{
...styles.listContainer,
...(rest.suggestionsListContainerStyle as object),
}}>
<FlatList
keyboardDismissMode="on-drag"
keyboardShouldPersistTaps="handled"
nestedScrollEnabled={true}
data={dataSet}
style={{ maxHeight: suggestionsListMaxHeight }}
renderItem={renderItem}
keyExtractor={(item) => item.id}
ListEmptyComponent={ListEmptyComponent}
ItemSeparatorComponent={ItemSeparatorComponent ?? defaultItemSeparator}
{...rest.flatListProps}
/>
</Animatable.View>
)
})

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,
},
})
Original file line number Diff line number Diff line change
@@ -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 = <P extends object>(
WrappedComponent: ComponentType<P>,
{ containerStyle }: WithFadeAnimationProps = {},
): FC<P> => {

Check failure on line 13 in mobile/components/AutocompleteDropdown-v4.3.1/HOC/withFadeAnimation.tsx

View workflow job for this annotation

GitHub Actions / build (lts/*)

Unexpected block statement surrounding arrow body; move the returned value immediately after the `=>`

Check failure on line 13 in mobile/components/AutocompleteDropdown-v4.3.1/HOC/withFadeAnimation.tsx

View workflow job for this annotation

GitHub Actions / build (lts/*)

Unexpected block statement surrounding arrow body; move the returned value immediately after the `=>`
return (props: P) => {

Check warning on line 14 in mobile/components/AutocompleteDropdown-v4.3.1/HOC/withFadeAnimation.tsx

View workflow job for this annotation

GitHub Actions / build (lts/*)

Function component is not a function expression

Check failure on line 14 in mobile/components/AutocompleteDropdown-v4.3.1/HOC/withFadeAnimation.tsx

View workflow job for this annotation

GitHub Actions / build (lts/*)

Component definition is missing display name

Check warning on line 14 in mobile/components/AutocompleteDropdown-v4.3.1/HOC/withFadeAnimation.tsx

View workflow job for this annotation

GitHub Actions / build (lts/*)

Function component is not a function expression

Check failure on line 14 in mobile/components/AutocompleteDropdown-v4.3.1/HOC/withFadeAnimation.tsx

View workflow job for this annotation

GitHub Actions / build (lts/*)

Component definition is missing display name
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 (
<Animated.View style={[containerStyle, { opacity: opacityAnimationValue }]}>
<WrappedComponent {...props} />
</Animated.View>
)
}
}
27 changes: 27 additions & 0 deletions mobile/components/AutocompleteDropdown-v4.3.1/NothingFound.tsx
Original file line number Diff line number Diff line change
@@ -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<NothingFoundProps> = memo(({ ...props }) => {

Check failure on line 10 in mobile/components/AutocompleteDropdown-v4.3.1/NothingFound.tsx

View workflow job for this annotation

GitHub Actions / build (lts/*)

Component definition is missing display name

Check failure on line 10 in mobile/components/AutocompleteDropdown-v4.3.1/NothingFound.tsx

View workflow job for this annotation

GitHub Actions / build (lts/*)

Component definition is missing display name
const EL = withFadeAnimation(
() => (
<View style={{ ...styles.container }}>
<Text style={styles.text}>{props.emptyResultText || 'Nothing found'}</Text>
</View>
),
{},
)
return <EL />
})

const styles = StyleSheet.create({
container: {
padding: 10,
},
text: { textAlign: 'center' },
})
Loading

0 comments on commit 6e944b9

Please sign in to comment.