import * as React from 'react'; import { Animated, StyleProp, StyleSheet, View, ViewStyle } from 'react-native'; import { withTheme } from '../core/theming'; import shadow from '../styles/shadow'; import type { $RemoveChildren } from '../types'; import Button from './Button'; import Icon, { IconSource } from './Icon'; import Surface from './Surface'; import Text from './Typography/Text'; const ELEVATION = 1; const DEFAULT_MAX_WIDTH = 960; type Props = $RemoveChildren & { /** * Whether banner is currently visible. */ visible: boolean; /** * Content that will be displayed inside banner. */ children: string; /** * 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; } & Omit, 'children'> >; /** * Style of banner's inner content. * Use this prop to apply custom width for wide layouts. */ contentStyle?: StyleProp; style?: StyleProp; ref?: React.RefObject; /** * @optional */ theme: ReactNativePaper.Theme; /** * @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, style, theme, onShowAnimationFinished = () => {}, onHideAnimationFinished = () => {}, ...rest }: Props) => { 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 { scale } = theme.animation; React.useEffect(() => { if (visible) { // show Animated.timing(position, { duration: 250 * scale, toValue: 1, useNativeDriver: false, }).start(onShowAnimationFinished); } else { // hide Animated.timing(position, { duration: 200 * scale, toValue: 0, useNativeDriver: false, }).start(onHideAnimationFinished); } }, [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({ container: { elevation: ELEVATION, }, 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, }, }); export default withTheme(Banner);