react-native-yastools 1.0.1 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -140
- package/dist/assets/imgs/close.png +0 -0
- package/dist/components/BottomTabs/__tests__/BottomTabs.test.js +69 -0
- package/dist/components/BottomTabs/index.d.ts +5 -0
- package/dist/components/BottomTabs/index.js +82 -0
- package/dist/components/BottomTabs/styles.d.ts +43 -0
- package/dist/components/BottomTabs/styles.js +44 -0
- package/dist/components/BottomTabs/type.d.ts +53 -0
- package/dist/components/BottomTabs/type.js +1 -0
- package/dist/components/Button/__tests__/Button.test.d.ts +1 -0
- package/dist/components/Button/__tests__/{YasButton.test.js → Button.test.js} +9 -9
- package/dist/components/Button/index.d.ts +3 -3
- package/dist/components/Button/index.js +30 -20
- package/dist/components/Button/styles.d.ts +1 -1
- package/dist/components/Button/styles.js +1 -1
- package/dist/components/Button/type.d.ts +7 -3
- package/dist/components/ConfirmationPopUp/__tests__/ConfirmationPopUp.test.d.ts +1 -0
- package/dist/components/ConfirmationPopUp/__tests__/ConfirmationPopUp.test.js +62 -0
- package/dist/components/ConfirmationPopUp/index.d.ts +15 -0
- package/dist/components/ConfirmationPopUp/index.js +41 -0
- package/dist/components/ConfirmationPopUp/styles.d.ts +79 -0
- package/dist/components/ConfirmationPopUp/styles.js +81 -0
- package/dist/index.d.ts +7 -3
- package/dist/index.js +5 -3
- package/dist/interactions.d.ts +8 -0
- package/dist/interactions.js +47 -0
- package/dist/theme/index.d.ts +3 -1
- package/dist/theme/index.js +3 -1
- package/package.json +6 -4
- package/src/assets/imgs/close.png +0 -0
- package/src/components/BottomTabs/__tests__/BottomTabs.test.tsx +142 -0
- package/src/components/BottomTabs/index.tsx +134 -0
- package/src/components/BottomTabs/styles.ts +45 -0
- package/src/components/BottomTabs/type.ts +61 -0
- package/src/components/Button/__tests__/{YasButton.test.tsx → Button.test.tsx} +9 -9
- package/src/components/Button/index.tsx +39 -21
- package/src/components/Button/styles.ts +1 -1
- package/src/components/Button/type.ts +7 -3
- package/src/components/ConfirmationPopUp/__tests__/ConfirmationPopUp.test.tsx +125 -0
- package/src/components/ConfirmationPopUp/index.tsx +103 -0
- package/src/components/ConfirmationPopUp/styles.ts +83 -0
- package/src/declarations.d.ts +5 -0
- package/src/index.ts +11 -4
- package/src/interactions.ts +58 -0
- package/src/theme/index.ts +3 -1
- /package/dist/components/{Button/__tests__/YasButton.test.d.ts → BottomTabs/__tests__/BottomTabs.test.d.ts} +0 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, fireEvent } from '@testing-library/react-native';
|
|
3
|
+
import BottomTabs from '../index';
|
|
4
|
+
import { TabItem } from '../type';
|
|
5
|
+
|
|
6
|
+
// Mock navigation
|
|
7
|
+
const mockNavigate = jest.fn();
|
|
8
|
+
const mockNavigation = {
|
|
9
|
+
navigate: mockNavigate,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// Sample tabs for testing
|
|
13
|
+
const sampleTabs: TabItem[] = [
|
|
14
|
+
{ name: 'Home', route: 'Home', icon: { uri: 'https://example.com/home.png' } },
|
|
15
|
+
{ name: 'Search', route: 'Search', icon: { uri: 'https://example.com/search.png' } },
|
|
16
|
+
{ name: 'Profile', route: 'Profile', icon: { uri: 'https://example.com/profile.png' } },
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
describe('BottomTabs', () => {
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
jest.clearAllMocks();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('renders all tabs correctly', () => {
|
|
25
|
+
const { getByText } = render(
|
|
26
|
+
<BottomTabs
|
|
27
|
+
navigation={mockNavigation}
|
|
28
|
+
currentRoute="Home"
|
|
29
|
+
tabs={sampleTabs}
|
|
30
|
+
/>
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
expect(getByText('Home')).toBeTruthy();
|
|
34
|
+
expect(getByText('Search')).toBeTruthy();
|
|
35
|
+
expect(getByText('Profile')).toBeTruthy();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('navigates to correct route when tab is pressed', () => {
|
|
39
|
+
const { getByText } = render(
|
|
40
|
+
<BottomTabs
|
|
41
|
+
navigation={mockNavigation}
|
|
42
|
+
currentRoute="Home"
|
|
43
|
+
tabs={sampleTabs}
|
|
44
|
+
/>
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
fireEvent.press(getByText('Search'));
|
|
48
|
+
expect(mockNavigate).toHaveBeenCalledWith('Search');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('calls onTabPress callback when provided', () => {
|
|
52
|
+
const onTabPressMock = jest.fn();
|
|
53
|
+
const { getByText } = render(
|
|
54
|
+
<BottomTabs
|
|
55
|
+
navigation={mockNavigation}
|
|
56
|
+
currentRoute="Home"
|
|
57
|
+
tabs={sampleTabs}
|
|
58
|
+
onTabPress={onTabPressMock}
|
|
59
|
+
/>
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
fireEvent.press(getByText('Profile'));
|
|
63
|
+
expect(onTabPressMock).toHaveBeenCalledWith('Profile');
|
|
64
|
+
expect(mockNavigate).toHaveBeenCalledWith('Profile');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('does not navigate when tab is disabled', () => {
|
|
68
|
+
const disabledTabs: TabItem[] = [
|
|
69
|
+
{ name: 'Home', route: 'Home', icon: { uri: 'https://example.com/home.png' } },
|
|
70
|
+
{ name: 'Search', route: 'Search', icon: { uri: 'https://example.com/search.png' }, disabled: true },
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
const { getByText } = render(
|
|
74
|
+
<BottomTabs
|
|
75
|
+
navigation={mockNavigation}
|
|
76
|
+
currentRoute="Home"
|
|
77
|
+
tabs={disabledTabs}
|
|
78
|
+
/>
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
fireEvent.press(getByText('Search'));
|
|
82
|
+
expect(mockNavigate).not.toHaveBeenCalled();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('applies custom testID to tabs', () => {
|
|
86
|
+
const tabsWithTestId: TabItem[] = [
|
|
87
|
+
{ name: 'Home', route: 'Home', icon: { uri: 'https://example.com/home.png' }, testID: 'home-tab' },
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
const { getByTestId } = render(
|
|
91
|
+
<BottomTabs
|
|
92
|
+
navigation={mockNavigation}
|
|
93
|
+
currentRoute="Home"
|
|
94
|
+
tabs={tabsWithTestId}
|
|
95
|
+
/>
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
expect(getByTestId('home-tab')).toBeTruthy();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('renders with container testID', () => {
|
|
102
|
+
const { getByTestId } = render(
|
|
103
|
+
<BottomTabs
|
|
104
|
+
navigation={mockNavigation}
|
|
105
|
+
currentRoute="Home"
|
|
106
|
+
tabs={sampleTabs}
|
|
107
|
+
testID="bottom-tabs"
|
|
108
|
+
/>
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
expect(getByTestId('bottom-tabs')).toBeTruthy();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('renders empty when no tabs are provided', () => {
|
|
115
|
+
const { queryByText } = render(
|
|
116
|
+
<BottomTabs
|
|
117
|
+
navigation={mockNavigation}
|
|
118
|
+
currentRoute="Home"
|
|
119
|
+
tabs={[]}
|
|
120
|
+
/>
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
expect(queryByText('Home')).toBeNull();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('handles single tab correctly', () => {
|
|
127
|
+
const singleTab: TabItem[] = [
|
|
128
|
+
{ name: 'Home', route: 'Home', icon: { uri: 'https://example.com/home.png' } },
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
const { getByText, queryByText } = render(
|
|
132
|
+
<BottomTabs
|
|
133
|
+
navigation={mockNavigation}
|
|
134
|
+
currentRoute="Home"
|
|
135
|
+
tabs={singleTab}
|
|
136
|
+
/>
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
expect(getByText('Home')).toBeTruthy();
|
|
140
|
+
expect(queryByText('Search')).toBeNull();
|
|
141
|
+
});
|
|
142
|
+
});
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import React, { memo, useCallback } from 'react';
|
|
2
|
+
import { TouchableOpacity, Image, View, Text } from 'react-native';
|
|
3
|
+
import styles from './styles';
|
|
4
|
+
import { COLORS } from '../../theme';
|
|
5
|
+
import { BottomTabsProps, TabItem } from './type';
|
|
6
|
+
|
|
7
|
+
export type { BottomTabsProps, TabItem };
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* BottomTabs - A customizable bottom navigation component for React Native
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```tsx
|
|
14
|
+
* import { BottomTabs } from 'react-native-yastools';
|
|
15
|
+
*
|
|
16
|
+
* const tabs = [
|
|
17
|
+
* { name: 'Home', route: 'Home', icon: require('./icons/home.png') },
|
|
18
|
+
* { name: 'Search', route: 'Search', icon: require('./icons/search.png') },
|
|
19
|
+
* { name: 'Profile', route: 'Profile', icon: require('./icons/profile.png') },
|
|
20
|
+
* ];
|
|
21
|
+
*
|
|
22
|
+
* <BottomTabs
|
|
23
|
+
* navigation={navigation}
|
|
24
|
+
* currentRoute="Home"
|
|
25
|
+
* tabs={tabs}
|
|
26
|
+
* activeColor="#007AFF"
|
|
27
|
+
* />
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
const BottomTabs: React.FC<BottomTabsProps> = ({
|
|
31
|
+
navigation,
|
|
32
|
+
currentRoute,
|
|
33
|
+
tabs,
|
|
34
|
+
containerStyle,
|
|
35
|
+
tabItemStyle,
|
|
36
|
+
activeTabItemStyle,
|
|
37
|
+
textStyle,
|
|
38
|
+
activeTextStyle,
|
|
39
|
+
iconStyle,
|
|
40
|
+
activeIconStyle,
|
|
41
|
+
activeColor,
|
|
42
|
+
inactiveColor,
|
|
43
|
+
onTabPress,
|
|
44
|
+
activeOpacity = 0.8,
|
|
45
|
+
testID,
|
|
46
|
+
}) => {
|
|
47
|
+
const handleTabPress = useCallback(
|
|
48
|
+
(tab: TabItem) => {
|
|
49
|
+
if (tab.disabled) return;
|
|
50
|
+
|
|
51
|
+
// Call custom callback if provided
|
|
52
|
+
if (onTabPress) {
|
|
53
|
+
onTabPress(tab.route);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Navigate to the route
|
|
57
|
+
navigation.navigate(tab.route);
|
|
58
|
+
},
|
|
59
|
+
[navigation, onTabPress]
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const renderTab = useCallback(
|
|
63
|
+
(tab: TabItem, index: number) => {
|
|
64
|
+
const isActive = currentRoute === tab.route;
|
|
65
|
+
|
|
66
|
+
// Determine icon tint color
|
|
67
|
+
const iconTintColor = isActive
|
|
68
|
+
? activeColor || COLORS.primary
|
|
69
|
+
: inactiveColor || COLORS.black;
|
|
70
|
+
|
|
71
|
+
// Determine text color
|
|
72
|
+
const textColor = isActive
|
|
73
|
+
? activeColor || COLORS.primary
|
|
74
|
+
: inactiveColor || COLORS.black;
|
|
75
|
+
|
|
76
|
+
// Determine which icon to use
|
|
77
|
+
const iconSource = isActive && tab.activeIcon ? tab.activeIcon : tab.icon;
|
|
78
|
+
|
|
79
|
+
// Merge styles based on active state
|
|
80
|
+
const tabStyle = isActive
|
|
81
|
+
? [styles.navItemActive, tabItemStyle, activeTabItemStyle]
|
|
82
|
+
: [styles.navItem, tabItemStyle];
|
|
83
|
+
|
|
84
|
+
const tabTextStyle = isActive
|
|
85
|
+
? [styles.navTextActive, { color: textColor }, textStyle, activeTextStyle]
|
|
86
|
+
: [styles.navText, { color: textColor }, textStyle];
|
|
87
|
+
|
|
88
|
+
const tabIconStyle = isActive
|
|
89
|
+
? [styles.navIconActive, { tintColor: iconTintColor }, iconStyle, activeIconStyle]
|
|
90
|
+
: [styles.navIcon, { tintColor: iconTintColor }, iconStyle];
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<TouchableOpacity
|
|
94
|
+
key={`tab-${tab.route}-${index}`}
|
|
95
|
+
style={tabStyle}
|
|
96
|
+
activeOpacity={activeOpacity}
|
|
97
|
+
onPress={() => handleTabPress(tab)}
|
|
98
|
+
disabled={tab.disabled}
|
|
99
|
+
testID={tab.testID || `tab-${tab.route}`}
|
|
100
|
+
>
|
|
101
|
+
<Image
|
|
102
|
+
resizeMode="contain"
|
|
103
|
+
source={iconSource}
|
|
104
|
+
style={tabIconStyle}
|
|
105
|
+
/>
|
|
106
|
+
<Text style={tabTextStyle} numberOfLines={1}>
|
|
107
|
+
{tab.name}
|
|
108
|
+
</Text>
|
|
109
|
+
</TouchableOpacity>
|
|
110
|
+
);
|
|
111
|
+
},
|
|
112
|
+
[
|
|
113
|
+
currentRoute,
|
|
114
|
+
activeColor,
|
|
115
|
+
inactiveColor,
|
|
116
|
+
tabItemStyle,
|
|
117
|
+
activeTabItemStyle,
|
|
118
|
+
textStyle,
|
|
119
|
+
activeTextStyle,
|
|
120
|
+
iconStyle,
|
|
121
|
+
activeIconStyle,
|
|
122
|
+
activeOpacity,
|
|
123
|
+
handleTabPress,
|
|
124
|
+
]
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
<View style={[styles.BottomTabsContainer, containerStyle]} testID={testID}>
|
|
129
|
+
{tabs.map(renderTab)}
|
|
130
|
+
</View>
|
|
131
|
+
);
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
export default memo(BottomTabs);
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { StyleSheet, Platform } from 'react-native';
|
|
2
|
+
import COLORS, { FONT_SIZES, FONT_FAMILY } from '../../theme';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Default styles for the BottomTabs component
|
|
6
|
+
* Uses standard React Native StyleSheet for maximum compatibility
|
|
7
|
+
*/
|
|
8
|
+
export default StyleSheet.create({
|
|
9
|
+
BottomTabsContainer: {
|
|
10
|
+
width: '100%',
|
|
11
|
+
flexDirection: 'row',
|
|
12
|
+
backgroundColor: COLORS.white,
|
|
13
|
+
paddingBottom: Platform.OS === 'ios' ? 10 : 0,
|
|
14
|
+
},
|
|
15
|
+
navItem: {
|
|
16
|
+
flex: 1,
|
|
17
|
+
justifyContent: 'flex-end',
|
|
18
|
+
alignItems: 'center',
|
|
19
|
+
paddingVertical: 8,
|
|
20
|
+
},
|
|
21
|
+
navItemActive: {
|
|
22
|
+
flex: 1,
|
|
23
|
+
justifyContent: 'flex-end',
|
|
24
|
+
alignItems: 'center',
|
|
25
|
+
paddingVertical: 8,
|
|
26
|
+
},
|
|
27
|
+
navText: {
|
|
28
|
+
fontFamily: FONT_FAMILY.InterRegular,
|
|
29
|
+
fontSize: FONT_SIZES.f11,
|
|
30
|
+
marginTop: 5,
|
|
31
|
+
},
|
|
32
|
+
navTextActive: {
|
|
33
|
+
fontFamily: FONT_FAMILY.InterRegular,
|
|
34
|
+
fontSize: FONT_SIZES.f11,
|
|
35
|
+
marginTop: 5,
|
|
36
|
+
},
|
|
37
|
+
navIcon: {
|
|
38
|
+
width: 20,
|
|
39
|
+
height: 20,
|
|
40
|
+
},
|
|
41
|
+
navIconActive: {
|
|
42
|
+
width: 20,
|
|
43
|
+
height: 20,
|
|
44
|
+
},
|
|
45
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import {
|
|
2
|
+
StyleProp,
|
|
3
|
+
ViewStyle,
|
|
4
|
+
TextStyle,
|
|
5
|
+
ImageSourcePropType,
|
|
6
|
+
ImageStyle,
|
|
7
|
+
} from 'react-native';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Represents a single tab item in the BottomTabs component
|
|
11
|
+
*/
|
|
12
|
+
export interface TabItem {
|
|
13
|
+
/** Display name shown below the icon */
|
|
14
|
+
name: string;
|
|
15
|
+
/** Route name to navigate to when tab is pressed */
|
|
16
|
+
route: string;
|
|
17
|
+
/** Icon to display for the tab (required) */
|
|
18
|
+
icon: ImageSourcePropType;
|
|
19
|
+
/** Optional: Active state icon (if different from default) */
|
|
20
|
+
activeIcon?: ImageSourcePropType;
|
|
21
|
+
/** Optional: Whether this tab is disabled */
|
|
22
|
+
disabled?: boolean;
|
|
23
|
+
/** Optional: Test ID for testing purposes */
|
|
24
|
+
testID?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Props for the BottomTabs component
|
|
29
|
+
*/
|
|
30
|
+
export interface BottomTabsProps {
|
|
31
|
+
/** Navigation object from React Navigation */
|
|
32
|
+
navigation: any;
|
|
33
|
+
/** Currently active route name */
|
|
34
|
+
currentRoute: string;
|
|
35
|
+
/** Array of tab items to render */
|
|
36
|
+
tabs: TabItem[];
|
|
37
|
+
/** Optional: Custom styles for the container */
|
|
38
|
+
containerStyle?: StyleProp<ViewStyle>;
|
|
39
|
+
/** Optional: Custom styles for tab items */
|
|
40
|
+
tabItemStyle?: StyleProp<ViewStyle>;
|
|
41
|
+
/** Optional: Custom styles for active tab items */
|
|
42
|
+
activeTabItemStyle?: StyleProp<ViewStyle>;
|
|
43
|
+
/** Optional: Custom styles for tab text */
|
|
44
|
+
textStyle?: StyleProp<TextStyle>;
|
|
45
|
+
/** Optional: Custom styles for active tab text */
|
|
46
|
+
activeTextStyle?: StyleProp<TextStyle>;
|
|
47
|
+
/** Optional: Custom styles for tab icons */
|
|
48
|
+
iconStyle?: StyleProp<ImageStyle>;
|
|
49
|
+
/** Optional: Custom styles for active tab icons */
|
|
50
|
+
activeIconStyle?: StyleProp<ImageStyle>;
|
|
51
|
+
/** Optional: Active color for icons and text (overrides theme) */
|
|
52
|
+
activeColor?: string;
|
|
53
|
+
/** Optional: Inactive color for icons and text (overrides theme) */
|
|
54
|
+
inactiveColor?: string;
|
|
55
|
+
/** Optional: Callback when a tab is pressed (receives route name) */
|
|
56
|
+
onTabPress?: (route: string) => void;
|
|
57
|
+
/** Optional: Active opacity when pressing tabs (default: 0.8) */
|
|
58
|
+
activeOpacity?: number;
|
|
59
|
+
/** Test ID for testing purposes */
|
|
60
|
+
testID?: string;
|
|
61
|
+
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { render, fireEvent } from '@testing-library/react-native';
|
|
3
|
-
import
|
|
3
|
+
import Button from '../index';
|
|
4
4
|
|
|
5
|
-
describe('
|
|
5
|
+
describe('Button', () => {
|
|
6
6
|
it('renders correctly with text', () => {
|
|
7
7
|
const { getByText } = render(
|
|
8
|
-
<
|
|
8
|
+
<Button text="Press Me" onPress={() => {}} />
|
|
9
9
|
);
|
|
10
10
|
expect(getByText('Press Me')).toBeTruthy();
|
|
11
11
|
});
|
|
@@ -13,7 +13,7 @@ describe('YasButton', () => {
|
|
|
13
13
|
it('calls onPress when pressed', () => {
|
|
14
14
|
const onPressMock = jest.fn();
|
|
15
15
|
const { getByText } = render(
|
|
16
|
-
<
|
|
16
|
+
<Button text="Press Me" onPress={onPressMock} />
|
|
17
17
|
);
|
|
18
18
|
|
|
19
19
|
fireEvent.press(getByText('Press Me'));
|
|
@@ -23,7 +23,7 @@ describe('YasButton', () => {
|
|
|
23
23
|
it('does not call onPress when disabled', () => {
|
|
24
24
|
const onPressMock = jest.fn();
|
|
25
25
|
const { getByText } = render(
|
|
26
|
-
<
|
|
26
|
+
<Button text="Press Me" onPress={onPressMock} disabled />
|
|
27
27
|
);
|
|
28
28
|
|
|
29
29
|
fireEvent.press(getByText('Press Me'));
|
|
@@ -32,7 +32,7 @@ describe('YasButton', () => {
|
|
|
32
32
|
|
|
33
33
|
it('shows loading indicator when fetching', () => {
|
|
34
34
|
const { queryByText, getByTestId } = render(
|
|
35
|
-
<
|
|
35
|
+
<Button text="Press Me" onPress={() => {}} fetching testID="button" />
|
|
36
36
|
);
|
|
37
37
|
|
|
38
38
|
expect(queryByText('Press Me')).toBeNull();
|
|
@@ -42,7 +42,7 @@ describe('YasButton', () => {
|
|
|
42
42
|
jest.useFakeTimers();
|
|
43
43
|
const onPressMock = jest.fn();
|
|
44
44
|
const { getByText } = render(
|
|
45
|
-
<
|
|
45
|
+
<Button text="Press Me" onPress={onPressMock} debounceTime={1000} />
|
|
46
46
|
);
|
|
47
47
|
|
|
48
48
|
const button = getByText('Press Me');
|
|
@@ -62,7 +62,7 @@ describe('YasButton', () => {
|
|
|
62
62
|
|
|
63
63
|
it('applies custom primary color', () => {
|
|
64
64
|
const { getByTestId } = render(
|
|
65
|
-
<
|
|
65
|
+
<Button
|
|
66
66
|
text="Press Me"
|
|
67
67
|
onPress={() => {}}
|
|
68
68
|
primaryColor="#FF0000"
|
|
@@ -80,7 +80,7 @@ describe('YasButton', () => {
|
|
|
80
80
|
it('renders without text when only icon is provided', () => {
|
|
81
81
|
const mockIcon = { uri: 'https://example.com/icon.png' };
|
|
82
82
|
const { queryByText } = render(
|
|
83
|
-
<
|
|
83
|
+
<Button onPress={() => {}} icon={mockIcon} />
|
|
84
84
|
);
|
|
85
85
|
|
|
86
86
|
expect(queryByText('Press Me')).toBeNull();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { memo, useRef } from 'react';
|
|
1
|
+
import React, { memo, useMemo, useRef } from 'react';
|
|
2
2
|
import {
|
|
3
3
|
TouchableOpacity,
|
|
4
4
|
Text,
|
|
@@ -7,34 +7,36 @@ import {
|
|
|
7
7
|
ViewStyle,
|
|
8
8
|
View,
|
|
9
9
|
Image,
|
|
10
|
+
Animated,
|
|
10
11
|
} from 'react-native';
|
|
11
12
|
|
|
12
13
|
// Internal styles and theme
|
|
13
14
|
import styles from './styles';
|
|
14
15
|
import COLORS from '../../theme';
|
|
15
|
-
import {
|
|
16
|
+
import { ButtonProps } from './type';
|
|
17
|
+
import { preventMultiPress, usePressScale } from '../../interactions';
|
|
16
18
|
|
|
17
|
-
export type {
|
|
19
|
+
export type { ButtonProps };
|
|
18
20
|
|
|
21
|
+
const AnimatedTouchableOpacity = Animated.createAnimatedComponent(TouchableOpacity);
|
|
19
22
|
|
|
20
23
|
/**
|
|
21
|
-
*
|
|
24
|
+
* Button - A customizable button component for React Native
|
|
22
25
|
*
|
|
23
26
|
* @example
|
|
24
27
|
* ```tsx
|
|
25
|
-
* import {
|
|
28
|
+
* import { Button } from 'react-native-yastools';
|
|
26
29
|
*
|
|
27
|
-
* <
|
|
30
|
+
* <Button
|
|
28
31
|
* text="Click Me"
|
|
29
32
|
* onPress={() => console.log('Pressed!')}
|
|
30
33
|
* primaryColor="#FF6B6B"
|
|
31
34
|
* />
|
|
32
35
|
* ```
|
|
33
36
|
*/
|
|
34
|
-
const
|
|
37
|
+
const Button: React.FC<ButtonProps> = ({
|
|
35
38
|
onPress,
|
|
36
39
|
additionalStyle,
|
|
37
|
-
additonalStyle, // Support legacy prop name
|
|
38
40
|
disabled,
|
|
39
41
|
fetching,
|
|
40
42
|
text,
|
|
@@ -47,18 +49,28 @@ const YasButton: React.FC<YasButtonProps> = ({
|
|
|
47
49
|
debounceTime = 1000,
|
|
48
50
|
activeOpacity = 0.8,
|
|
49
51
|
testID,
|
|
52
|
+
animateScale,
|
|
50
53
|
}) => {
|
|
51
|
-
|
|
54
|
+
// Keep latest onPress in a ref to avoid resetting the debouncer closure when onPress changes
|
|
55
|
+
const onPressRef = useRef(onPress);
|
|
56
|
+
onPressRef.current = onPress;
|
|
52
57
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
+
// Setup interactions
|
|
59
|
+
// Default to 1 (no scaling) if not provided.
|
|
60
|
+
const { scaleAnim, handlePressIn, handlePressOut } = usePressScale(animateScale ?? 1);
|
|
61
|
+
|
|
62
|
+
// Check if animation is requested
|
|
63
|
+
const isAnimated = animateScale !== undefined;
|
|
64
|
+
|
|
65
|
+
// Memoize the handler so existing debounce timer isn't lost on re-renders
|
|
66
|
+
const handlePress = useMemo(() => {
|
|
67
|
+
// If debounceTime is 0 or less, just call directly (or use logic if user wants default protection)
|
|
68
|
+
// Assuming if user passes 0 they want NO debounce. But existing code defaulted to 1000.
|
|
69
|
+
if (debounceTime > 0) {
|
|
70
|
+
return preventMultiPress(() => onPressRef.current && onPressRef.current(), debounceTime);
|
|
58
71
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
};
|
|
72
|
+
return () => onPressRef.current && onPressRef.current();
|
|
73
|
+
}, [debounceTime]);
|
|
62
74
|
|
|
63
75
|
// Determine container style based on state
|
|
64
76
|
const containerStyle: StyleProp<ViewStyle> = [
|
|
@@ -67,14 +79,20 @@ const YasButton: React.FC<YasButtonProps> = ({
|
|
|
67
79
|
!disabled && primaryColor && { backgroundColor: primaryColor },
|
|
68
80
|
disabled && disabledColor && { backgroundColor: disabledColor },
|
|
69
81
|
// Support both prop names for backwards compatibility
|
|
70
|
-
additionalStyle
|
|
82
|
+
additionalStyle
|
|
71
83
|
];
|
|
72
84
|
|
|
85
|
+
// Combine animated style
|
|
86
|
+
const animatedStyle = isAnimated ? { transform: [{ scale: scaleAnim }] } : undefined;
|
|
87
|
+
|
|
73
88
|
return (
|
|
74
|
-
<
|
|
89
|
+
<AnimatedTouchableOpacity
|
|
75
90
|
onPress={handlePress}
|
|
91
|
+
onPressIn={isAnimated ? handlePressIn : undefined}
|
|
92
|
+
onPressOut={isAnimated ? handlePressOut : undefined}
|
|
76
93
|
activeOpacity={activeOpacity}
|
|
77
94
|
disabled={disabled || fetching}
|
|
95
|
+
style={[animatedStyle]}
|
|
78
96
|
>
|
|
79
97
|
<View style={containerStyle} testID={testID}>
|
|
80
98
|
{fetching ? (
|
|
@@ -89,8 +107,8 @@ const YasButton: React.FC<YasButtonProps> = ({
|
|
|
89
107
|
</>
|
|
90
108
|
)}
|
|
91
109
|
</View>
|
|
92
|
-
</
|
|
110
|
+
</AnimatedTouchableOpacity>
|
|
93
111
|
);
|
|
94
112
|
};
|
|
95
113
|
|
|
96
|
-
export default memo(
|
|
114
|
+
export default memo(Button);
|
|
@@ -2,7 +2,7 @@ import { StyleSheet } from 'react-native';
|
|
|
2
2
|
import COLORS, { FONT_SIZES } from '../../theme';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Default styles for the
|
|
5
|
+
* Default styles for the Button component
|
|
6
6
|
* Uses standard React Native StyleSheet for maximum compatibility
|
|
7
7
|
*/
|
|
8
8
|
export default StyleSheet.create({
|
|
@@ -6,13 +6,11 @@ import {
|
|
|
6
6
|
ImageStyle,
|
|
7
7
|
} from "react-native";
|
|
8
8
|
|
|
9
|
-
export interface
|
|
9
|
+
export interface ButtonProps {
|
|
10
10
|
/** Callback function when button is pressed */
|
|
11
11
|
onPress: () => void;
|
|
12
12
|
/** Additional styles to apply to the button container */
|
|
13
13
|
additionalStyle?: StyleProp<ViewStyle>;
|
|
14
|
-
/** @deprecated Use additionalStyle instead */
|
|
15
|
-
additonalStyle?: StyleProp<ViewStyle>;
|
|
16
14
|
/** Whether the button is disabled */
|
|
17
15
|
disabled?: boolean;
|
|
18
16
|
/** Whether to show loading indicator */
|
|
@@ -37,4 +35,10 @@ export interface YasButtonProps {
|
|
|
37
35
|
activeOpacity?: number;
|
|
38
36
|
/** Test ID for testing purposes */
|
|
39
37
|
testID?: string;
|
|
38
|
+
/**
|
|
39
|
+
* Scale value to animate to when pressed (0 to 1).
|
|
40
|
+
* If undefined, no scaling animation occurs.
|
|
41
|
+
* @example 0.95
|
|
42
|
+
*/
|
|
43
|
+
animateScale?: number;
|
|
40
44
|
}
|