UNPKG

react-native-ui-lib

Version:

[![SWUbanner](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/banner-direct.svg)](https://stand-with-ukraine.pp.ua)

214 lines (211 loc) • 7.99 kB
import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { StyleSheet } from 'react-native'; import Animated, { useAnimatedKeyboard, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated'; import { Keyboard } from 'uilib-native'; import { SafeAreaContextPackage } from "../../optionalDependencies"; import View from "../view"; import Image from "../image"; import Assets from "../../assets"; import { Colors, Shadows, Spacings } from "../../style"; import { asBaseComponent, Constants } from "../../commons/new"; import { useKeyboardHeight } from "../../hooks"; import { ScreenFooterProps, ScreenFooterLayouts, ScreenFooterBackgrounds, FooterAlignment, HorizontalItemsDistribution, ItemsFit, KeyboardBehavior, ScreenFooterShadow } from "./types"; export { ScreenFooterProps, ScreenFooterLayouts, ScreenFooterBackgrounds, FooterAlignment, HorizontalItemsDistribution, ItemsFit, KeyboardBehavior, ScreenFooterShadow }; const ScreenFooter = props => { const { testID, layout, alignment, horizontalAlignment, backgroundType, children, keyboardBehavior = KeyboardBehavior.STICKY, itemsFit, itemWidth, horizontalItemsDistribution: distribution, visible = true, animationDuration = 200, shadow = ScreenFooterShadow.SH20, hideDivider = false, contentContainerStyle: contentContainerStyleOverride } = props; const keyboard = useAnimatedKeyboard(); const [height, setHeight] = useState(0); const visibilityTranslateY = useSharedValue(0); // Update visibility translation when visible or height changes useEffect(() => { visibilityTranslateY.value = withTiming(visible ? 0 : height, { duration: animationDuration }); }, [visible, height, animationDuration, visibilityTranslateY]); // Animated style for STICKY behavior (counters Android system offset + visibility) const stickyAnimatedStyle = useAnimatedStyle(() => { const counterSystemOffset = Constants.isAndroid ? keyboard.height.value : 0; return { transform: [{ translateY: counterSystemOffset + visibilityTranslateY.value }] }; }); // Animated style for HOISTED behavior (visibility only, keyboard handled by KeyboardAccessoryView) const hoistedAnimatedStyle = useAnimatedStyle(() => { return { transform: [{ translateY: visibilityTranslateY.value }] }; }); const onLayout = useCallback(event => { setHeight(event.nativeEvent.layout.height); }, []); const isSolid = backgroundType === ScreenFooterBackgrounds.SOLID; const isFading = backgroundType === ScreenFooterBackgrounds.FADING; const isHorizontal = layout === ScreenFooterLayouts.HORIZONTAL; const childrenCount = React.Children.count(children); const justifyContent = useMemo(() => { if (isHorizontal) { if (distribution === HorizontalItemsDistribution.SPREAD) { return childrenCount === 1 ? 'center' : 'space-between'; } switch (horizontalAlignment) { case FooterAlignment.START: return 'flex-start'; case FooterAlignment.END: return 'flex-end'; default: return 'center'; } } return 'flex-start'; }, [isHorizontal, distribution, horizontalAlignment, childrenCount]); const alignItems = useMemo(() => { if (layout === ScreenFooterLayouts.VERTICAL) { if (itemsFit === ItemsFit.STRETCH) { return 'stretch'; } } switch (alignment) { case FooterAlignment.START: return 'flex-start'; case FooterAlignment.END: return 'flex-end'; default: return 'center'; } }, [layout, itemsFit, alignment]); const useSafeAreaInsets = SafeAreaContextPackage?.useSafeAreaInsets ?? (() => Constants.getSafeAreaInsets()); const insets = useSafeAreaInsets(); const keyboardHeight = useKeyboardHeight(); const isKeyboardVisible = keyboardHeight > 0; const contentContainerStyle = useMemo(() => { const style = [styles.contentContainer, layout === ScreenFooterLayouts.HORIZONTAL ? styles.horizontalContainer : styles.verticalContainer, { alignItems, justifyContent }]; if (!isKeyboardVisible) { style.push({ paddingBottom: insets.bottom }); } if (isSolid) { const shadowStyle = Shadows[shadow]?.top; const backgroundElevation = shadowStyle?.elevation || 0; style.push({ elevation: backgroundElevation + 1 }); } if (contentContainerStyleOverride) { style.push(contentContainerStyleOverride); } return style; }, [layout, alignItems, justifyContent, insets.bottom, isSolid, shadow, isKeyboardVisible, contentContainerStyleOverride]); const solidBackgroundStyle = useMemo(() => { if (!isSolid) { return undefined; } const shadowStyle = Shadows[shadow]?.top; const dividerStyle = hideDivider ? undefined : { borderTopWidth: 1, borderColor: Colors.$outlineDefault }; return [shadowStyle, dividerStyle]; }, [isSolid, shadow, hideDivider]); const renderBackground = useCallback(() => { if (isSolid) { return <View testID={testID ? `${testID}.solidBackground` : undefined} absF bg-$backgroundElevated style={solidBackgroundStyle} pointerEvents="none" />; } if (isFading) { return <View testID={testID ? `${testID}.fadingBackground` : undefined} absF pointerEvents="none"> <Image source={Assets.internal.images.bottomGradient} style={styles.background} resizeMode="stretch" tintColor={Colors.$backgroundDefault} /> </View>; } return null; }, [testID, isSolid, isFading, solidBackgroundStyle]); const renderChild = useCallback((child, index) => { if (itemsFit === ItemsFit.FIXED && itemWidth) { const fixedStyle = isHorizontal ? { width: itemWidth, flexShrink: 1, overflow: 'hidden', flexDirection: 'row', justifyContent: 'center' } : { width: itemWidth, maxWidth: '100%' }; return <View key={index} style={fixedStyle}> {child} </View>; } if (isHorizontal && React.isValidElement(child) && itemsFit === ItemsFit.STRETCH) { return <View flex row centerH key={index}> {child} </View>; } return child; }, [itemsFit, itemWidth, isHorizontal]); const childrenArray = React.Children.toArray(children).slice(0, 3).map(renderChild); const renderFooterContent = useCallback(() => { return <> {renderBackground()} <View testID={testID ? `${testID}.content` : undefined} style={contentContainerStyle}> {childrenArray} </View> </>; }, [renderBackground, testID, contentContainerStyle, childrenArray]); if (keyboardBehavior === KeyboardBehavior.HOISTED) { return <Animated.View style={[styles.container, hoistedAnimatedStyle]} pointerEvents={visible ? 'box-none' : 'none'}> <Keyboard.KeyboardAccessoryView renderContent={renderFooterContent} kbInputRef={undefined} scrollBehavior={Keyboard.KeyboardAccessoryView.scrollBehaviors.FIXED_OFFSET} useSafeArea={false} manageScrollView={false} revealKeyboardInteractive onHeightChanged={setHeight} /> </Animated.View>; } return <Animated.View testID={testID} onLayout={onLayout} style={[styles.container, stickyAnimatedStyle]}> {renderFooterContent()} </Animated.View>; }; ScreenFooter.displayName = 'ScreenFooter'; const styles = StyleSheet.create({ container: { position: 'absolute', bottom: 0, left: 0, right: 0 }, contentContainer: { paddingTop: Spacings.s4, paddingHorizontal: Spacings.s5, paddingBottom: Spacings.s5 }, horizontalContainer: { flexDirection: 'row', gap: Spacings.s5 }, verticalContainer: { flexDirection: 'column', gap: Spacings.s3 }, background: { width: '100%', height: '100%' } }); export default asBaseComponent(ScreenFooter);