Skip to content

Commit eeafe4a

Browse files
committed
feat: implement Alert component with customizable styles, dismiss, and animation support
1 parent f7bd59f commit eeafe4a

File tree

4 files changed

+268
-0
lines changed

4 files changed

+268
-0
lines changed

src/components/Alert/controller.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { useCallback } from 'react';
2+
3+
import type { ViewProps } from './types';
4+
import { alert } from '../../alert.api';
5+
6+
export const useController = <R = unknown>({
7+
onAwaitableDismiss,
8+
onDismiss,
9+
resolve,
10+
}: ViewProps<R>) => {
11+
const onDismissButtonPress = useCallback(() => {
12+
const resolveWrapper = (value: R) => {
13+
resolve(value);
14+
15+
alert.hide();
16+
};
17+
18+
if (onAwaitableDismiss) {
19+
onAwaitableDismiss(resolveWrapper);
20+
} else {
21+
onDismiss?.();
22+
23+
resolveWrapper(undefined as R);
24+
}
25+
}, [onAwaitableDismiss, onDismiss, resolve]);
26+
27+
return { onDismissButtonPress };
28+
};

src/components/Alert/index.tsx

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import { type FC, useCallback, useMemo } from 'react';
2+
import {
3+
Animated,
4+
Pressable,
5+
ScrollView,
6+
StyleSheet,
7+
Text,
8+
View,
9+
} from 'react-native';
10+
11+
import type { ViewProps } from './types';
12+
13+
// @ts-ignore: TODO: fix this
14+
import CloseIcon from '../../assets/close.svg';
15+
16+
import { useAnimation } from '../../hooks/useAnimation';
17+
import { useController } from './controller';
18+
import { styles, useContainerDimensions } from './styles';
19+
20+
export const Alert: FC<ViewProps> = (props) => {
21+
const {
22+
afterButtonsSlot,
23+
animationDuration,
24+
beforeButtonsSlot,
25+
beforeMessageSlot,
26+
beforeTitleSlot,
27+
icon,
28+
iconColor = 'black',
29+
iconSize,
30+
isDismissible,
31+
isHiding,
32+
testID,
33+
titleAlign = 'center',
34+
} = props;
35+
36+
const { animation } = useAnimation({
37+
animationDuration,
38+
isHiding,
39+
});
40+
41+
const { onDismissButtonPress } = useController(props);
42+
43+
const containerDimensions = useContainerDimensions();
44+
45+
const containerAnimation = useMemo(
46+
() => ({
47+
opacity: animation.interpolate({
48+
inputRange: [0, 1],
49+
outputRange: [0, 1],
50+
}),
51+
transform: [
52+
{
53+
scaleX: animation.interpolate({
54+
inputRange: [0, 1],
55+
outputRange: [0.8, 1],
56+
}),
57+
},
58+
{
59+
scaleY: animation.interpolate({
60+
inputRange: [0, 1],
61+
outputRange: [0.8, 1],
62+
}),
63+
},
64+
],
65+
}),
66+
[animation]
67+
);
68+
69+
const containerStyle = useMemo(
70+
() =>
71+
StyleSheet.compose(
72+
StyleSheet.compose(styles.container, containerDimensions),
73+
containerAnimation
74+
),
75+
[containerAnimation, containerDimensions]
76+
);
77+
78+
const titleStyle = useMemo(
79+
() => StyleSheet.compose(styles.title, { textAlign: titleAlign }),
80+
[titleAlign]
81+
);
82+
83+
const renderTitleCb = useCallback(() => {
84+
const title = props.title ? (
85+
<Text numberOfLines={4} style={titleStyle}>
86+
{props.title}
87+
</Text>
88+
) : (
89+
props.renderTitle?.() || null
90+
);
91+
92+
return title ? <View style={styles.titleContainer}>{title}</View> : null;
93+
}, [props, titleStyle]);
94+
95+
const renderMessageCb = useCallback(() => {
96+
const message = props.message ? (
97+
<Text>{props.message}</Text>
98+
) : (
99+
props.renderMessage?.() || null
100+
);
101+
102+
return message ? (
103+
<ScrollView bounces={false} style={styles.messageContainer}>
104+
{message}
105+
</ScrollView>
106+
) : null;
107+
}, [props]);
108+
109+
const renderIconCb = useCallback(() => {
110+
const Svg = icon;
111+
112+
return Svg ? (
113+
<Svg fill={iconColor} width={iconSize} height={iconSize} />
114+
) : null;
115+
}, [icon, iconColor, iconSize]);
116+
117+
const renderCloseButtonCb = useCallback(() => {
118+
return isDismissible ? (
119+
<Pressable onPress={onDismissButtonPress}>
120+
<CloseIcon width={24} height={24} fill="gray" />
121+
</Pressable>
122+
) : null;
123+
}, [isDismissible, onDismissButtonPress]);
124+
125+
return (
126+
<Animated.View style={containerStyle} testID={testID || 'Alert'}>
127+
{renderIconCb()}
128+
{beforeTitleSlot?.() || null}
129+
{renderTitleCb()}
130+
{beforeMessageSlot?.() || null}
131+
{renderMessageCb()}
132+
{beforeButtonsSlot?.() || null}
133+
{renderCloseButtonCb()}
134+
{/* ADD BUTTONS HERE */}
135+
{afterButtonsSlot?.() || null}
136+
</Animated.View>
137+
);
138+
};

