import { StackNavigationOptions } from '@react-navigation/stack'; import { useHeaderHeight } from '@react-navigation/elements'; import React, { forwardRef, Fragment, ReactNode, Ref, useCallback, useContext, useEffect, useImperativeHandle, useRef, } from 'react'; import { Animated, KeyboardAvoidingView, NativeScrollEvent, NativeSyntheticEvent, Platform, ScrollView, ScrollViewProps, StatusBar, StatusBarStyle, useWindowDimensions, View, ViewProps, } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { ApplicationContext, ScreenContext } from '../Context'; import Navigation from '../Application/Navigation'; import { AnimatedHeader, NavigationOptions, ScreenTrackingParams, SearchHeaderProps, } from '../Application/types'; import { Colors, Spacing, Styles } from '../Consts'; import { FloatingButton, FloatingButtonProps } from './FloatingButton'; import styles from './styles'; import { HeaderType } from './types'; import { InputRef } from '../Input'; import { exportHeaderTitle, getOptions } from '../Application/utils'; import { validateChildren } from './utils'; import Section from './Section'; import { HeaderBackground, HeaderExtendHeader, HeaderLeft, HeaderTitle, SearchHeader, } from '../Application'; export interface ScreenProps extends ViewProps { /** * ReactNode representing the content of the screen. */ children: React.ReactNode; /** * Optional. Navigation object for handling navigation actions. */ navigation?: Navigation; /** * Optional. If `true`, enable keyboard avoiding view for the screen. */ enableKeyboardAvoidingView?: boolean; /** * Optional. If `true`, the screen content is scrollable. */ scrollable?: boolean; /** * Optional. Set type of header. */ headerType?: HeaderType; /** * Optional. Props for the animated component or bool used top navigation animated. */ animatedHeader?: AnimatedHeader; /** * Optional. Props for the underlying ScrollView component when `scrollable` is `true`. */ scrollViewProps?: ScrollViewProps; /** * Optional. ReactNode representing the header component of the screen. */ headerComponent?: ReactNode; /** * Optional. ReactNode representing the footer component of the screen. */ footerComponent?: ReactNode; /** * Optional. Background color of the screen. */ backgroundColor?: string; /** * Optional. layout card offset overlap header banner. */ layoutOffset?: -8 | -24 | -56; /** * Optional. Width of the right header. */ headerRightWidth?: 0 | 73 | 110 | number; /** * Optional. Props for the floating action button. */ floatingButtonProps?: Omit; /** * Optional. Keyboard vertical offset. */ keyboardVerticalOffset?: number; /** * Optional. If `true`, the screen content is using grid layout by span. */ useGridLayout?: boolean; /** * Optional. specs for input search. */ inputSearchProps?: SearchHeaderProps; /** * Optional. Ref for input search. */ inputSearchRef?: Ref; /** * Optional. Animated value for header. */ animatedValue?: Animated.Value; /** * Optional. If `true`, use shadow header. */ useShadowHeader?: boolean; /** * Optional. Custom header Gradient Color. */ gradientColor?: string; /** * Optional. Custom headerBackground Image */ headerBackground?: string; /** * Optional. Custom tracking params */ trackingParams?: ScreenTrackingParams; } const Screen = forwardRef( ( { children, enableKeyboardAvoidingView = false, scrollable = false, navigation, animatedHeader, scrollViewProps, headerComponent: Header, footerComponent: Footer, layoutOffset = -56, headerRightWidth = 73, backgroundColor, headerType = 'default', floatingButtonProps, useGridLayout = true, useShadowHeader = true, keyboardVerticalOffset, inputSearchProps, inputSearchRef, animatedValue: customAnimatedValue, headerBackground, gradientColor, trackingParams, }: ScreenProps, ref: any, ) => { const screenRef = useRef(null); const { width: widthDevice } = useWindowDimensions(); const { theme } = useContext(ApplicationContext); const screen: any = useContext(ScreenContext); const insets = useSafeAreaInsets(); const heightHeader = useHeaderHeight(); const animatedValue = useRef( customAnimatedValue || new Animated.Value(0), ); const currentTint = useRef(undefined); const isTab = navigation?.instance?.getState?.()?.type === 'tab'; let handleScroll; let Component: any = View; let keyboardOffset = heightHeader - Math.min(insets.bottom, 21); if (headerType === 'extended' || animatedHeader || inputSearchProps) { keyboardOffset = -Math.min(insets.bottom, 21); } /** * inject params for screen tracking */ screen?.onSetParams?.(trackingParams); /** * export options for screen * @param headerType */ const setHeaderType = useCallback( (headerType: HeaderType) => { let options: StackNavigationOptions; switch (headerType) { case 'none': options = { headerTransparent: true, headerBackground: () => null, headerShown: false, }; break; case 'extended': options = { headerShown: true, headerTransparent: true, headerBackground: () => null, headerTitle: (props: any) => , }; if (inputSearchProps) { options = { headerShown: true, headerTransparent: true, headerBackground: () => null, headerTitle: (props: any) => ( ), }; } break; default: options = { headerTransparent: false, headerTintColor: Colors.black_17, headerShown: true, headerBackground: (props: any) => ( ), headerTitle: (props: any) => , }; if (inputSearchProps) { options = { headerShown: true, headerTransparent: true, headerBackground: () => null, headerTitle: (props: any) => ( ), }; } } navigation?.instance?.setOptions(options); }, [ gradientColor, headerBackground, inputSearchProps, navigation?.instance, useShadowHeader, ], ); /** * export animatedHeader options for screen * @param animatedHeader */ const setAnimatedHeader = useCallback( (animatedHeader: AnimatedHeader) => { let options: StackNavigationOptions; if (!currentTint.current) { currentTint.current = animatedHeader?.headerTintColor; } options = { headerTintColor: currentTint.current ?? Colors.black_17, headerTransparent: true, headerBackground: (props: any) => ( ), headerTitle: (props: any) => { return ; }, }; if (animatedHeader.headerTitle) { options = { ...options, ...exportHeaderTitle(animatedHeader), }; } navigation?.instance?.setOptions(options); }, [gradientColor, headerBackground, navigation?.instance, useShadowHeader], ); /** * export animatedHeader options for screen */ const setAnimatedSearch = useCallback(() => { const options: StackNavigationOptions = { headerShown: true, headerTransparent: true, headerBackground: () => null, headerTitle: (props: any) => ( ), }; navigation?.instance?.setOptions(options); }, [navigation?.instance]); /** * export search header */ const setSearchHeader = (params: SearchHeaderProps) => { const searchWidth = widthDevice - 24; const options: StackNavigationOptions = { headerRight: undefined, headerTitleContainerStyle: { width: searchWidth, maxWidth: searchWidth, marginLeft: 0, marginEnd: 0, marginStart: 0, marginHorizontal: 0, marginVertical: 0, margin: 0, left: Spacing.M, }, headerLeft: (props: any) => params?.hiddenBack ? null : , headerTitle: () => ( ), }; navigation?.instance?.setOptions(options); }; useEffect(() => { const focus = navigation?.instance?.addListener?.('focus', () => { let barStyle: StatusBarStyle = 'dark-content'; if (currentTint.current === Colors.black_01) { barStyle = 'light-content'; } StatusBar.setBarStyle(barStyle, true); }); if (animatedHeader) { setAnimatedHeader(animatedHeader); } else if (inputSearchProps) { setAnimatedSearch(); } else { setHeaderType(headerType); } return () => { focus?.(); }; }, [ headerType, animatedHeader, inputSearchProps, useShadowHeader, navigation?.instance, setAnimatedHeader, setAnimatedSearch, setHeaderType, ]); /** * expose ref for screen */ useImperativeHandle(ref, () => { return Object.assign(screenRef.current || {}, { setOptions: (params: NavigationOptions) => { const options = getOptions(params, undefined); navigation?.instance?.setOptions(options); }, setSearchHeader: setSearchHeader, }); }); /** * animated when use scroll && animated value */ if (scrollable) { Component = Animated.ScrollView; handleScroll = Animated.event( [ { nativeEvent: { contentOffset: { y: animatedValue.current as Animated.Value }, }, }, ], { useNativeDriver: true, listener: (e: NativeSyntheticEvent) => { scrollViewProps?.onScroll?.(e); if (animatedHeader) { const offsetY = e.nativeEvent.contentOffset.y; let color = animatedHeader?.headerTintColor ?? Colors.black_17; if (offsetY > 50) { color = Colors.black_17; } if (color !== currentTint.current) { currentTint.current = color; navigation?.setOptions({ headerTintColor: color, }); let barStyle: StatusBarStyle = 'dark-content'; if (currentTint.current === Colors.black_01) { barStyle = 'light-content'; } StatusBar.setBarStyle(barStyle, true); } } }, }, ); } /** * handle scroll end * @param e */ const handleScrollEnd = (e: NativeSyntheticEvent) => { const offsetY = e.nativeEvent.contentOffset.y; if (inputSearchProps && offsetY < 100 && offsetY > 0) { Animated.timing(animatedValue.current, { toValue: 0, useNativeDriver: true, duration: 300, }).start(); ref?.scrollTo?.({ y: 0, animated: true }); } scrollViewProps?.onScrollEndDrag?.(e); }; /** * render top navigation banner */ const renderAnimatedHeader = () => { if (typeof animatedHeader?.component === 'function') { return ( {animatedHeader?.component({ animatedValue: animatedValue.current, })} ); } }; /** * build content for screen for grid rule */ const renderContent = (children: any): any => { const results = validateChildren(children, [undefined]); if (Array.isArray(results)) { return results.map((item, index) => { const space = item?.props?.useMargin === false ? 0 : Spacing.M; if (item?.type === Fragment) { return renderContent(item?.props?.children); } if (item) { return ( {item} ); } }); } else { const item: any = children; const space = item?.props?.useMargin === false ? 0 : Spacing.M; if (item?.type === Fragment) { return renderContent(item?.props?.children); } return ( {children} ); } }; return ( {Header} {renderAnimatedHeader()} {useGridLayout ? renderContent(children) : children} {floatingButtonProps && ( )} {Footer && (
{Footer}
)}
); }, ); export default Screen;