import * as React from 'react'; import { Animated, StyleProp, StyleSheet, View, ViewStyle } from 'react-native'; import useLatestCallback from 'use-latest-callback'; import { useInternalTheme } from '../core/theming'; import type { $Omit, $RemoveChildren, ThemeProp } from '../types'; import Button from './Button/Button'; import Icon, { IconSource } from './Icon'; import Surface from './Surface'; import Text from './Typography/Text'; const DEFAULT_MAX_WIDTH = 960; export type Props = $Omit<$RemoveChildren, 'mode'> & { /** * Whether banner is currently visible. */ visible: boolean; /** * Content that will be displayed inside banner. */ children: React.ReactNode; /** * Icon to display for the `Banner`. Can be an image. */ icon?: IconSource; /** * Action items to shown in the banner. * An action item should contain the following properties: * * - `label`: label of the action button (required) * - `onPress`: callback that is called when button is pressed (required) * * To customize button you can pass other props that button component takes. */ actions?: Array< { label: string; } & $RemoveChildren >; /** * Style of banner's inner content. * Use this prop to apply custom width for wide layouts. */ contentStyle?: StyleProp; /** * @supported Available in v5.x with theme version 3 * Changes Banner shadow and background on iOS and Android. */ elevation?: 0 | 1 | 2 | 3 | 4 | 5 | Animated.Value; style?: Animated.WithAnimatedValue>; ref?: React.RefObject; /** * @optional */ theme?: ThemeProp; /** * @optional * Optional callback that will be called after the opening animation finished running normally */ onShowAnimationFinished?: Animated.EndCallback; /** * @optional * Optional callback that will be called after the closing animation finished running normally */ onHideAnimationFinished?: Animated.EndCallback; }; type NativeEvent = { nativeEvent: { layout: { x: number; y: number; width: number; height: number; }; }; }; /** * Banner displays a prominent message and related actions. * *
* *
* * ## Usage * ```js * import * as React from 'react'; * import { Image } from 'react-native'; * import { Banner } from 'react-native-paper'; * * const MyComponent = () => { * const [visible, setVisible] = React.useState(true); * * return ( * setVisible(false), * }, * { * label: 'Learn more', * onPress: () => setVisible(false), * }, * ]} * icon={({size}) => ( * * )}> * There was a problem processing a transaction on your credit card. * * ); * }; * * export default MyComponent; * ``` */ const Banner = ({ visible, icon, children, actions = [], contentStyle, elevation = 1, style, theme: themeOverrides, onShowAnimationFinished = () => {}, onHideAnimationFinished = () => {}, ...rest }: Props) => { const theme = useInternalTheme(themeOverrides); const { current: position } = React.useRef( new Animated.Value(visible ? 1 : 0) ); const [layout, setLayout] = React.useState<{ height: number; measured: boolean; }>({ height: 0, measured: false, }); const showCallback = useLatestCallback(onShowAnimationFinished); const hideCallback = useLatestCallback(onHideAnimationFinished); const { scale } = theme.animation; const opacity = position.interpolate({ inputRange: [0, 0.1, 1], outputRange: [0, 1, 1], }); React.useEffect(() => { if (visible) { // show Animated.timing(position, { duration: 250 * scale, toValue: 1, useNativeDriver: false, }).start(showCallback); } else { // hide Animated.timing(position, { duration: 200 * scale, toValue: 0, useNativeDriver: false, }).start(hideCallback); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [visible, position, scale]); const handleLayout = ({ nativeEvent }: NativeEvent) => { const { height } = nativeEvent.layout; setLayout({ height, measured: true }); }; // The banner animation has 2 parts: // 1. Blank spacer element which animates its height to move the content // 2. Actual banner which animates its translateY // In initial render, we position everything normally and measure the height of the banner // Once we have the height, we apply the height to the spacer and switch the banner to position: absolute // We need this because we need to move the content below as if banner's height was being animated // However we can't animated banner's height directly as it'll also resize the content inside const height = Animated.multiply(position, layout.height); const translateY = Animated.multiply( Animated.add(position, -1), layout.height ); return ( {icon ? ( ) : null} {children} {actions.map(({ label, ...others }, i) => ( ))} ); }; const styles = StyleSheet.create({ wrapper: { overflow: 'hidden', alignSelf: 'center', width: '100%', maxWidth: DEFAULT_MAX_WIDTH, }, absolute: { position: 'absolute', top: 0, width: '100%', }, content: { flexDirection: 'row', justifyContent: 'flex-start', marginHorizontal: 8, marginTop: 16, marginBottom: 0, }, icon: { margin: 8, }, message: { flex: 1, margin: 8, }, actions: { flexDirection: 'row', justifyContent: 'flex-end', margin: 4, }, button: { margin: 4, }, elevation: { elevation: 1, }, transparent: { opacity: 0, }, }); export default Banner;