src/components/Alert/styles.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import {
2+
StyleSheet,
3+
useWindowDimensions,
4+
type StyleProp,
5+
type ViewStyle,
6+
} from 'react-native';
7+
8+
export const useContainerDimensions = (): StyleProp<ViewStyle> => {
9+
const { width: windowWidth, height: windowHeight } = useWindowDimensions();
10+
11+
const maxWidth = 340;
12+
const maxHeight = Math.round(windowHeight * 0.85);
13+
14+
return {
15+
width: windowWidth - 10 * 2,
16+
maxWidth,
17+
maxHeight,
18+
};
19+
};
20+
21+
export const styles = StyleSheet.create({
22+
container: {
23+
borderRadius: 15,
24+
paddingTop: 17,
25+
backgroundColor: '#fff',
26+
zIndex: 7,
27+
overflow: 'hidden',
28+
marginHorizontal: 20,
29+
},
30+
titleContainer: {
31+
paddingTop: 0,
32+
paddingBottom: 15,
33+
paddingHorizontal: 20,
34+
alignItems: 'center',
35+
justifyContent: 'center',
36+
},
37+
title: {
38+
fontSize: 20,
39+
lineHeight: 28,
40+
textAlign: 'center',
41+
fontWeight: 'bold',
42+
color: '#000',
43+
flex: 1,
44+
},
45+
closeButton: {
46+
width: 28,
47+
height: 28,
48+
borderRadius: 14,
49+
position: 'absolute',
50+
right: 20,
51+
top: 20,
52+
alignItems: 'center',
53+
justifyContent: 'center',
54+
},
55+
messageContainer: {
56+
paddingHorizontal: 20,
57+
marginBottom: 20,
58+
display: 'flex',
59+
flexDirection: 'column',
60+
flexGrow: 0,
61+
},
62+
message: {
63+
fontSize: 15,
64+
lineHeight: 20,
65+
textAlign: 'center',
66+
color: '#000',
67+
},
68+
});

src/components/Alert/types.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import type { ReactElement } from 'react';
2+
import type { ColorValue } from 'react-native';
3+
4+
type IconProps = {
5+
fill?: ColorValue;
6+
width?: number;
7+
height?: number;
8+
};
9+
10+
export interface Props<R = unknown> {
11+
id?: string;
12+
testID?: string;
13+
afterButtonsSlot?: () => ReactElement<any>;
14+
beforeButtonsSlot?: () => ReactElement<any>;
15+
beforeMessageSlot?: () => ReactElement<any>;
16+
beforeTitleSlot?: () => ReactElement<any>;
17+
icon?: React.FC<IconProps>;
18+
iconColor?: ColorValue;
19+
iconSize?: number;
20+
isDismissible?: boolean;
21+
message?: string;
22+
onAwaitableDismiss?: (resolve: (value: R) => void) => void;
23+
onDismiss?: (() => Promise<R>) | (() => R);
24+
renderMessage?: () => ReactElement<any>;
25+
renderTitle?: () => ReactElement<any>;
26+
title?: string;
27+
titleAlign?: 'center' | 'left' | 'right';
28+
}
29+
30+
export type ViewProps<R = unknown> = Props<R> & {
31+
animationDuration: number;
32+
isHiding: boolean;
33+
resolve: (value?: R) => void;
34+
};

0 commit comments

Comments
 (0)