|
| 1 | +import * as React from 'react'; |
| 2 | +import { ScrollView, Text, View, StyleSheet, Pressable } from 'react-native'; |
| 3 | +import { |
| 4 | + createStaticNavigation, |
| 5 | + CommonActions, |
| 6 | + NavigationIndependentTree, |
| 7 | + NavigationContainer, |
| 8 | + StackActions, |
| 9 | +} from '@react-navigation/native'; |
| 10 | +import { createStackNavigator, StackAnimationName, StackNavigationOptions } from '@react-navigation/stack'; |
| 11 | +import { SettingsPicker, SettingsSwitch } from '../shared'; |
| 12 | +import { createNativeStackNavigator, NativeStackNavigationProp } from '@react-navigation/native-stack'; |
| 13 | +import Colors from '../shared/styling/Colors'; |
| 14 | +import { Button } from '@react-navigation/elements'; |
| 15 | +import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; |
| 16 | +import { SafeAreaView } from 'react-native-safe-area-context'; |
| 17 | + |
| 18 | +// #region Types |
| 19 | + |
| 20 | +interface StackScreenConfig { |
| 21 | + title: string; |
| 22 | + presentation: Exclude<StackNavigationOptions['presentation'], undefined>; |
| 23 | + animation: StackAnimationName; |
| 24 | + detachPreviousScreen: boolean; |
| 25 | +} |
| 26 | + |
| 27 | +interface TabScreenConfig { |
| 28 | + title: string; |
| 29 | + screens: StackScreenConfig[]; |
| 30 | +} |
| 31 | + |
| 32 | +interface SettableConfigContext<T> { |
| 33 | + config: T; |
| 34 | + setConfig: React.Dispatch<React.SetStateAction<T>>; |
| 35 | +} |
| 36 | + |
| 37 | +interface SingleItemConfigProps<T> { |
| 38 | + config: T; |
| 39 | + setItemConfig: (config: T) => void; |
| 40 | +} |
| 41 | + |
| 42 | +type TestRoutes = { |
| 43 | + Config: undefined; |
| 44 | + TestStack: undefined; |
| 45 | + TestBottomTabs: undefined; |
| 46 | +}; |
| 47 | + |
| 48 | +// #region Globals and constants |
| 49 | + |
| 50 | +const STACK_ANIMATION_NAMES: StackAnimationName[] = [ |
| 51 | + 'default', |
| 52 | + 'fade', |
| 53 | + 'fade_from_bottom', |
| 54 | + 'fade_from_right', |
| 55 | + 'none', |
| 56 | + 'reveal_from_bottom', |
| 57 | + 'scale_from_center', |
| 58 | + 'slide_from_bottom', |
| 59 | + 'slide_from_right', |
| 60 | + 'slide_from_left' |
| 61 | +]; |
| 62 | + |
| 63 | +const DEFAULT_SCREEN: StackScreenConfig = { |
| 64 | + title: 'Home', |
| 65 | + presentation: 'card', |
| 66 | + detachPreviousScreen: true, |
| 67 | + animation: 'default', |
| 68 | +}; |
| 69 | + |
| 70 | +const StackConfigContext = React.createContext<SettableConfigContext<StackScreenConfig[]>>({ config: [], setConfig: () => { } }); |
| 71 | +const BottomTabsConfigContext = React.createContext<SettableConfigContext<TabScreenConfig[]>>({ config: [], setConfig: () => { } }); |
| 72 | + |
| 73 | +// #region Test configuration |
| 74 | + |
| 75 | +function applyConfigChange<T>(configList: T[], index: number, config: T) { |
| 76 | + configList = [...configList]; |
| 77 | + configList[index] = config |
| 78 | + return configList; |
| 79 | +} |
| 80 | + |
| 81 | +function StackScreenConfigItem(props: SingleItemConfigProps<StackScreenConfig>) { |
| 82 | + const { config, setItemConfig } = props; |
| 83 | + |
| 84 | + return ( |
| 85 | + <View style={styles.configItem}> |
| 86 | + <Text style={styles.titleText}>{props.config.title}</Text> |
| 87 | + <SettingsSwitch |
| 88 | + label='detachPreviousScreen' |
| 89 | + value={props.config.detachPreviousScreen} |
| 90 | + onValueChange={v => setItemConfig({ ...config, detachPreviousScreen: v })} |
| 91 | + /> |
| 92 | + <SettingsPicker<StackScreenConfig['presentation']> |
| 93 | + label='presentation' |
| 94 | + value={config.presentation} |
| 95 | + onValueChange={v => setItemConfig({ ...config, presentation: v })} |
| 96 | + items={['card', 'modal', 'transparentModal']} |
| 97 | + /> |
| 98 | + <SettingsPicker<StackAnimationName> |
| 99 | + label='animation' |
| 100 | + value={config.animation} |
| 101 | + onValueChange={v => setItemConfig({ ...config, animation: v })} |
| 102 | + items={STACK_ANIMATION_NAMES} |
| 103 | + /> |
| 104 | + </View> |
| 105 | + ) |
| 106 | +} |
| 107 | + |
| 108 | +function BottomTabsScreenConfigItem(props: SingleItemConfigProps<TabScreenConfig>) { |
| 109 | + const { config, setItemConfig } = props; |
| 110 | + |
| 111 | + return ( |
| 112 | + <View style={styles.configItem}> |
| 113 | + <Text style={styles.titleText}>{props.config.title}</Text> |
| 114 | + {config.screens.map((stackConfig, i) => ( |
| 115 | + <StackScreenConfigItem |
| 116 | + key={i} |
| 117 | + config={stackConfig} |
| 118 | + setItemConfig={newConfig => setItemConfig({ ...config, screens: applyConfigChange(config.screens, i, newConfig) })} |
| 119 | + /> |
| 120 | + ))} |
| 121 | + <Pressable |
| 122 | + style={styles.stackButton} |
| 123 | + onPress={() => { |
| 124 | + setItemConfig({ |
| 125 | + ...config, |
| 126 | + screens: [ ...config.screens, { ...DEFAULT_SCREEN, title: `Screen${config.screens.length}` } ], |
| 127 | + }) |
| 128 | + }} |
| 129 | + > |
| 130 | + <Text>Add</Text> |
| 131 | + </Pressable> |
| 132 | + <Pressable |
| 133 | + style={styles.stackButton} |
| 134 | + onPress={() => { |
| 135 | + setItemConfig({ |
| 136 | + ...config, |
| 137 | + screens: [ ...config.screens.slice(0, -1) ], |
| 138 | + }) |
| 139 | + }} |
| 140 | + > |
| 141 | + <Text>Remove</Text> |
| 142 | + </Pressable> |
| 143 | + </View> |
| 144 | + ) |
| 145 | +} |
| 146 | + |
| 147 | +function Config(props: { navigation: NativeStackNavigationProp<TestRoutes> }) { |
| 148 | + const { config: stackConfig, setConfig: setStackConfig } = React.useContext(StackConfigContext); |
| 149 | + const { config: bottomTabsConfig, setConfig: setBottomTabsConfig } = React.useContext(BottomTabsConfigContext); |
| 150 | + const [ scenario, setScenario ] = React.useState<'stack' | 'tabs'>('stack'); |
| 151 | + |
| 152 | + return ( |
| 153 | + <ScrollView style={styles.configList}> |
| 154 | + <SafeAreaView> |
| 155 | + <SettingsPicker<'stack' | 'tabs'> |
| 156 | + label='Scenario' |
| 157 | + value={scenario} |
| 158 | + onValueChange={s => setScenario(s)} |
| 159 | + items={['stack', 'tabs']} |
| 160 | + /> |
| 161 | + {scenario === 'stack' && (<> |
| 162 | + {stackConfig.map((config, i) => ( |
| 163 | + <StackScreenConfigItem |
| 164 | + key={i} |
| 165 | + config={config} |
| 166 | + setItemConfig={newConfig => setStackConfig(applyConfigChange(stackConfig, i, newConfig))} |
| 167 | + /> |
| 168 | + ))} |
| 169 | + <Pressable style={styles.stackButton} onPress={() => {setStackConfig([...stackConfig, { ...DEFAULT_SCREEN, title: `Screen${stackConfig.length}` }])}}> |
| 170 | + <Text>Add</Text> |
| 171 | + </Pressable> |
| 172 | + <Pressable style={styles.stackButton} onPress={() => setStackConfig([...stackConfig.slice(0, -1)])}> |
| 173 | + <Text>Remove</Text> |
| 174 | + </Pressable> |
| 175 | + <Pressable style={styles.stackButton} onPress={() => props.navigation.navigate('TestStack')}> |
| 176 | + <Text>Test</Text> |
| 177 | + </Pressable> |
| 178 | + </>)} |
| 179 | + {scenario === 'tabs' && (<> |
| 180 | + {bottomTabsConfig.map((config, i) => ( |
| 181 | + <BottomTabsScreenConfigItem |
| 182 | + key={i} |
| 183 | + config={config} |
| 184 | + setItemConfig={newConfig => setBottomTabsConfig(applyConfigChange(bottomTabsConfig, i, newConfig))} |
| 185 | + /> |
| 186 | + ))} |
| 187 | + <Pressable style={styles.tabsButton} onPress={() => {setBottomTabsConfig([...bottomTabsConfig, { title: `Tab${stackConfig.length}`, screens: [{ ...DEFAULT_SCREEN }] }])}}> |
| 188 | + <Text>Add</Text> |
| 189 | + </Pressable> |
| 190 | + <Pressable style={styles.tabsButton} onPress={() => setBottomTabsConfig([...bottomTabsConfig.slice(0, -1)])}> |
| 191 | + <Text>Remove</Text> |
| 192 | + </Pressable> |
| 193 | + <Pressable style={styles.tabsButton} onPress={() => props.navigation.navigate('TestBottomTabs')}> |
| 194 | + <Text>Test</Text> |
| 195 | + </Pressable> |
| 196 | + </>)} |
| 197 | + </SafeAreaView> |
| 198 | + </ScrollView> |
| 199 | + ) |
| 200 | +} |
| 201 | + |
| 202 | +// #region Test routes |
| 203 | + |
| 204 | +function TestScreen(props: { nextScreen: string | undefined, isFirst: boolean }) { |
| 205 | + return ( |
| 206 | + <View style={{ padding: 8, gap: 8 }}> |
| 207 | + { props.nextScreen && <Button screen={props.nextScreen}>Go to {props.nextScreen}</Button> } |
| 208 | + { !props.isFirst && <Button action={CommonActions.goBack()}>Go back</Button> } |
| 209 | + { !props.isFirst && <Button action={StackActions.pop(2)}>Pop 2</Button> } |
| 210 | + </View> |
| 211 | + ) |
| 212 | +} |
| 213 | + |
| 214 | +function TestStack( props: { configList: StackScreenConfig[] }) { |
| 215 | + const JSStack = createStackNavigator({ |
| 216 | + detachInactiveScreens: true, |
| 217 | + screens: Object.fromEntries(props.configList.map((config, i) => [ |
| 218 | + config.title, |
| 219 | + { |
| 220 | + screen: () => <TestScreen nextScreen={ props.configList.at(i + 1)?.title } isFirst={i === 0} />, |
| 221 | + options: { |
| 222 | + presentation: config.presentation, |
| 223 | + detachPreviousScreen: config.detachPreviousScreen, |
| 224 | + animation: config.animation, |
| 225 | + gestureEnabled: true, |
| 226 | + }, |
| 227 | + }, |
| 228 | + ])) |
| 229 | + }); |
| 230 | + |
| 231 | + const Navigation = createStaticNavigation(JSStack); |
| 232 | + |
| 233 | + return ( |
| 234 | + <NavigationIndependentTree> |
| 235 | + <Navigation /> |
| 236 | + </NavigationIndependentTree> |
| 237 | + ) |
| 238 | +} |
| 239 | + |
| 240 | +function TestBottomTabs( props: { configList: TabScreenConfig[] }) { |
| 241 | + const Tab = createBottomTabNavigator(); |
| 242 | + |
| 243 | + return ( |
| 244 | + <Tab.Navigator detachInactiveScreens={true}> |
| 245 | + {props.configList.map(config => ( |
| 246 | + <Tab.Screen |
| 247 | + name={config.title} |
| 248 | + options={{ headerShown: false }} |
| 249 | + component={() => <TestStack configList={config.screens} />} |
| 250 | + /> |
| 251 | + ))} |
| 252 | + </Tab.Navigator> |
| 253 | + ); |
| 254 | +} |
| 255 | + |
| 256 | +export default function App() { |
| 257 | + const [stackScreenConfig, setStackScreenConfig] = React.useState<StackScreenConfig[]>([ |
| 258 | + { ...DEFAULT_SCREEN } |
| 259 | + ]); |
| 260 | + const [tabScreenConfig, setTabScreenConfig] = React.useState<TabScreenConfig[]>([ |
| 261 | + { |
| 262 | + title: 'HomeTab', |
| 263 | + screens: [ { ...DEFAULT_SCREEN } ] |
| 264 | + } |
| 265 | + ]); |
| 266 | + |
| 267 | + const Stack = createNativeStackNavigator<TestRoutes>(); |
| 268 | + |
| 269 | + return ( |
| 270 | + <StackConfigContext.Provider value={{ |
| 271 | + config: stackScreenConfig, |
| 272 | + setConfig: setStackScreenConfig, |
| 273 | + }}> |
| 274 | + <BottomTabsConfigContext.Provider value={{ |
| 275 | + config: tabScreenConfig, |
| 276 | + setConfig: setTabScreenConfig, |
| 277 | + }}> |
| 278 | + <NavigationIndependentTree> |
| 279 | + <NavigationContainer> |
| 280 | + <Stack.Navigator> |
| 281 | + <Stack.Screen |
| 282 | + name='Config' |
| 283 | + component={Config} |
| 284 | + /> |
| 285 | + <Stack.Screen |
| 286 | + name='TestStack' |
| 287 | + component={() => <TestStack configList={stackScreenConfig} />} |
| 288 | + options={{ |
| 289 | + headerShown: false, |
| 290 | + }} |
| 291 | + /> |
| 292 | + <Stack.Screen |
| 293 | + name='TestBottomTabs' |
| 294 | + component={() => <TestBottomTabs configList={tabScreenConfig} />} |
| 295 | + options={{ |
| 296 | + headerShown: false, |
| 297 | + }} |
| 298 | + /> |
| 299 | + </Stack.Navigator> |
| 300 | + </NavigationContainer> |
| 301 | + </NavigationIndependentTree> |
| 302 | + </BottomTabsConfigContext.Provider> |
| 303 | + </StackConfigContext.Provider> |
| 304 | + ) |
| 305 | +} |
| 306 | + |
| 307 | +const styles = StyleSheet.create({ |
| 308 | + configList: { |
| 309 | + padding: 8, |
| 310 | + }, |
| 311 | + configItem: { |
| 312 | + flex: 1, |
| 313 | + borderStyle: 'solid', |
| 314 | + borderRadius: 16, |
| 315 | + borderColor: Colors.cardBorder, |
| 316 | + borderWidth: 1, |
| 317 | + margin: 4, |
| 318 | + }, |
| 319 | + titleText: { |
| 320 | + fontSize: 18, |
| 321 | + textAlign: 'center', |
| 322 | + }, |
| 323 | + stackButton: { |
| 324 | + borderRadius: 16, |
| 325 | + fontWeight: 'bold', |
| 326 | + flex: 1, |
| 327 | + alignItems: 'center', |
| 328 | + justifyContent: 'center', |
| 329 | + backgroundColor: Colors.BlueDark100, |
| 330 | + margin: 4, |
| 331 | + padding: 8, |
| 332 | + }, |
| 333 | + tabsButton: { |
| 334 | + borderRadius: 16, |
| 335 | + fontWeight: 'bold', |
| 336 | + flex: 1, |
| 337 | + alignItems: 'center', |
| 338 | + justifyContent: 'center', |
| 339 | + backgroundColor: Colors.GreenDark100, |
| 340 | + margin: 4, |
| 341 | + padding: 8, |
| 342 | + } |
| 343 | +}); |
0 commit comments