react-native-yastools 1.0.1
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 +173 -0
- package/dist/components/Button/__tests__/YasButton.test.d.ts +1 -0
- package/dist/components/Button/__tests__/YasButton.test.js +52 -0
- package/dist/components/Button/index.d.ts +5 -0
- package/dist/components/Button/index.js +50 -0
- package/dist/components/Button/styles.d.ts +36 -0
- package/dist/components/Button/styles.js +37 -0
- package/dist/components/Button/type.d.ts +33 -0
- package/dist/components/Button/type.js +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +6 -0
- package/dist/theme/index.d.ts +24 -0
- package/dist/theme/index.js +24 -0
- package/package.json +83 -0
- package/src/components/Button/__tests__/YasButton.test.tsx +88 -0
- package/src/components/Button/index.tsx +96 -0
- package/src/components/Button/styles.ts +38 -0
- package/src/components/Button/type.ts +40 -0
- package/src/index.ts +9 -0
- package/src/theme/index.ts +28 -0
package/README.md
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# Yastools React Native Utils
|
|
2
|
+
|
|
3
|
+
[](https://github.com/YassineBenZriouil/react-native-yastools/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/react-native-yastools)
|
|
5
|
+
|
|
6
|
+
A collection of reusable React Native utility components.
|
|
7
|
+
|
|
8
|
+
## Installation
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm install react-native-yastools
|
|
12
|
+
# or
|
|
13
|
+
yarn add react-native-yastools
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
### Peer Dependencies
|
|
17
|
+
|
|
18
|
+
This package requires the following peer dependencies to be installed in your project:
|
|
19
|
+
|
|
20
|
+
- `react` >= 17.0.0
|
|
21
|
+
- `react-native` >= 0.64.0
|
|
22
|
+
|
|
23
|
+
## Components
|
|
24
|
+
|
|
25
|
+
### YasButton
|
|
26
|
+
|
|
27
|
+
A customizable button component with built-in loading state, debounce functionality, and theming support.
|
|
28
|
+
|
|
29
|
+
#### Basic Usage
|
|
30
|
+
|
|
31
|
+
```tsx
|
|
32
|
+
import { YasButton } from 'react-native-yastools';
|
|
33
|
+
|
|
34
|
+
const MyComponent = () => {
|
|
35
|
+
return (
|
|
36
|
+
<YasButton
|
|
37
|
+
text="Click Me"
|
|
38
|
+
onPress={() => console.log('Button pressed!')}
|
|
39
|
+
/>
|
|
40
|
+
);
|
|
41
|
+
};
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
#### With Loading State
|
|
45
|
+
|
|
46
|
+
```tsx
|
|
47
|
+
import { YasButton } from 'react-native-yastools';
|
|
48
|
+
|
|
49
|
+
const MyComponent = () => {
|
|
50
|
+
const [loading, setLoading] = useState(false);
|
|
51
|
+
|
|
52
|
+
const handlePress = async () => {
|
|
53
|
+
setLoading(true);
|
|
54
|
+
await someAsyncOperation();
|
|
55
|
+
setLoading(false);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<YasButton
|
|
60
|
+
text="Submit"
|
|
61
|
+
onPress={handlePress}
|
|
62
|
+
fetching={loading}
|
|
63
|
+
loaderColor="#FFFFFF"
|
|
64
|
+
/>
|
|
65
|
+
);
|
|
66
|
+
};
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
#### With Custom Styling
|
|
70
|
+
|
|
71
|
+
```tsx
|
|
72
|
+
import { YasButton } from 'react-native-yastools';
|
|
73
|
+
|
|
74
|
+
const MyComponent = () => {
|
|
75
|
+
return (
|
|
76
|
+
<YasButton
|
|
77
|
+
text="Custom Button"
|
|
78
|
+
onPress={() => {}}
|
|
79
|
+
primaryColor="#FF6B6B"
|
|
80
|
+
disabledColor="#CCCCCC"
|
|
81
|
+
additionalStyle={{
|
|
82
|
+
width: 200,
|
|
83
|
+
height: 60,
|
|
84
|
+
borderRadius: 30,
|
|
85
|
+
}}
|
|
86
|
+
textStyle={{
|
|
87
|
+
fontSize: 18,
|
|
88
|
+
fontWeight: 'bold',
|
|
89
|
+
}}
|
|
90
|
+
/>
|
|
91
|
+
);
|
|
92
|
+
};
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
#### With Icon
|
|
96
|
+
|
|
97
|
+
```tsx
|
|
98
|
+
import { YasButton } from 'react-native-yastools';
|
|
99
|
+
|
|
100
|
+
const MyComponent = () => {
|
|
101
|
+
return (
|
|
102
|
+
<YasButton
|
|
103
|
+
text="Settings"
|
|
104
|
+
icon={require('./assets/settings-icon.png')}
|
|
105
|
+
onPress={() => navigation.navigate('Settings')}
|
|
106
|
+
/>
|
|
107
|
+
);
|
|
108
|
+
};
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
#### Props
|
|
112
|
+
|
|
113
|
+
| Prop | Type | Default | Description |
|
|
114
|
+
|------|------|---------|-------------|
|
|
115
|
+
| `onPress` | `() => void` | **Required** | Callback function when button is pressed |
|
|
116
|
+
| `text` | `string` | `undefined` | Text to display on the button |
|
|
117
|
+
| `icon` | `ImageSourcePropType` | `undefined` | Icon to display on the button |
|
|
118
|
+
| `disabled` | `boolean` | `false` | Whether the button is disabled |
|
|
119
|
+
| `fetching` | `boolean` | `false` | Whether to show loading indicator |
|
|
120
|
+
| `additionalStyle` | `StyleProp<ViewStyle>` | `undefined` | Additional styles for the button container |
|
|
121
|
+
| `textStyle` | `StyleProp<TextStyle>` | `undefined` | Custom styles for the button text |
|
|
122
|
+
| `iconStyle` | `StyleProp<ImageStyle>` | `undefined` | Custom styles for the icon |
|
|
123
|
+
| `loaderColor` | `string` | Theme primary | Color of the loading indicator |
|
|
124
|
+
| `primaryColor` | `string` | `#007AFF` | Custom primary/background color |
|
|
125
|
+
| `disabledColor` | `string` | `#E5E5E5` | Custom disabled background color |
|
|
126
|
+
| `debounceTime` | `number` | `1000` | Debounce time in ms to prevent multiple clicks |
|
|
127
|
+
| `activeOpacity` | `number` | `0.8` | Opacity when pressing the button |
|
|
128
|
+
| `testID` | `string` | `undefined` | Test ID for testing purposes |
|
|
129
|
+
|
|
130
|
+
## Theme Customization
|
|
131
|
+
|
|
132
|
+
You can import the default theme values to use in your own components:
|
|
133
|
+
|
|
134
|
+
```tsx
|
|
135
|
+
import { COLORS, FONT_SIZES, FONT_FAMILY } from 'react-native-yastools';
|
|
136
|
+
|
|
137
|
+
const styles = StyleSheet.create({
|
|
138
|
+
container: {
|
|
139
|
+
backgroundColor: COLORS.primary,
|
|
140
|
+
},
|
|
141
|
+
text: {
|
|
142
|
+
fontSize: FONT_SIZES.f16,
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Development
|
|
148
|
+
|
|
149
|
+
### Running Tests
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
npm test
|
|
153
|
+
|
|
154
|
+
# Watch mode
|
|
155
|
+
npm run test:watch
|
|
156
|
+
|
|
157
|
+
# Coverage report
|
|
158
|
+
npm run test:coverage
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Building
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
npm run build
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Contributing
|
|
168
|
+
|
|
169
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
170
|
+
|
|
171
|
+
## License
|
|
172
|
+
|
|
173
|
+
ISC
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, fireEvent } from '@testing-library/react-native';
|
|
3
|
+
import YasButton from '../index';
|
|
4
|
+
describe('YasButton', () => {
|
|
5
|
+
it('renders correctly with text', () => {
|
|
6
|
+
const { getByText } = render(<YasButton text="Press Me" onPress={() => { }}/>);
|
|
7
|
+
expect(getByText('Press Me')).toBeTruthy();
|
|
8
|
+
});
|
|
9
|
+
it('calls onPress when pressed', () => {
|
|
10
|
+
const onPressMock = jest.fn();
|
|
11
|
+
const { getByText } = render(<YasButton text="Press Me" onPress={onPressMock}/>);
|
|
12
|
+
fireEvent.press(getByText('Press Me'));
|
|
13
|
+
expect(onPressMock).toHaveBeenCalledTimes(1);
|
|
14
|
+
});
|
|
15
|
+
it('does not call onPress when disabled', () => {
|
|
16
|
+
const onPressMock = jest.fn();
|
|
17
|
+
const { getByText } = render(<YasButton text="Press Me" onPress={onPressMock} disabled/>);
|
|
18
|
+
fireEvent.press(getByText('Press Me'));
|
|
19
|
+
expect(onPressMock).not.toHaveBeenCalled();
|
|
20
|
+
});
|
|
21
|
+
it('shows loading indicator when fetching', () => {
|
|
22
|
+
const { queryByText, getByTestId } = render(<YasButton text="Press Me" onPress={() => { }} fetching testID="button"/>);
|
|
23
|
+
expect(queryByText('Press Me')).toBeNull();
|
|
24
|
+
});
|
|
25
|
+
it('debounces multiple rapid presses', () => {
|
|
26
|
+
jest.useFakeTimers();
|
|
27
|
+
const onPressMock = jest.fn();
|
|
28
|
+
const { getByText } = render(<YasButton text="Press Me" onPress={onPressMock} debounceTime={1000}/>);
|
|
29
|
+
const button = getByText('Press Me');
|
|
30
|
+
fireEvent.press(button);
|
|
31
|
+
fireEvent.press(button);
|
|
32
|
+
fireEvent.press(button);
|
|
33
|
+
expect(onPressMock).toHaveBeenCalledTimes(1);
|
|
34
|
+
jest.advanceTimersByTime(1000);
|
|
35
|
+
fireEvent.press(button);
|
|
36
|
+
expect(onPressMock).toHaveBeenCalledTimes(2);
|
|
37
|
+
jest.useRealTimers();
|
|
38
|
+
});
|
|
39
|
+
it('applies custom primary color', () => {
|
|
40
|
+
const { getByTestId } = render(<YasButton text="Press Me" onPress={() => { }} primaryColor="#FF0000" testID="custom-button"/>);
|
|
41
|
+
const button = getByTestId('custom-button');
|
|
42
|
+
const flattenedStyle = Array.isArray(button.props.style)
|
|
43
|
+
? button.props.style.flat().reduce((acc, style) => ({ ...acc, ...style }), {})
|
|
44
|
+
: button.props.style;
|
|
45
|
+
expect(flattenedStyle.backgroundColor).toBe('#FF0000');
|
|
46
|
+
});
|
|
47
|
+
it('renders without text when only icon is provided', () => {
|
|
48
|
+
const mockIcon = { uri: 'https://example.com/icon.png' };
|
|
49
|
+
const { queryByText } = render(<YasButton onPress={() => { }} icon={mockIcon}/>);
|
|
50
|
+
expect(queryByText('Press Me')).toBeNull();
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import React, { memo, useRef } from 'react';
|
|
2
|
+
import { TouchableOpacity, Text, ActivityIndicator, View, Image, } from 'react-native';
|
|
3
|
+
// Internal styles and theme
|
|
4
|
+
import styles from './styles';
|
|
5
|
+
import COLORS from '../../theme';
|
|
6
|
+
/**
|
|
7
|
+
* YasButton - A customizable button component for React Native
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```tsx
|
|
11
|
+
* import { YasButton } from 'react-native-yastools';
|
|
12
|
+
*
|
|
13
|
+
* <YasButton
|
|
14
|
+
* text="Click Me"
|
|
15
|
+
* onPress={() => console.log('Pressed!')}
|
|
16
|
+
* primaryColor="#FF6B6B"
|
|
17
|
+
* />
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
const YasButton = ({ onPress, additionalStyle, additonalStyle, // Support legacy prop name
|
|
21
|
+
disabled, fetching, text, textStyle, icon, iconStyle, loaderColor, primaryColor, disabledColor, debounceTime = 1000, activeOpacity = 0.8, testID, }) => {
|
|
22
|
+
const lastPressTime = useRef(0);
|
|
23
|
+
const handlePress = () => {
|
|
24
|
+
// Prevent multiple clicks with configurable debounce
|
|
25
|
+
const now = Date.now();
|
|
26
|
+
if (now - lastPressTime.current < debounceTime) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
lastPressTime.current = now;
|
|
30
|
+
onPress();
|
|
31
|
+
};
|
|
32
|
+
// Determine container style based on state
|
|
33
|
+
const containerStyle = [
|
|
34
|
+
disabled ? styles.disabled : styles.container,
|
|
35
|
+
// Apply custom colors if provided
|
|
36
|
+
!disabled && primaryColor && { backgroundColor: primaryColor },
|
|
37
|
+
disabled && disabledColor && { backgroundColor: disabledColor },
|
|
38
|
+
// Support both prop names for backwards compatibility
|
|
39
|
+
additionalStyle || additonalStyle,
|
|
40
|
+
];
|
|
41
|
+
return (<TouchableOpacity onPress={handlePress} activeOpacity={activeOpacity} disabled={disabled || fetching}>
|
|
42
|
+
<View style={containerStyle} testID={testID}>
|
|
43
|
+
{fetching ? (<ActivityIndicator size="small" color={loaderColor || primaryColor || COLORS.primary}/>) : (<>
|
|
44
|
+
{text && <Text style={[styles.text, textStyle]}>{text}</Text>}
|
|
45
|
+
{icon && <Image source={icon} style={[styles.icon, iconStyle]}/>}
|
|
46
|
+
</>)}
|
|
47
|
+
</View>
|
|
48
|
+
</TouchableOpacity>);
|
|
49
|
+
};
|
|
50
|
+
export default memo(YasButton);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default styles for the YasButton component
|
|
3
|
+
* Uses standard React Native StyleSheet for maximum compatibility
|
|
4
|
+
*/
|
|
5
|
+
declare const _default: {
|
|
6
|
+
container: {
|
|
7
|
+
width: number;
|
|
8
|
+
height: number;
|
|
9
|
+
justifyContent: "center";
|
|
10
|
+
alignItems: "center";
|
|
11
|
+
borderRadius: number;
|
|
12
|
+
backgroundColor: string;
|
|
13
|
+
flexDirection: "row";
|
|
14
|
+
gap: number;
|
|
15
|
+
};
|
|
16
|
+
disabled: {
|
|
17
|
+
width: number;
|
|
18
|
+
height: number;
|
|
19
|
+
justifyContent: "center";
|
|
20
|
+
alignItems: "center";
|
|
21
|
+
borderRadius: number;
|
|
22
|
+
backgroundColor: string;
|
|
23
|
+
flexDirection: "row";
|
|
24
|
+
gap: number;
|
|
25
|
+
};
|
|
26
|
+
text: {
|
|
27
|
+
fontSize: number;
|
|
28
|
+
color: string;
|
|
29
|
+
fontWeight: "600";
|
|
30
|
+
};
|
|
31
|
+
icon: {
|
|
32
|
+
width: number;
|
|
33
|
+
height: number;
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
export default _default;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { StyleSheet } from 'react-native';
|
|
2
|
+
import COLORS, { FONT_SIZES } from '../../theme';
|
|
3
|
+
/**
|
|
4
|
+
* Default styles for the YasButton component
|
|
5
|
+
* Uses standard React Native StyleSheet for maximum compatibility
|
|
6
|
+
*/
|
|
7
|
+
export default StyleSheet.create({
|
|
8
|
+
container: {
|
|
9
|
+
width: 176,
|
|
10
|
+
height: 53,
|
|
11
|
+
justifyContent: 'center',
|
|
12
|
+
alignItems: 'center',
|
|
13
|
+
borderRadius: 8,
|
|
14
|
+
backgroundColor: COLORS.primary,
|
|
15
|
+
flexDirection: 'row',
|
|
16
|
+
gap: 20,
|
|
17
|
+
},
|
|
18
|
+
disabled: {
|
|
19
|
+
width: 176,
|
|
20
|
+
height: 53,
|
|
21
|
+
justifyContent: 'center',
|
|
22
|
+
alignItems: 'center',
|
|
23
|
+
borderRadius: 8,
|
|
24
|
+
backgroundColor: COLORS.grayBg,
|
|
25
|
+
flexDirection: 'row',
|
|
26
|
+
gap: 20,
|
|
27
|
+
},
|
|
28
|
+
text: {
|
|
29
|
+
fontSize: FONT_SIZES.f16,
|
|
30
|
+
color: COLORS.white,
|
|
31
|
+
fontWeight: '600',
|
|
32
|
+
},
|
|
33
|
+
icon: {
|
|
34
|
+
width: 24,
|
|
35
|
+
height: 26,
|
|
36
|
+
},
|
|
37
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { StyleProp, ViewStyle, TextStyle, ImageSourcePropType, ImageStyle } from "react-native";
|
|
2
|
+
export interface YasButtonProps {
|
|
3
|
+
/** Callback function when button is pressed */
|
|
4
|
+
onPress: () => void;
|
|
5
|
+
/** Additional styles to apply to the button container */
|
|
6
|
+
additionalStyle?: StyleProp<ViewStyle>;
|
|
7
|
+
/** @deprecated Use additionalStyle instead */
|
|
8
|
+
additonalStyle?: StyleProp<ViewStyle>;
|
|
9
|
+
/** Whether the button is disabled */
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
/** Whether to show loading indicator */
|
|
12
|
+
fetching?: boolean;
|
|
13
|
+
/** Text to display on the button */
|
|
14
|
+
text?: string;
|
|
15
|
+
/** Icon to display on the button */
|
|
16
|
+
icon?: ImageSourcePropType;
|
|
17
|
+
/** Custom styles for the button text */
|
|
18
|
+
textStyle?: StyleProp<TextStyle>;
|
|
19
|
+
/** Custom styles for the icon */
|
|
20
|
+
iconStyle?: StyleProp<ImageStyle>;
|
|
21
|
+
/** Color of the loading indicator (defaults to theme primary color) */
|
|
22
|
+
loaderColor?: string;
|
|
23
|
+
/** Custom primary color to override theme */
|
|
24
|
+
primaryColor?: string;
|
|
25
|
+
/** Custom disabled background color to override theme */
|
|
26
|
+
disabledColor?: string;
|
|
27
|
+
/** Debounce time in milliseconds to prevent multiple clicks (default: 1000) */
|
|
28
|
+
debounceTime?: number;
|
|
29
|
+
/** Active opacity when pressing the button (default: 0.8) */
|
|
30
|
+
activeOpacity?: number;
|
|
31
|
+
/** Test ID for testing purposes */
|
|
32
|
+
testID?: string;
|
|
33
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Components
|
|
2
|
+
export { default as YasButton } from './components/Button';
|
|
3
|
+
// Theme utilities (for customization)
|
|
4
|
+
export { default as COLORS, FONT_FAMILY, FONT_SIZES } from './theme';
|
|
5
|
+
// Re-export Button as default for convenience
|
|
6
|
+
export { default } from './components/Button';
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default theme configuration for YasButton
|
|
3
|
+
* Users can override these values by passing custom props
|
|
4
|
+
*/
|
|
5
|
+
export declare const COLORS: {
|
|
6
|
+
primary: string;
|
|
7
|
+
white: string;
|
|
8
|
+
grayBg: string;
|
|
9
|
+
gray: string;
|
|
10
|
+
};
|
|
11
|
+
export declare const FONT_FAMILY: {
|
|
12
|
+
InterSemiBold: string;
|
|
13
|
+
InterRegular: string;
|
|
14
|
+
InterBold: string;
|
|
15
|
+
};
|
|
16
|
+
export declare const FONT_SIZES: {
|
|
17
|
+
f12: number;
|
|
18
|
+
f14: number;
|
|
19
|
+
f16: number;
|
|
20
|
+
f18: number;
|
|
21
|
+
f20: number;
|
|
22
|
+
f24: number;
|
|
23
|
+
};
|
|
24
|
+
export default COLORS;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default theme configuration for YasButton
|
|
3
|
+
* Users can override these values by passing custom props
|
|
4
|
+
*/
|
|
5
|
+
export const COLORS = {
|
|
6
|
+
primary: '#007AFF',
|
|
7
|
+
white: '#FFFFFF',
|
|
8
|
+
grayBg: '#E5E5E5',
|
|
9
|
+
gray: '#666666',
|
|
10
|
+
};
|
|
11
|
+
export const FONT_FAMILY = {
|
|
12
|
+
InterSemiBold: 'Inter-SemiBold',
|
|
13
|
+
InterRegular: 'Inter-Regular',
|
|
14
|
+
InterBold: 'Inter-Bold',
|
|
15
|
+
};
|
|
16
|
+
export const FONT_SIZES = {
|
|
17
|
+
f12: 12,
|
|
18
|
+
f14: 14,
|
|
19
|
+
f16: 16,
|
|
20
|
+
f18: 18,
|
|
21
|
+
f20: 20,
|
|
22
|
+
f24: 24,
|
|
23
|
+
};
|
|
24
|
+
export default COLORS;
|
package/package.json
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "react-native-yastools",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"main": "dist/index.js",
|
|
5
|
+
"module": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist",
|
|
9
|
+
"src"
|
|
10
|
+
],
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "tsc",
|
|
13
|
+
"prepare": "npm run build",
|
|
14
|
+
"test": "jest",
|
|
15
|
+
"test:watch": "jest --watch",
|
|
16
|
+
"test:coverage": "jest --coverage"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"react-native",
|
|
20
|
+
"components",
|
|
21
|
+
"button",
|
|
22
|
+
"utils"
|
|
23
|
+
],
|
|
24
|
+
"author": "",
|
|
25
|
+
"license": "ISC",
|
|
26
|
+
"description": "React Native Utility Files and Utility Components ",
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"react": ">=17.0.0",
|
|
29
|
+
"react-native": ">=0.64.0"
|
|
30
|
+
},
|
|
31
|
+
"peerDependenciesMeta": {
|
|
32
|
+
"react": {
|
|
33
|
+
"optional": false
|
|
34
|
+
},
|
|
35
|
+
"react-native": {
|
|
36
|
+
"optional": false
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@babel/core": "^7.24.0",
|
|
41
|
+
"@babel/preset-env": "^7.24.0",
|
|
42
|
+
"@babel/preset-react": "^7.24.0",
|
|
43
|
+
"@babel/preset-flow": "^7.24.0",
|
|
44
|
+
"@babel/preset-typescript": "^7.24.0",
|
|
45
|
+
"@testing-library/react-native": "^12.4.0",
|
|
46
|
+
"@types/jest": "^29.5.0",
|
|
47
|
+
"@types/react": "^18.2.0",
|
|
48
|
+
"@types/react-native": "^0.72.0",
|
|
49
|
+
"jest": "^29.7.0",
|
|
50
|
+
"react": "18.2.0",
|
|
51
|
+
"react-native": "^0.73.0",
|
|
52
|
+
"react-test-renderer": "18.2.0",
|
|
53
|
+
"typescript": "^5.0.0"
|
|
54
|
+
},
|
|
55
|
+
"jest": {
|
|
56
|
+
"preset": "react-native",
|
|
57
|
+
"moduleFileExtensions": [
|
|
58
|
+
"ts",
|
|
59
|
+
"tsx",
|
|
60
|
+
"js",
|
|
61
|
+
"jsx",
|
|
62
|
+
"json"
|
|
63
|
+
],
|
|
64
|
+
"testMatch": [
|
|
65
|
+
"<rootDir>/src/**/__tests__/**/*.(ts|tsx|js)",
|
|
66
|
+
"<rootDir>/src/**/*.(test|spec).(ts|tsx|js)"
|
|
67
|
+
],
|
|
68
|
+
"testPathIgnorePatterns": [
|
|
69
|
+
"/node_modules/",
|
|
70
|
+
"/dist/"
|
|
71
|
+
],
|
|
72
|
+
"transformIgnorePatterns": [
|
|
73
|
+
"node_modules/(?!((jest-)?react-native|@react-native(-community)?)/)"
|
|
74
|
+
],
|
|
75
|
+
"setupFilesAfterEnv": [
|
|
76
|
+
"<rootDir>/jest.setup.js"
|
|
77
|
+
],
|
|
78
|
+
"collectCoverageFrom": [
|
|
79
|
+
"src/**/*.{ts,tsx}",
|
|
80
|
+
"!src/**/*.d.ts"
|
|
81
|
+
]
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, fireEvent } from '@testing-library/react-native';
|
|
3
|
+
import YasButton from '../index';
|
|
4
|
+
|
|
5
|
+
describe('YasButton', () => {
|
|
6
|
+
it('renders correctly with text', () => {
|
|
7
|
+
const { getByText } = render(
|
|
8
|
+
<YasButton text="Press Me" onPress={() => {}} />
|
|
9
|
+
);
|
|
10
|
+
expect(getByText('Press Me')).toBeTruthy();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('calls onPress when pressed', () => {
|
|
14
|
+
const onPressMock = jest.fn();
|
|
15
|
+
const { getByText } = render(
|
|
16
|
+
<YasButton text="Press Me" onPress={onPressMock} />
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
fireEvent.press(getByText('Press Me'));
|
|
20
|
+
expect(onPressMock).toHaveBeenCalledTimes(1);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('does not call onPress when disabled', () => {
|
|
24
|
+
const onPressMock = jest.fn();
|
|
25
|
+
const { getByText } = render(
|
|
26
|
+
<YasButton text="Press Me" onPress={onPressMock} disabled />
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
fireEvent.press(getByText('Press Me'));
|
|
30
|
+
expect(onPressMock).not.toHaveBeenCalled();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('shows loading indicator when fetching', () => {
|
|
34
|
+
const { queryByText, getByTestId } = render(
|
|
35
|
+
<YasButton text="Press Me" onPress={() => {}} fetching testID="button" />
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
expect(queryByText('Press Me')).toBeNull();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('debounces multiple rapid presses', () => {
|
|
42
|
+
jest.useFakeTimers();
|
|
43
|
+
const onPressMock = jest.fn();
|
|
44
|
+
const { getByText } = render(
|
|
45
|
+
<YasButton text="Press Me" onPress={onPressMock} debounceTime={1000} />
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const button = getByText('Press Me');
|
|
49
|
+
|
|
50
|
+
fireEvent.press(button);
|
|
51
|
+
fireEvent.press(button);
|
|
52
|
+
fireEvent.press(button);
|
|
53
|
+
|
|
54
|
+
expect(onPressMock).toHaveBeenCalledTimes(1);
|
|
55
|
+
|
|
56
|
+
jest.advanceTimersByTime(1000);
|
|
57
|
+
fireEvent.press(button);
|
|
58
|
+
|
|
59
|
+
expect(onPressMock).toHaveBeenCalledTimes(2);
|
|
60
|
+
jest.useRealTimers();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('applies custom primary color', () => {
|
|
64
|
+
const { getByTestId } = render(
|
|
65
|
+
<YasButton
|
|
66
|
+
text="Press Me"
|
|
67
|
+
onPress={() => {}}
|
|
68
|
+
primaryColor="#FF0000"
|
|
69
|
+
testID="custom-button"
|
|
70
|
+
/>
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const button = getByTestId('custom-button');
|
|
74
|
+
const flattenedStyle = Array.isArray(button.props.style)
|
|
75
|
+
? button.props.style.flat().reduce((acc: object, style: object) => ({ ...acc, ...style }), {})
|
|
76
|
+
: button.props.style;
|
|
77
|
+
expect(flattenedStyle.backgroundColor).toBe('#FF0000');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('renders without text when only icon is provided', () => {
|
|
81
|
+
const mockIcon = { uri: 'https://example.com/icon.png' };
|
|
82
|
+
const { queryByText } = render(
|
|
83
|
+
<YasButton onPress={() => {}} icon={mockIcon} />
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
expect(queryByText('Press Me')).toBeNull();
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import React, { memo, useRef } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
TouchableOpacity,
|
|
4
|
+
Text,
|
|
5
|
+
ActivityIndicator,
|
|
6
|
+
StyleProp,
|
|
7
|
+
ViewStyle,
|
|
8
|
+
View,
|
|
9
|
+
Image,
|
|
10
|
+
} from 'react-native';
|
|
11
|
+
|
|
12
|
+
// Internal styles and theme
|
|
13
|
+
import styles from './styles';
|
|
14
|
+
import COLORS from '../../theme';
|
|
15
|
+
import { YasButtonProps } from './type';
|
|
16
|
+
|
|
17
|
+
export type { YasButtonProps };
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* YasButton - A customizable button component for React Native
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```tsx
|
|
25
|
+
* import { YasButton } from 'react-native-yastools';
|
|
26
|
+
*
|
|
27
|
+
* <YasButton
|
|
28
|
+
* text="Click Me"
|
|
29
|
+
* onPress={() => console.log('Pressed!')}
|
|
30
|
+
* primaryColor="#FF6B6B"
|
|
31
|
+
* />
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
const YasButton: React.FC<YasButtonProps> = ({
|
|
35
|
+
onPress,
|
|
36
|
+
additionalStyle,
|
|
37
|
+
additonalStyle, // Support legacy prop name
|
|
38
|
+
disabled,
|
|
39
|
+
fetching,
|
|
40
|
+
text,
|
|
41
|
+
textStyle,
|
|
42
|
+
icon,
|
|
43
|
+
iconStyle,
|
|
44
|
+
loaderColor,
|
|
45
|
+
primaryColor,
|
|
46
|
+
disabledColor,
|
|
47
|
+
debounceTime = 1000,
|
|
48
|
+
activeOpacity = 0.8,
|
|
49
|
+
testID,
|
|
50
|
+
}) => {
|
|
51
|
+
const lastPressTime = useRef(0);
|
|
52
|
+
|
|
53
|
+
const handlePress = () => {
|
|
54
|
+
// Prevent multiple clicks with configurable debounce
|
|
55
|
+
const now = Date.now();
|
|
56
|
+
if (now - lastPressTime.current < debounceTime) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
lastPressTime.current = now;
|
|
60
|
+
onPress();
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// Determine container style based on state
|
|
64
|
+
const containerStyle: StyleProp<ViewStyle> = [
|
|
65
|
+
disabled ? styles.disabled : styles.container,
|
|
66
|
+
// Apply custom colors if provided
|
|
67
|
+
!disabled && primaryColor && { backgroundColor: primaryColor },
|
|
68
|
+
disabled && disabledColor && { backgroundColor: disabledColor },
|
|
69
|
+
// Support both prop names for backwards compatibility
|
|
70
|
+
additionalStyle || additonalStyle,
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<TouchableOpacity
|
|
75
|
+
onPress={handlePress}
|
|
76
|
+
activeOpacity={activeOpacity}
|
|
77
|
+
disabled={disabled || fetching}
|
|
78
|
+
>
|
|
79
|
+
<View style={containerStyle} testID={testID}>
|
|
80
|
+
{fetching ? (
|
|
81
|
+
<ActivityIndicator
|
|
82
|
+
size="small"
|
|
83
|
+
color={loaderColor || primaryColor || COLORS.primary}
|
|
84
|
+
/>
|
|
85
|
+
) : (
|
|
86
|
+
<>
|
|
87
|
+
{text && <Text style={[styles.text, textStyle]}>{text}</Text>}
|
|
88
|
+
{icon && <Image source={icon} style={[styles.icon, iconStyle]} />}
|
|
89
|
+
</>
|
|
90
|
+
)}
|
|
91
|
+
</View>
|
|
92
|
+
</TouchableOpacity>
|
|
93
|
+
);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export default memo(YasButton);
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { StyleSheet } from 'react-native';
|
|
2
|
+
import COLORS, { FONT_SIZES } from '../../theme';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Default styles for the YasButton component
|
|
6
|
+
* Uses standard React Native StyleSheet for maximum compatibility
|
|
7
|
+
*/
|
|
8
|
+
export default StyleSheet.create({
|
|
9
|
+
container: {
|
|
10
|
+
width: 176,
|
|
11
|
+
height: 53,
|
|
12
|
+
justifyContent: 'center',
|
|
13
|
+
alignItems: 'center',
|
|
14
|
+
borderRadius: 8,
|
|
15
|
+
backgroundColor: COLORS.primary,
|
|
16
|
+
flexDirection: 'row',
|
|
17
|
+
gap: 20,
|
|
18
|
+
},
|
|
19
|
+
disabled: {
|
|
20
|
+
width: 176,
|
|
21
|
+
height: 53,
|
|
22
|
+
justifyContent: 'center',
|
|
23
|
+
alignItems: 'center',
|
|
24
|
+
borderRadius: 8,
|
|
25
|
+
backgroundColor: COLORS.grayBg,
|
|
26
|
+
flexDirection: 'row',
|
|
27
|
+
gap: 20,
|
|
28
|
+
},
|
|
29
|
+
text: {
|
|
30
|
+
fontSize: FONT_SIZES.f16,
|
|
31
|
+
color: COLORS.white,
|
|
32
|
+
fontWeight: '600',
|
|
33
|
+
},
|
|
34
|
+
icon: {
|
|
35
|
+
width: 24,
|
|
36
|
+
height: 26,
|
|
37
|
+
},
|
|
38
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import {
|
|
2
|
+
StyleProp,
|
|
3
|
+
ViewStyle,
|
|
4
|
+
TextStyle,
|
|
5
|
+
ImageSourcePropType,
|
|
6
|
+
ImageStyle,
|
|
7
|
+
} from "react-native";
|
|
8
|
+
|
|
9
|
+
export interface YasButtonProps {
|
|
10
|
+
/** Callback function when button is pressed */
|
|
11
|
+
onPress: () => void;
|
|
12
|
+
/** Additional styles to apply to the button container */
|
|
13
|
+
additionalStyle?: StyleProp<ViewStyle>;
|
|
14
|
+
/** @deprecated Use additionalStyle instead */
|
|
15
|
+
additonalStyle?: StyleProp<ViewStyle>;
|
|
16
|
+
/** Whether the button is disabled */
|
|
17
|
+
disabled?: boolean;
|
|
18
|
+
/** Whether to show loading indicator */
|
|
19
|
+
fetching?: boolean;
|
|
20
|
+
/** Text to display on the button */
|
|
21
|
+
text?: string;
|
|
22
|
+
/** Icon to display on the button */
|
|
23
|
+
icon?: ImageSourcePropType;
|
|
24
|
+
/** Custom styles for the button text */
|
|
25
|
+
textStyle?: StyleProp<TextStyle>;
|
|
26
|
+
/** Custom styles for the icon */
|
|
27
|
+
iconStyle?: StyleProp<ImageStyle>;
|
|
28
|
+
/** Color of the loading indicator (defaults to theme primary color) */
|
|
29
|
+
loaderColor?: string;
|
|
30
|
+
/** Custom primary color to override theme */
|
|
31
|
+
primaryColor?: string;
|
|
32
|
+
/** Custom disabled background color to override theme */
|
|
33
|
+
disabledColor?: string;
|
|
34
|
+
/** Debounce time in milliseconds to prevent multiple clicks (default: 1000) */
|
|
35
|
+
debounceTime?: number;
|
|
36
|
+
/** Active opacity when pressing the button (default: 0.8) */
|
|
37
|
+
activeOpacity?: number;
|
|
38
|
+
/** Test ID for testing purposes */
|
|
39
|
+
testID?: string;
|
|
40
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Components
|
|
2
|
+
export { default as YasButton } from './components/Button';
|
|
3
|
+
export type { YasButtonProps } from './components/Button';
|
|
4
|
+
|
|
5
|
+
// Theme utilities (for customization)
|
|
6
|
+
export { default as COLORS, FONT_FAMILY, FONT_SIZES } from './theme';
|
|
7
|
+
|
|
8
|
+
// Re-export Button as default for convenience
|
|
9
|
+
export { default } from './components/Button';
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default theme configuration for YasButton
|
|
3
|
+
* Users can override these values by passing custom props
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const COLORS = {
|
|
7
|
+
primary: '#007AFF',
|
|
8
|
+
white: '#FFFFFF',
|
|
9
|
+
grayBg: '#E5E5E5',
|
|
10
|
+
gray: '#666666',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const FONT_FAMILY = {
|
|
14
|
+
InterSemiBold: 'Inter-SemiBold',
|
|
15
|
+
InterRegular: 'Inter-Regular',
|
|
16
|
+
InterBold: 'Inter-Bold',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const FONT_SIZES = {
|
|
20
|
+
f12: 12,
|
|
21
|
+
f14: 14,
|
|
22
|
+
f16: 16,
|
|
23
|
+
f18: 18,
|
|
24
|
+
f20: 20,
|
|
25
|
+
f24: 24,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export default COLORS;
|