UNPKG

6.61 kBTypeScriptView Raw
1import * as React from 'react';
2import {
3 Animated,
4 SafeAreaView,
5 StyleProp,
6 StyleSheet,
7 ViewStyle,
8 View,
9} from 'react-native';
10
11import Button from './Button';
12import Surface from './Surface';
13import Text from './Typography/Text';
14import { withTheme } from '../core/theming';
15
16export type SnackbarProps = React.ComponentProps<typeof Surface> & {
17 /**
18 * Whether the Snackbar is currently visible.
19 */
20 visible: boolean;
21 /**
22 * Label and press callback for the action button. It should contain the following properties:
23 * - `label` - Label of the action button
24 * - `onPress` - Callback that is called when action button is pressed.
25 */
26 action?: Omit<React.ComponentProps<typeof Button>, 'children'> & {
27 label: string;
28 };
29 /**
30 * The duration for which the Snackbar is shown.
31 */
32 duration?: number;
33 /**
34 * Callback called when Snackbar is dismissed. The `visible` prop needs to be updated when this is called.
35 */
36 onDismiss: () => void;
37 /**
38 * Text content of the Snackbar.
39 */
40 children: React.ReactNode;
41 /**
42 * Style for the wrapper of the snackbar
43 */
44 wrapperStyle?: StyleProp<ViewStyle>;
45 style?: StyleProp<ViewStyle>;
46 ref?: React.RefObject<View>;
47 /**
48 * @optional
49 */
50 theme: ReactNativePaper.Theme;
51};
52
53const DURATION_SHORT = 4000;
54const DURATION_MEDIUM = 7000;
55const DURATION_LONG = 10000;
56
57/**
58 * Snackbars provide brief feedback about an operation through a message at the bottom of the screen.
59 * Snackbar by default uses `onSurface` color from theme.
60 * <div class="screenshots">
61 * <img class="medium" src="screenshots/snackbar.gif" />
62 * </div>
63 *
64 * ## Usage
65 * ```js
66 * import * as React from 'react';
67 * import { View, StyleSheet } from 'react-native';
68 * import { Button, Snackbar } from 'react-native-paper';
69 *
70 * const MyComponent = () => {
71 * const [visible, setVisible] = React.useState(false);
72 *
73 * const onToggleSnackBar = () => setVisible(!visible);
74 *
75 * const onDismissSnackBar = () => setVisible(false);
76 *
77 * return (
78 * <View style={styles.container}>
79 * <Button onPress={onToggleSnackBar}>{visible ? 'Hide' : 'Show'}</Button>
80 * <Snackbar
81 * visible={visible}
82 * onDismiss={onDismissSnackBar}
83 * action={{
84 * label: 'Undo',
85 * onPress: () => {
86 * // Do something
87 * },
88 * }}>
89 * Hey there! I'm a Snackbar.
90 * </Snackbar>
91 * </View>
92 * );
93 * };
94 *
95 * const styles = StyleSheet.create({
96 * container: {
97 * flex: 1,
98 * justifyContent: 'space-between',
99 * },
100 * });
101 *
102 * export default MyComponent;
103 * ```
104 */
105const Snackbar = ({
106 visible,
107 action,
108 duration = DURATION_MEDIUM,
109 onDismiss,
110 children,
111 wrapperStyle,
112 style,
113 theme,
114 ...rest
115}: SnackbarProps) => {
116 const { current: opacity } = React.useRef<Animated.Value>(
117 new Animated.Value(0.0)
118 );
119 const [hidden, setHidden] = React.useState<boolean>(!visible);
120
121 const hideTimeout = React.useRef<NodeJS.Timeout | undefined>(undefined);
122
123 const { scale } = theme.animation;
124
125 React.useEffect(() => {
126 return () => {
127 if (hideTimeout.current) clearTimeout(hideTimeout.current);
128 };
129 }, []);
130
131 React.useLayoutEffect(() => {
132 if (visible) {
133 // show
134 if (hideTimeout.current) clearTimeout(hideTimeout.current);
135 setHidden(false);
136 Animated.timing(opacity, {
137 toValue: 1,
138 duration: 200 * scale,
139 useNativeDriver: true,
140 }).start(({ finished }) => {
141 if (finished) {
142 const isInfinity =
143 duration === Number.POSITIVE_INFINITY ||
144 duration === Number.NEGATIVE_INFINITY;
145
146 if (finished && !isInfinity) {
147 hideTimeout.current = (setTimeout(
148 onDismiss,
149 duration
150 ) as unknown) as NodeJS.Timeout;
151 }
152 }
153 });
154 } else {
155 // hide
156 if (hideTimeout.current) clearTimeout(hideTimeout.current);
157
158 Animated.timing(opacity, {
159 toValue: 0,
160 duration: 100 * scale,
161 useNativeDriver: true,
162 }).start(({ finished }) => {
163 if (finished) setHidden(true);
164 });
165 }
166 }, [visible, duration, opacity, scale, onDismiss]);
167
168 const { colors, roundness } = theme;
169
170 if (hidden) return null;
171
172 const {
173 style: actionStyle,
174 label: actionLabel,
175 onPress: onPressAction,
176 ...actionProps
177 } = action || {};
178
179 return (
180 <SafeAreaView
181 pointerEvents="box-none"
182 style={[styles.wrapper, wrapperStyle]}
183 >
184 <Surface
185 pointerEvents="box-none"
186 accessibilityLiveRegion="polite"
187 style={
188 [
189 styles.container,
190 {
191 borderRadius: roundness,
192 opacity: opacity,
193 transform: [
194 {
195 scale: visible
196 ? opacity.interpolate({
197 inputRange: [0, 1],
198 outputRange: [0.9, 1],
199 })
200 : 1,
201 },
202 ],
203 },
204 { backgroundColor: colors.onSurface },
205 style,
206 ] as StyleProp<ViewStyle>
207 }
208 {...rest}
209 >
210 <Text
211 style={[
212 styles.content,
213 { marginRight: action ? 0 : 16, color: colors.surface },
214 ]}
215 >
216 {children}
217 </Text>
218 {action ? (
219 <Button
220 onPress={() => {
221 onPressAction?.();
222 onDismiss();
223 }}
224 style={[styles.button, actionStyle]}
225 color={colors.accent}
226 compact
227 mode="text"
228 {...actionProps}
229 >
230 {actionLabel}
231 </Button>
232 ) : null}
233 </Surface>
234 </SafeAreaView>
235 );
236};
237
238/**
239 * Show the Snackbar for a short duration.
240 */
241Snackbar.DURATION_SHORT = DURATION_SHORT;
242
243/**
244 * Show the Snackbar for a medium duration.
245 */
246Snackbar.DURATION_MEDIUM = DURATION_MEDIUM;
247
248/**
249 * Show the Snackbar for a long duration.
250 */
251Snackbar.DURATION_LONG = DURATION_LONG;
252
253const styles = StyleSheet.create({
254 wrapper: {
255 position: 'absolute',
256 bottom: 0,
257 width: '100%',
258 },
259 container: {
260 elevation: 6,
261 flexDirection: 'row',
262 justifyContent: 'space-between',
263 alignItems: 'center',
264 margin: 8,
265 borderRadius: 4,
266 },
267 content: {
268 marginLeft: 16,
269 marginVertical: 14,
270 flexWrap: 'wrap',
271 flex: 1,
272 },
273 button: {
274 marginHorizontal: 8,
275 marginVertical: 6,
276 },
277});
278
279export default withTheme(Snackbar);