mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
at verify-code 5.8 kB view raw
1import React, {useEffect, useState} from 'react' 2import { 3 NativeSyntheticEvent, 4 NativeScrollEvent, 5 Pressable, 6 RefreshControl, 7 StyleSheet, 8 View, 9 ScrollView, 10} from 'react-native' 11import {FlatList_INTERNAL} from './Views' 12import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle' 13import {Text} from './text/Text' 14import {usePalette} from 'lib/hooks/usePalette' 15import {clamp} from 'lib/numbers' 16import {s, colors} from 'lib/styles' 17import {isAndroid} from 'platform/detection' 18 19const HEADER_ITEM = {_reactKey: '__header__'} 20const SELECTOR_ITEM = {_reactKey: '__selector__'} 21const STICKY_HEADER_INDICES = [1] 22 23export type ViewSelectorHandle = { 24 scrollToTop: () => void 25} 26 27export const ViewSelector = React.forwardRef< 28 ViewSelectorHandle, 29 { 30 sections: string[] 31 items: any[] 32 refreshing?: boolean 33 swipeEnabled?: boolean 34 renderHeader?: () => JSX.Element 35 renderItem: (item: any) => JSX.Element 36 ListFooterComponent?: 37 | React.ComponentType<any> 38 | React.ReactElement 39 | null 40 | undefined 41 onSelectView?: (viewIndex: number) => void 42 onScroll?: (event: NativeSyntheticEvent<NativeScrollEvent>) => void 43 onRefresh?: () => void 44 onEndReached?: (info: {distanceFromEnd: number}) => void 45 } 46>(function ViewSelectorImpl( 47 { 48 sections, 49 items, 50 refreshing, 51 renderHeader, 52 renderItem, 53 ListFooterComponent, 54 onSelectView, 55 onScroll, 56 onRefresh, 57 onEndReached, 58 }, 59 ref, 60) { 61 const pal = usePalette('default') 62 const [selectedIndex, setSelectedIndex] = useState<number>(0) 63 const flatListRef = React.useRef<FlatList_INTERNAL>(null) 64 65 // events 66 // = 67 68 const keyExtractor = React.useCallback((item: any) => item._reactKey, []) 69 70 const onPressSelection = React.useCallback( 71 (index: number) => setSelectedIndex(clamp(index, 0, sections.length)), 72 [setSelectedIndex, sections], 73 ) 74 useEffect(() => { 75 onSelectView?.(selectedIndex) 76 }, [selectedIndex, onSelectView]) 77 78 React.useImperativeHandle(ref, () => ({ 79 scrollToTop: () => { 80 flatListRef.current?.scrollToOffset({offset: 0}) 81 }, 82 })) 83 84 // rendering 85 // = 86 87 const renderItemInternal = React.useCallback( 88 ({item}: {item: any}) => { 89 if (item === HEADER_ITEM) { 90 if (renderHeader) { 91 return renderHeader() 92 } 93 return <View /> 94 } else if (item === SELECTOR_ITEM) { 95 return ( 96 <Selector 97 items={sections} 98 selectedIndex={selectedIndex} 99 onSelect={onPressSelection} 100 /> 101 ) 102 } else { 103 return renderItem(item) 104 } 105 }, 106 [sections, selectedIndex, onPressSelection, renderHeader, renderItem], 107 ) 108 109 const data = React.useMemo( 110 () => [HEADER_ITEM, SELECTOR_ITEM, ...items], 111 [items], 112 ) 113 return ( 114 <FlatList_INTERNAL 115 ref={flatListRef} 116 data={data} 117 keyExtractor={keyExtractor} 118 renderItem={renderItemInternal} 119 ListFooterComponent={ListFooterComponent} 120 // NOTE sticky header disabled on android due to major performance issues -prf 121 stickyHeaderIndices={isAndroid ? undefined : STICKY_HEADER_INDICES} 122 onScroll={onScroll} 123 onEndReached={onEndReached} 124 refreshControl={ 125 <RefreshControl 126 refreshing={refreshing!} 127 onRefresh={onRefresh} 128 tintColor={pal.colors.text} 129 /> 130 } 131 onEndReachedThreshold={0.6} 132 contentContainerStyle={s.contentContainer} 133 removeClippedSubviews={true} 134 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 135 /> 136 ) 137}) 138 139export function Selector({ 140 selectedIndex, 141 items, 142 onSelect, 143}: { 144 selectedIndex: number 145 items: string[] 146 onSelect?: (index: number) => void 147}) { 148 const pal = usePalette('default') 149 const borderColor = useColorSchemeStyle( 150 {borderColor: colors.black}, 151 {borderColor: colors.white}, 152 ) 153 154 const onPressItem = (index: number) => { 155 onSelect?.(index) 156 } 157 158 return ( 159 <View 160 style={{ 161 width: '100%', 162 backgroundColor: pal.colors.background, 163 }}> 164 <ScrollView 165 testID="selector" 166 horizontal 167 showsHorizontalScrollIndicator={false}> 168 <View style={[pal.view, styles.outer]}> 169 {items.map((item, i) => { 170 const selected = i === selectedIndex 171 return ( 172 <Pressable 173 testID={`selector-${i}`} 174 key={item} 175 onPress={() => onPressItem(i)} 176 accessibilityLabel={item} 177 accessibilityHint={`Selects ${item}`} 178 // TODO: Modify the component API such that lint fails 179 // at the invocation site as well 180 > 181 <View 182 style={[ 183 styles.item, 184 selected && styles.itemSelected, 185 borderColor, 186 ]}> 187 <Text 188 style={ 189 selected 190 ? [styles.labelSelected, pal.text] 191 : [styles.label, pal.textLight] 192 }> 193 {item} 194 </Text> 195 </View> 196 </Pressable> 197 ) 198 })} 199 </View> 200 </ScrollView> 201 </View> 202 ) 203} 204 205const styles = StyleSheet.create({ 206 outer: { 207 flexDirection: 'row', 208 paddingHorizontal: 14, 209 }, 210 item: { 211 marginRight: 14, 212 paddingHorizontal: 10, 213 paddingTop: 8, 214 paddingBottom: 12, 215 }, 216 itemSelected: { 217 borderBottomWidth: 3, 218 }, 219 label: { 220 fontWeight: '600', 221 }, 222 labelSelected: { 223 fontWeight: '600', 224 }, 225 underline: { 226 position: 'absolute', 227 height: 4, 228 bottom: 0, 229 }, 230})