1 | import * as React from 'react';
|
2 | import {
|
3 | Animated,
|
4 | SafeAreaView,
|
5 | StyleProp,
|
6 | StyleSheet,
|
7 | ViewStyle,
|
8 | View,
|
9 | } from 'react-native';
|
10 |
|
11 | import Button from './Button';
|
12 | import Surface from './Surface';
|
13 | import Text from './Typography/Text';
|
14 | import { withTheme } from '../core/theming';
|
15 |
|
16 | export type SnackbarProps = React.ComponentProps<typeof Surface> & {
|
17 | |
18 |
|
19 |
|
20 | visible: boolean;
|
21 | |
22 |
|
23 |
|
24 |
|
25 |
|
26 | action?: Omit<React.ComponentProps<typeof Button>, 'children'> & {
|
27 | label: string;
|
28 | };
|
29 | |
30 |
|
31 |
|
32 | duration?: number;
|
33 | |
34 |
|
35 |
|
36 | onDismiss: () => void;
|
37 | |
38 |
|
39 |
|
40 | children: React.ReactNode;
|
41 | |
42 |
|
43 |
|
44 | wrapperStyle?: StyleProp<ViewStyle>;
|
45 | style?: StyleProp<ViewStyle>;
|
46 | ref?: React.RefObject<View>;
|
47 | |
48 |
|
49 |
|
50 | theme: ReactNativePaper.Theme;
|
51 | };
|
52 |
|
53 | const DURATION_SHORT = 4000;
|
54 | const DURATION_MEDIUM = 7000;
|
55 | const DURATION_LONG = 10000;
|
56 |
|
57 |
|
58 |
|
59 |
|
60 |
|
61 |
|
62 |
|
63 |
|
64 |
|
65 |
|
66 |
|
67 |
|
68 |
|
69 |
|
70 |
|
71 |
|
72 |
|
73 |
|
74 |
|
75 |
|
76 |
|
77 |
|
78 |
|
79 |
|
80 |
|
81 |
|
82 |
|
83 |
|
84 |
|
85 |
|
86 |
|
87 |
|
88 |
|
89 |
|
90 |
|
91 |
|
92 |
|
93 |
|
94 |
|
95 |
|
96 |
|
97 |
|
98 |
|
99 |
|
100 |
|
101 |
|
102 |
|
103 |
|
104 |
|
105 | const 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 |
|
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 |
|
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 |
|
240 |
|
241 | Snackbar.DURATION_SHORT = DURATION_SHORT;
|
242 |
|
243 |
|
244 |
|
245 |
|
246 | Snackbar.DURATION_MEDIUM = DURATION_MEDIUM;
|
247 |
|
248 |
|
249 |
|
250 |
|
251 | Snackbar.DURATION_LONG = DURATION_LONG;
|
252 |
|
253 | const 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 |
|
279 | export default withTheme(Snackbar);
|