let's roll with custom bottom sheet for now
This commit is contained in:
@@ -152,6 +152,7 @@
|
|||||||
"react-responsive": "^9.0.2",
|
"react-responsive": "^9.0.2",
|
||||||
"rn-fetch-blob": "^0.12.0",
|
"rn-fetch-blob": "^0.12.0",
|
||||||
"sentry-expo": "~7.0.0",
|
"sentry-expo": "~7.0.0",
|
||||||
|
"smitter": "^1.1.1",
|
||||||
"tippy.js": "^6.3.7",
|
"tippy.js": "^6.3.7",
|
||||||
"tlds": "^1.234.0",
|
"tlds": "^1.234.0",
|
||||||
"zeego": "^1.6.2",
|
"zeego": "^1.6.2",
|
||||||
|
|||||||
@@ -7,16 +7,18 @@ import {
|
|||||||
Platform,
|
Platform,
|
||||||
Pressable,
|
Pressable,
|
||||||
ScrollView,
|
ScrollView,
|
||||||
|
TextInput,
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
import {
|
import {
|
||||||
FontAwesomeIcon,
|
FontAwesomeIcon,
|
||||||
FontAwesomeIconStyle,
|
FontAwesomeIconStyle,
|
||||||
} from '@fortawesome/react-native-fontawesome'
|
} from '@fortawesome/react-native-fontawesome'
|
||||||
import BottomSheet, {
|
|
||||||
BottomSheetBackdrop,
|
|
||||||
BottomSheetTextInput,
|
|
||||||
} from '@gorhom/bottom-sheet'
|
|
||||||
|
|
||||||
|
import {
|
||||||
|
useSheet,
|
||||||
|
Sheet as BottomSheet,
|
||||||
|
Backdrop as BottomSheetBackdrop,
|
||||||
|
} from 'view/com/util/BottomSheet'
|
||||||
import {Portal} from 'view/com/util/Portal'
|
import {Portal} from 'view/com/util/Portal'
|
||||||
import {TagsAutocompleteModel} from 'state/models/ui/tags-autocomplete'
|
import {TagsAutocompleteModel} from 'state/models/ui/tags-autocomplete'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
@@ -43,15 +45,31 @@ export function TagInput({
|
|||||||
}) {
|
}) {
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
const model = React.useMemo(() => new TagsAutocompleteModel(store), [store])
|
const model = React.useMemo(() => new TagsAutocompleteModel(store), [store])
|
||||||
const sheet = React.useRef<BottomSheet>(null)
|
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const input = React.useRef<HTMLInputElement>(null)
|
const input = React.useRef<TextInput>(null)
|
||||||
|
|
||||||
const [value, setValue] = React.useState('')
|
const [value, setValue] = React.useState('')
|
||||||
const [tags, setTags] = React.useState<string[]>([])
|
const [tags, setTags] = React.useState<string[]>([])
|
||||||
const [suggestions, setSuggestions] = React.useState<string[]>([])
|
const [suggestions, setSuggestions] = React.useState<string[]>([])
|
||||||
const [isInitialLoad, setIsInitialLoad] = React.useState(true)
|
const [isInitialLoad, setIsInitialLoad] = React.useState(true)
|
||||||
|
|
||||||
|
const sheet = useSheet({
|
||||||
|
index: 0,
|
||||||
|
snaps: [0, '90%'],
|
||||||
|
async onStateChange(state) {
|
||||||
|
if (state.index > 0) {
|
||||||
|
model.setActive(true)
|
||||||
|
await model.search('') // get default results
|
||||||
|
setSuggestions(model.suggestions)
|
||||||
|
setIsInitialLoad(false)
|
||||||
|
} else {
|
||||||
|
reset()
|
||||||
|
setIsInitialLoad(true)
|
||||||
|
input.current?.blur()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
const reset = React.useCallback(() => {
|
const reset = React.useCallback(() => {
|
||||||
setValue('')
|
setValue('')
|
||||||
model.setActive(false)
|
model.setActive(false)
|
||||||
@@ -83,10 +101,7 @@ export function TagInput({
|
|||||||
addTags(uniq([...tags, tag]).slice(0, max))
|
addTags(uniq([...tags, tag]).slice(0, max))
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(() => {
|
setValue('')
|
||||||
setValue('')
|
|
||||||
input.current?.focus()
|
|
||||||
}, 1)
|
|
||||||
},
|
},
|
||||||
[max, tags, setValue, addTags],
|
[max, tags, setValue, addTags],
|
||||||
)
|
)
|
||||||
@@ -102,8 +117,10 @@ export function TagInput({
|
|||||||
if (key === 'Backspace' && value === '') {
|
if (key === 'Backspace' && value === '') {
|
||||||
addTags(tags.slice(0, -1))
|
addTags(tags.slice(0, -1))
|
||||||
} else if (key === ' ') {
|
} else if (key === ' ') {
|
||||||
e.preventDefault() // prevents an additional space on web
|
|
||||||
addTagAndReset(value)
|
addTagAndReset(value)
|
||||||
|
setTimeout(() => {
|
||||||
|
setValue('')
|
||||||
|
}, 1)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[value, tags, addTags, addTagAndReset],
|
[value, tags, addTags, addTagAndReset],
|
||||||
@@ -127,31 +144,17 @@ export function TagInput({
|
|||||||
[model, setValue],
|
[model, setValue],
|
||||||
)
|
)
|
||||||
|
|
||||||
const onCloseSheet = React.useCallback(() => {
|
const openSheet = React.useCallback(() => {
|
||||||
reset()
|
sheet.index = 1
|
||||||
setIsInitialLoad(true)
|
input.current?.focus()
|
||||||
}, [reset, setIsInitialLoad])
|
}, [sheet])
|
||||||
|
|
||||||
const onSheetChange = React.useCallback(
|
|
||||||
async (index: number) => {
|
|
||||||
if (index > -1) {
|
|
||||||
model.setActive(true)
|
|
||||||
await model.search('') // get default results
|
|
||||||
setSuggestions(model.suggestions)
|
|
||||||
setIsInitialLoad(false)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[model, setIsInitialLoad, setSuggestions],
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
<Pressable
|
<Pressable
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
style={styles.selectedTags}
|
style={styles.selectedTags}
|
||||||
onPress={() => {
|
onPress={openSheet}>
|
||||||
sheet.current?.snapToIndex(0)
|
|
||||||
}}>
|
|
||||||
<View style={[pal.viewLight, styles.button]}>
|
<View style={[pal.viewLight, styles.button]}>
|
||||||
{tags.length ? (
|
{tags.length ? (
|
||||||
<Text type="md-medium" style={[pal.textLight]}>
|
<Text type="md-medium" style={[pal.textLight]}>
|
||||||
@@ -181,76 +184,61 @@ export function TagInput({
|
|||||||
</Pressable>
|
</Pressable>
|
||||||
|
|
||||||
<Portal>
|
<Portal>
|
||||||
<BottomSheet
|
<BottomSheet sheet={sheet}>
|
||||||
ref={sheet}
|
<BottomSheetBackdrop sheet={sheet} />
|
||||||
index={-1}
|
|
||||||
snapPoints={['90%']}
|
|
||||||
enablePanDownToClose
|
|
||||||
keyboardBehavior="extend"
|
|
||||||
backgroundStyle={{backgroundColor: 'transparent'}}
|
|
||||||
android_keyboardInputMode="adjustResize"
|
|
||||||
backdropComponent={props => (
|
|
||||||
<BottomSheetBackdrop
|
|
||||||
appearsOnIndex={0}
|
|
||||||
disappearsOnIndex={-1}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
handleIndicatorStyle={{backgroundColor: pal.text.color}}
|
|
||||||
handleStyle={{display: 'none'}}
|
|
||||||
onChange={onSheetChange}
|
|
||||||
onClose={onCloseSheet}>
|
|
||||||
<Sheet.Outer>
|
<Sheet.Outer>
|
||||||
<Sheet.Handle />
|
<Sheet.Handle />
|
||||||
|
|
||||||
<View style={styles.outer}>
|
<Sheet.Content>
|
||||||
<FontAwesomeIcon
|
<View style={styles.outer}>
|
||||||
icon="tags"
|
<FontAwesomeIcon
|
||||||
size={14}
|
icon="tags"
|
||||||
style={pal.textLight as FontAwesomeIconStyle}
|
size={14}
|
||||||
/>
|
style={pal.textLight as FontAwesomeIconStyle}
|
||||||
|
/>
|
||||||
|
|
||||||
{tags.map(tag => (
|
{tags.map(tag => (
|
||||||
<TagButton key={tag} value={tag} onClick={removeTag} />
|
<TagButton key={tag} value={tag} onClick={removeTag} />
|
||||||
))}
|
))}
|
||||||
|
|
||||||
<BottomSheetTextInput
|
<TextInput
|
||||||
autoCapitalize="none"
|
ref={input}
|
||||||
autoComplete="off"
|
blurOnSubmit={false}
|
||||||
placeholder="Add tags..."
|
autoCapitalize="none"
|
||||||
value={value}
|
autoComplete="off"
|
||||||
style={[
|
placeholder="Add tags..."
|
||||||
styles.input,
|
value={value}
|
||||||
{
|
style={[styles.input, {}]}
|
||||||
placeholderTextColor: pal.textLight.color,
|
onChangeText={onChangeText}
|
||||||
},
|
onKeyPress={onKeyPress}
|
||||||
]}
|
onSubmitEditing={onSubmitEditing}
|
||||||
onChangeText={onChangeText}
|
accessibilityHint="Add tags to your post"
|
||||||
onKeyPress={onKeyPress}
|
accessibilityLabel="Add tags to your post"
|
||||||
onSubmitEditing={onSubmitEditing}
|
/>
|
||||||
/>
|
</View>
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={{marginHorizontal: -20}}>
|
<View style={{marginHorizontal: -20}}>
|
||||||
<ScrollView horizontal>
|
<ScrollView horizontal>
|
||||||
<View style={styles.suggestions}>
|
<View style={styles.suggestions}>
|
||||||
{isInitialLoad && <ActivityIndicator />}
|
{isInitialLoad && <ActivityIndicator />}
|
||||||
|
|
||||||
{suggestions
|
{suggestions
|
||||||
.filter(s => !tags.find(t => t === s))
|
.filter(s => !tags.find(t => t === s))
|
||||||
.map(suggestion => {
|
.map(suggestion => {
|
||||||
return (
|
return (
|
||||||
<TagButton
|
<TagButton
|
||||||
key={suggestion}
|
key={suggestion}
|
||||||
icon="plus"
|
icon="plus"
|
||||||
value={suggestion}
|
value={suggestion}
|
||||||
onClick={addTagAndReset}
|
onClick={addTagAndReset}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</View>
|
</View>
|
||||||
|
</Sheet.Content>
|
||||||
</Sheet.Outer>
|
</Sheet.Outer>
|
||||||
</BottomSheet>
|
</BottomSheet>
|
||||||
</Portal>
|
</Portal>
|
||||||
|
|||||||
@@ -10,11 +10,15 @@ export function Outer(props: React.PropsWithChildren<{}>) {
|
|||||||
<>
|
<>
|
||||||
<View style={[pal.view, styles.background]} />
|
<View style={[pal.view, styles.background]} />
|
||||||
|
|
||||||
<View style={styles.content}>{props.children}</View>
|
{props.children}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function Content(props: React.PropsWithChildren<{}>) {
|
||||||
|
return <View style={styles.content}>{props.children}</View>
|
||||||
|
}
|
||||||
|
|
||||||
export function Handle() {
|
export function Handle() {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
return (
|
return (
|
||||||
@@ -28,7 +32,7 @@ const styles = StyleSheet.create({
|
|||||||
borderTopLeftRadius: 40,
|
borderTopLeftRadius: 40,
|
||||||
borderTopRightRadius: 40,
|
borderTopRightRadius: 40,
|
||||||
height: Dimensions.get('window').height * 2,
|
height: Dimensions.get('window').height * 2,
|
||||||
zIndex: -1,
|
zIndex: 1,
|
||||||
},
|
},
|
||||||
content: {
|
content: {
|
||||||
paddingVertical: 40,
|
paddingVertical: 40,
|
||||||
@@ -36,6 +40,7 @@ const styles = StyleSheet.create({
|
|||||||
borderTopLeftRadius: 40,
|
borderTopLeftRadius: 40,
|
||||||
borderTopRightRadius: 40,
|
borderTopRightRadius: 40,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
|
zIndex: 2,
|
||||||
},
|
},
|
||||||
handle: {
|
handle: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
@@ -44,5 +49,6 @@ const styles = StyleSheet.create({
|
|||||||
width: 80,
|
width: 80,
|
||||||
height: 6,
|
height: 6,
|
||||||
borderRadius: 10,
|
borderRadius: 10,
|
||||||
|
zIndex: 2,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,394 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import {View, Dimensions, Pressable} from 'react-native'
|
||||||
|
import {Gesture, GestureDetector} from 'react-native-gesture-handler'
|
||||||
|
import Animated, {
|
||||||
|
useSharedValue,
|
||||||
|
useAnimatedStyle,
|
||||||
|
withTiming,
|
||||||
|
Easing,
|
||||||
|
runOnJS,
|
||||||
|
} from 'react-native-reanimated'
|
||||||
|
import {smitter} from 'smitter'
|
||||||
|
|
||||||
|
type BottomSheetState = {
|
||||||
|
index: number
|
||||||
|
minIndex: number
|
||||||
|
maxIndex: number
|
||||||
|
position: number
|
||||||
|
pinned: boolean
|
||||||
|
offset: number
|
||||||
|
snaps: number[]
|
||||||
|
}
|
||||||
|
|
||||||
|
type BottomSheetProps = {
|
||||||
|
sheet: ReturnType<typeof useSheet>
|
||||||
|
}
|
||||||
|
|
||||||
|
type InternalEvents = {
|
||||||
|
syncState: BottomSheetState
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSheet({
|
||||||
|
index: initialIndex = 0,
|
||||||
|
minIndex: initialMinIndex = 0,
|
||||||
|
snaps,
|
||||||
|
onStateChange,
|
||||||
|
}: {
|
||||||
|
index?: number
|
||||||
|
minIndex?: number
|
||||||
|
maxIndex?: number
|
||||||
|
snaps: (number | string)[]
|
||||||
|
onStateChange?: (state: BottomSheetState) => void
|
||||||
|
}) {
|
||||||
|
const internal = React.useMemo(() => smitter<InternalEvents>(), [])
|
||||||
|
const dimensions = React.useMemo(() => Dimensions.get('window'), []) // TODO needs change?
|
||||||
|
const snapPoints = React.useMemo(() => {
|
||||||
|
return snaps.map(p => {
|
||||||
|
const px =
|
||||||
|
typeof p === 'number' ? p : (parseInt(p) / 100) * dimensions.height
|
||||||
|
return px
|
||||||
|
})
|
||||||
|
}, [snaps, dimensions.height])
|
||||||
|
|
||||||
|
const index = React.useRef(initialIndex)
|
||||||
|
const minIndex = React.useRef(Math.max(initialMinIndex, 0))
|
||||||
|
const maxIndex = React.useRef(snaps.length - 1)
|
||||||
|
const position = React.useRef(
|
||||||
|
index.current > -1 ? snapPoints[index.current] : 0,
|
||||||
|
)
|
||||||
|
const pinned = React.useRef(false)
|
||||||
|
const offset = React.useRef(0)
|
||||||
|
|
||||||
|
const getState = React.useCallback(() => {
|
||||||
|
return {
|
||||||
|
index: index.current,
|
||||||
|
minIndex: minIndex.current,
|
||||||
|
maxIndex: maxIndex.current,
|
||||||
|
position: position.current,
|
||||||
|
pinned: pinned.current,
|
||||||
|
offset: offset.current,
|
||||||
|
snaps: snapPoints,
|
||||||
|
}
|
||||||
|
}, [snapPoints])
|
||||||
|
|
||||||
|
const syncState = React.useCallback(
|
||||||
|
(state: Partial<BottomSheetState>) => {
|
||||||
|
if (state.minIndex !== undefined) {
|
||||||
|
minIndex.current = Math.max(state.minIndex, 0)
|
||||||
|
}
|
||||||
|
if (state.maxIndex !== undefined) {
|
||||||
|
maxIndex.current = Math.min(state.maxIndex, snapPoints.length - 1)
|
||||||
|
}
|
||||||
|
if (state.index !== undefined) {
|
||||||
|
index.current = Math.max(
|
||||||
|
Math.min(state.index, maxIndex.current),
|
||||||
|
minIndex.current,
|
||||||
|
)
|
||||||
|
position.current = snapPoints[index.current]
|
||||||
|
}
|
||||||
|
if (state.position !== undefined) {
|
||||||
|
position.current = Math.max(
|
||||||
|
Math.min(state.position, snapPoints[maxIndex.current]),
|
||||||
|
snapPoints[minIndex.current],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (state.pinned !== undefined) {
|
||||||
|
pinned.current = state.pinned
|
||||||
|
}
|
||||||
|
if (state.offset !== undefined) {
|
||||||
|
offset.current = state.offset
|
||||||
|
}
|
||||||
|
|
||||||
|
onStateChange?.(getState())
|
||||||
|
},
|
||||||
|
[getState, onStateChange, snapPoints],
|
||||||
|
)
|
||||||
|
|
||||||
|
const setState = React.useCallback(
|
||||||
|
(state: Partial<BottomSheetState>) => {
|
||||||
|
syncState(state)
|
||||||
|
internal.emit('syncState', getState())
|
||||||
|
},
|
||||||
|
[syncState, getState, internal],
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
get state() {
|
||||||
|
return getState()
|
||||||
|
},
|
||||||
|
open() {
|
||||||
|
setState({
|
||||||
|
index: 1,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
close() {
|
||||||
|
setState({
|
||||||
|
index: 0,
|
||||||
|
minIndex: 0,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
set index(value: number) {
|
||||||
|
setState({index: value})
|
||||||
|
},
|
||||||
|
get index() {
|
||||||
|
return index.current
|
||||||
|
},
|
||||||
|
set position(value: number | string) {
|
||||||
|
const position =
|
||||||
|
typeof value === 'number'
|
||||||
|
? value
|
||||||
|
: (parseInt(value) / 100) * dimensions.height
|
||||||
|
setState({position})
|
||||||
|
},
|
||||||
|
get position() {
|
||||||
|
return position.current
|
||||||
|
},
|
||||||
|
set minIndex(index: number) {
|
||||||
|
setState({minIndex: index})
|
||||||
|
},
|
||||||
|
get minIndex() {
|
||||||
|
return minIndex.current
|
||||||
|
},
|
||||||
|
set maxIndex(index: number) {
|
||||||
|
setState({maxIndex: index})
|
||||||
|
},
|
||||||
|
get maxIndex() {
|
||||||
|
return maxIndex.current
|
||||||
|
},
|
||||||
|
set pinned(value: boolean) {
|
||||||
|
setState({pinned: value})
|
||||||
|
},
|
||||||
|
get pinned() {
|
||||||
|
return pinned.current
|
||||||
|
},
|
||||||
|
set offset(offset: number) {
|
||||||
|
setState({offset})
|
||||||
|
},
|
||||||
|
get offset() {
|
||||||
|
return offset.current
|
||||||
|
},
|
||||||
|
events: {
|
||||||
|
internal,
|
||||||
|
},
|
||||||
|
_syncState: syncState,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Sheet({
|
||||||
|
children,
|
||||||
|
sheet,
|
||||||
|
}: React.PropsWithChildren<BottomSheetProps>) {
|
||||||
|
const state = useSharedValue(sheet.state)
|
||||||
|
state.value = sheet.state
|
||||||
|
|
||||||
|
const {index, snaps} = state.value
|
||||||
|
|
||||||
|
const dimensions = React.useMemo(() => Dimensions.get('window'), [])
|
||||||
|
|
||||||
|
const top = useSharedValue(index > -1 ? snaps[index] : 0)
|
||||||
|
const animatedSheetStyles = useAnimatedStyle(() => ({
|
||||||
|
transform: [{translateY: -top.value}],
|
||||||
|
}))
|
||||||
|
|
||||||
|
const offset = useSharedValue(dimensions.height)
|
||||||
|
const animatedOuterStyles = useAnimatedStyle(() => ({
|
||||||
|
transform: [{translateY: offset.value}],
|
||||||
|
}))
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
function goToPosition(pos: number) {
|
||||||
|
top.value = withTiming(pos, {
|
||||||
|
duration: 500,
|
||||||
|
easing: Easing.out(Easing.exp),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
sheet.events.internal.on('syncState', s => {
|
||||||
|
if (state.value.index != s.index) {
|
||||||
|
const pos = index > -1 ? snaps[s.index] : 0
|
||||||
|
goToPosition(pos)
|
||||||
|
}
|
||||||
|
if (state.value.position != s.position) {
|
||||||
|
goToPosition(s.position)
|
||||||
|
}
|
||||||
|
if (state.value.offset != s.offset) {
|
||||||
|
offset.value = withTiming(dimensions.height - s.offset, {
|
||||||
|
duration: 500,
|
||||||
|
easing: Easing.out(Easing.exp),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
state.value = s
|
||||||
|
})
|
||||||
|
}, [
|
||||||
|
snaps,
|
||||||
|
dimensions.height,
|
||||||
|
index,
|
||||||
|
offset,
|
||||||
|
sheet.events.internal,
|
||||||
|
state,
|
||||||
|
top,
|
||||||
|
])
|
||||||
|
|
||||||
|
const pan = Gesture.Pan()
|
||||||
|
.onChange(e => {
|
||||||
|
top.value = top.value - e.changeY
|
||||||
|
})
|
||||||
|
.onFinalize(e => {
|
||||||
|
// ignore taps
|
||||||
|
if (Math.abs(e.translationY) < 5) return
|
||||||
|
|
||||||
|
let y = top.value // from the bottom
|
||||||
|
|
||||||
|
const dir = e.velocityY > 0 ? 1 : -1
|
||||||
|
let v = Math.abs(e.velocityY) / 100
|
||||||
|
|
||||||
|
let decayDistance = 0
|
||||||
|
while (v > 0.1) {
|
||||||
|
v *= 1 - 0.15
|
||||||
|
decayDistance += v
|
||||||
|
}
|
||||||
|
|
||||||
|
decayDistance = decayDistance * dir
|
||||||
|
y = y - decayDistance
|
||||||
|
|
||||||
|
let {index, minIndex, maxIndex, position, pinned} = state.value
|
||||||
|
let nextPosition = position
|
||||||
|
|
||||||
|
if (!pinned) {
|
||||||
|
for (let i = index; i < snaps.length; i++) {
|
||||||
|
const lower = snaps[i - 1] || snaps[0]
|
||||||
|
const curr = snaps[i]
|
||||||
|
const upper = snaps[i + 1] || snaps[snaps.length - 1]
|
||||||
|
|
||||||
|
const lowerThreshold = (curr - lower) / 2 + lower
|
||||||
|
const upperThreshold = (upper - curr) / 2 + curr
|
||||||
|
|
||||||
|
if (y < curr && y < lowerThreshold) {
|
||||||
|
index = Math.max(i - 1, minIndex)
|
||||||
|
break
|
||||||
|
} else if (
|
||||||
|
(y <= curr && // less than current snap point
|
||||||
|
y > lowerThreshold) || // more than half way to current snap point
|
||||||
|
(y >= curr && // more than current snap point
|
||||||
|
y < upperThreshold) // less than half way to upper snap point
|
||||||
|
) {
|
||||||
|
index = i
|
||||||
|
break
|
||||||
|
} else if (
|
||||||
|
y > upper && // less than upper snap point
|
||||||
|
y > upperThreshold // more than current snap point
|
||||||
|
) {
|
||||||
|
index = Math.min(i + 1, maxIndex)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nextPosition = index > 0 ? snaps[index] : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
top.value = withTiming(nextPosition, {
|
||||||
|
duration: 500,
|
||||||
|
easing: Easing.out(Easing.exp),
|
||||||
|
})
|
||||||
|
|
||||||
|
// update UI thread state
|
||||||
|
state.value = {
|
||||||
|
...state.value,
|
||||||
|
index,
|
||||||
|
position: nextPosition,
|
||||||
|
}
|
||||||
|
|
||||||
|
// update JS thread state without cyclical emit
|
||||||
|
runOnJS(sheet._syncState)({index, position: nextPosition})
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Animated.View
|
||||||
|
style={[
|
||||||
|
{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
zIndex: 9999,
|
||||||
|
},
|
||||||
|
animatedOuterStyles,
|
||||||
|
]}>
|
||||||
|
<GestureDetector gesture={pan}>
|
||||||
|
<Animated.View
|
||||||
|
style={[
|
||||||
|
{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
},
|
||||||
|
animatedSheetStyles,
|
||||||
|
]}>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
{
|
||||||
|
zIndex: 1,
|
||||||
|
height: snaps[snaps.length - 1],
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
{children}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
zIndex: 0,
|
||||||
|
height: dimensions.height * 2,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Animated.View>
|
||||||
|
</GestureDetector>
|
||||||
|
</Animated.View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Backdrop({sheet}: {sheet: ReturnType<typeof useSheet>}) {
|
||||||
|
const active = sheet.position > 0
|
||||||
|
const opacity = useSharedValue(0)
|
||||||
|
const style = useAnimatedStyle(() => ({
|
||||||
|
position: 'absolute',
|
||||||
|
top: '-200%',
|
||||||
|
bottom: '-200%',
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
backgroundColor: '#000',
|
||||||
|
zIndex: 0,
|
||||||
|
opacity: opacity.value,
|
||||||
|
display: opacity.value > 0 ? 'flex' : 'none',
|
||||||
|
}))
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
opacity.value = withTiming(active ? 0.5 : 0, {
|
||||||
|
duration: 500,
|
||||||
|
easing: Easing.out(Easing.exp),
|
||||||
|
})
|
||||||
|
}, [active, opacity])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Animated.View style={style}>
|
||||||
|
<Pressable
|
||||||
|
accessibilityHint="Click here to close the bottom sheet"
|
||||||
|
accessibilityLabel="Click here to close the bottom sheet"
|
||||||
|
onPress={() => sheet.close()}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Animated.View>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -16985,6 +16985,11 @@ slugify@^1.3.4:
|
|||||||
resolved "https://registry.yarnpkg.com/slugify/-/slugify-1.6.6.tgz#2d4ac0eacb47add6af9e04d3be79319cbcc7924b"
|
resolved "https://registry.yarnpkg.com/slugify/-/slugify-1.6.6.tgz#2d4ac0eacb47add6af9e04d3be79319cbcc7924b"
|
||||||
integrity sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==
|
integrity sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==
|
||||||
|
|
||||||
|
smitter@^1.1.1:
|
||||||
|
version "1.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/smitter/-/smitter-1.1.1.tgz#cade535ccd3b2cc8ad274a9fe9b02937f50a316f"
|
||||||
|
integrity sha512-6AwxCy1VfHVBpCljZb/QCGUcRmZKL6s3o5NRjJfJKAQxtiC8GCJUpy1OFs3RcJinykoj/p7jIkPrM3Z3bYmgZg==
|
||||||
|
|
||||||
sockjs@^0.3.24:
|
sockjs@^0.3.24:
|
||||||
version "0.3.24"
|
version "0.3.24"
|
||||||
resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.24.tgz#c9bc8995f33a111bea0395ec30aa3206bdb5ccce"
|
resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.24.tgz#c9bc8995f33a111bea0395ec30aa3206bdb5ccce"
|
||||||
|
|||||||
Reference in New Issue
Block a user