Skip to content

Commit c96dc87

Browse files
feat(buttons): implemented floating button
1 parent 5dd0cef commit c96dc87

File tree

5 files changed

+135
-1
lines changed

5 files changed

+135
-1
lines changed

example/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
"react-native": "0.72.4",
1414
"react-native-gesture-handler": "2.17.1",
1515
"react-native-reanimated": "3.5.1",
16-
"react-native-safe-area-context": "4.10.7"
16+
"react-native-safe-area-context": "4.10.7",
17+
"react-native-svg": "15.3.0"
1718
},
1819
"devDependencies": {
1920
"@babel/core": "^7.20.0",
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import React, {useMemo} from 'react';
2+
import Animated, {FadeInRight, LinearTransition, FadeOutRight} from 'react-native-reanimated';
3+
import {TouchableOpacity, type StyleProp, type TextStyle, type TouchableOpacityProps} from 'react-native';
4+
5+
import {useTheme} from '../../theme/useTheme.hook';
6+
import {type IconProps} from '../../icons/icon-props';
7+
import {EditIcon} from '../../icons/edit-icon/EditIcon.component';
8+
import {useTypography} from '../../typography/useTypography.component';
9+
import {getBaseFloatingButtonShape, styles} from './floating-button.styles';
10+
11+
export enum FloatingButtonSize {
12+
SMALL = 'SMALL',
13+
BIG = 'BIG',
14+
}
15+
16+
export enum FloatingButtonType {
17+
SURFACE = 'SURFACE',
18+
PRIMARY = 'PRIMARY',
19+
SECONDARY = 'SECONDARY',
20+
TERTIARY = 'TERTIARY',
21+
}
22+
23+
export interface FloatingButtonProps extends TouchableOpacityProps {
24+
type?: FloatingButtonType;
25+
label?: string;
26+
extended?: boolean;
27+
Icon?: React.FC<IconProps>;
28+
iconProps?: IconProps;
29+
size?: FloatingButtonSize;
30+
labelStyle?: StyleProp<TextStyle>;
31+
}
32+
33+
export const FloatingButton: React.FC<FloatingButtonProps> = ({
34+
label,
35+
iconProps,
36+
labelStyle,
37+
Icon = EditIcon,
38+
extended = true,
39+
size = FloatingButtonSize.SMALL,
40+
type = FloatingButtonType.PRIMARY,
41+
style,
42+
...props
43+
}) => {
44+
const {labelLarge} = useTypography();
45+
const {shadow, primaryContainer, surfaceContainer, secondaryContainer, tertiaryContainer, primary} = useTheme();
46+
47+
const colors = useMemo(
48+
() => ({
49+
[FloatingButtonType.SURFACE]: {backgroundColor: surfaceContainer.backgroundHigh, iconColor: primary.background},
50+
[FloatingButtonType.PRIMARY]: {backgroundColor: primaryContainer.background, iconColor: primaryContainer.text},
51+
[FloatingButtonType.SECONDARY]: {backgroundColor: secondaryContainer.background, iconColor: secondaryContainer.text},
52+
[FloatingButtonType.TERTIARY]: {backgroundColor: tertiaryContainer.background, iconColor: tertiaryContainer.text},
53+
}),
54+
[]
55+
);
56+
57+
const [paddingStart, paddingEnd] = label && extended ? [16, 20] : [0, 0];
58+
59+
return (
60+
<TouchableOpacity {...props}>
61+
<Animated.View
62+
layout={LinearTransition}
63+
style={[
64+
styles.container,
65+
{backgroundColor: colors[type].backgroundColor, shadowColor: shadow, paddingStart, paddingEnd},
66+
getBaseFloatingButtonShape(size).container,
67+
style,
68+
]}>
69+
{Icon ? <Icon size={24} color={colors[type].iconColor} {...iconProps} /> : null}
70+
{label && extended ? (
71+
<Animated.Text layout={LinearTransition} exiting={FadeOutRight} entering={FadeInRight} style={[labelLarge, labelStyle]}>
72+
{label}
73+
</Animated.Text>
74+
) : null}
75+
</Animated.View>
76+
</TouchableOpacity>
77+
);
78+
};
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import {StyleSheet, type ViewStyle} from 'react-native';
2+
3+
import {FloatingButtonSize} from './FloatingButton.component';
4+
5+
export const styles = StyleSheet.create({
6+
container: {
7+
position: 'absolute',
8+
zIndex: 1,
9+
bottom: 0,
10+
right: 0,
11+
12+
gap: 12,
13+
flexDirection: 'row',
14+
alignItems: 'center',
15+
justifyContent: 'center',
16+
17+
overflow: 'hidden',
18+
19+
shadowOffset: {
20+
width: 0,
21+
height: 2,
22+
},
23+
shadowOpacity: 0.15,
24+
shadowRadius: 5,
25+
elevation: 3,
26+
},
27+
});
28+
29+
export const getBaseFloatingButtonShape = (size: FloatingButtonSize) => {
30+
const buttonSizes: Record<FloatingButtonSize, ViewStyle> = {
31+
[FloatingButtonSize.SMALL]: {minWidth: 40, height: 40, borderRadius: 12},
32+
[FloatingButtonSize.BIG]: {minWidth: 56, height: 56, borderRadius: 16},
33+
};
34+
35+
return StyleSheet.create({
36+
container: buttonSizes[size] || buttonSizes[FloatingButtonSize.SMALL],
37+
});
38+
};
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import React from 'react';
2+
import Svg, {Path} from 'react-native-svg';
3+
4+
import {path} from './path.json';
5+
import type {IconProps} from '../icon-props';
6+
7+
const DEFAULT_SIZE = 24;
8+
const DEFAULT_COLOR = '#000';
9+
10+
export const EditIcon: React.FC<IconProps> = ({color = DEFAULT_COLOR, size = DEFAULT_SIZE}) => (
11+
<Svg viewBox="0 0 24 24" width={size} height={size}>
12+
<Path fill={color} d={path} />
13+
</Svg>
14+
);

src/icons/edit-icon/path.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"path": "M19.060 3.59l1.35 1.35c0.79 0.78 0.79 2.050 0 2.83l-13.23 13.23h-4.18v-4.18l13.23-13.23c0.78-0.78 2.050-0.78 2.83 0zM5 19l1.41 0.060 9.82-9.83-1.41-1.41-9.82 9.82v1.36z"
3+
}

0 commit comments

Comments
 (0)