bsky-app/src/view/com/util/ViewSelector.tsx
2023-10-13 15:25:16 +01:00

229 lines
5.6 KiB
TypeScript

import React, {useEffect, useState} from 'react'
import {
Pressable,
RefreshControl,
StyleSheet,
View,
ScrollView,
} from 'react-native'
import {FlatList} from './Views'
import {OnScrollCb} from 'lib/hooks/useOnMainScroll'
import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle'
import {Text} from './text/Text'
import {usePalette} from 'lib/hooks/usePalette'
import {clamp} from 'lib/numbers'
import {s, colors} from 'lib/styles'
import {isAndroid} from 'platform/detection'
const HEADER_ITEM = {_reactKey: '__header__'}
const SELECTOR_ITEM = {_reactKey: '__selector__'}
const STICKY_HEADER_INDICES = [1]
export type ViewSelectorHandle = {
scrollToTop: () => void
}
export const ViewSelector = React.forwardRef<
ViewSelectorHandle,
{
sections: string[]
items: any[]
refreshing?: boolean
swipeEnabled?: boolean
renderHeader?: () => JSX.Element
renderItem: (item: any) => JSX.Element
ListFooterComponent?:
| React.ComponentType<any>
| React.ReactElement
| null
| undefined
onSelectView?: (viewIndex: number) => void
onScroll?: OnScrollCb
onRefresh?: () => void
onEndReached?: (info: {distanceFromEnd: number}) => void
}
>(function ViewSelectorImpl(
{
sections,
items,
refreshing,
renderHeader,
renderItem,
ListFooterComponent,
onSelectView,
onScroll,
onRefresh,
onEndReached,
},
ref,
) {
const pal = usePalette('default')
const [selectedIndex, setSelectedIndex] = useState<number>(0)
const flatListRef = React.useRef<FlatList>(null)
// events
// =
const keyExtractor = React.useCallback((item: any) => item._reactKey, [])
const onPressSelection = React.useCallback(
(index: number) => setSelectedIndex(clamp(index, 0, sections.length)),
[setSelectedIndex, sections],
)
useEffect(() => {
onSelectView?.(selectedIndex)
}, [selectedIndex, onSelectView])
React.useImperativeHandle(ref, () => ({
scrollToTop: () => {
flatListRef.current?.scrollToOffset({offset: 0})
},
}))
// rendering
// =
const renderItemInternal = React.useCallback(
({item}: {item: any}) => {
if (item === HEADER_ITEM) {
if (renderHeader) {
return renderHeader()
}
return <View />
} else if (item === SELECTOR_ITEM) {
return (
<Selector
items={sections}
selectedIndex={selectedIndex}
onSelect={onPressSelection}
/>
)
} else {
return renderItem(item)
}
},
[sections, selectedIndex, onPressSelection, renderHeader, renderItem],
)
const data = React.useMemo(
() => [HEADER_ITEM, SELECTOR_ITEM, ...items],
[items],
)
return (
<FlatList
ref={flatListRef}
data={data}
keyExtractor={keyExtractor}
renderItem={renderItemInternal}
ListFooterComponent={ListFooterComponent}
// NOTE sticky header disabled on android due to major performance issues -prf
stickyHeaderIndices={isAndroid ? undefined : STICKY_HEADER_INDICES}
onScroll={onScroll}
onEndReached={onEndReached}
refreshControl={
<RefreshControl
refreshing={refreshing!}
onRefresh={onRefresh}
tintColor={pal.colors.text}
/>
}
onEndReachedThreshold={0.6}
contentContainerStyle={s.contentContainer}
removeClippedSubviews={true}
scrollIndicatorInsets={{right: 1}} // fixes a bug where the scroll indicator is on the middle of the screen https://github.com/bluesky-social/social-app/pull/464
/>
)
})
export function Selector({
selectedIndex,
items,
onSelect,
}: {
selectedIndex: number
items: string[]
onSelect?: (index: number) => void
}) {
const pal = usePalette('default')
const borderColor = useColorSchemeStyle(
{borderColor: colors.black},
{borderColor: colors.white},
)
const onPressItem = (index: number) => {
onSelect?.(index)
}
return (
<View
style={{
width: '100%',
backgroundColor: pal.colors.background,
}}>
<ScrollView
testID="selector"
horizontal
showsHorizontalScrollIndicator={false}>
<View style={[pal.view, styles.outer]}>
{items.map((item, i) => {
const selected = i === selectedIndex
return (
<Pressable
testID={`selector-${i}`}
key={item}
onPress={() => onPressItem(i)}
accessibilityLabel={item}
accessibilityHint={`Selects ${item}`}
// TODO: Modify the component API such that lint fails
// at the invocation site as well
>
<View
style={[
styles.item,
selected && styles.itemSelected,
borderColor,
]}>
<Text
style={
selected
? [styles.labelSelected, pal.text]
: [styles.label, pal.textLight]
}>
{item}
</Text>
</View>
</Pressable>
)
})}
</View>
</ScrollView>
</View>
)
}
const styles = StyleSheet.create({
outer: {
flexDirection: 'row',
paddingHorizontal: 14,
},
item: {
marginRight: 14,
paddingHorizontal: 10,
paddingTop: 8,
paddingBottom: 12,
},
itemSelected: {
borderBottomWidth: 3,
},
label: {
fontWeight: '600',
},
labelSelected: {
fontWeight: '600',
},
underline: {
position: 'absolute',
height: 4,
bottom: 0,
},
})