@compsych/mobile-ui 1.0.8 → 1.0.10
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/package.json +40 -2
- package/src/components/ActionSheet/ActionSheet.test.tsx +89 -0
- package/src/{ActionSheet.tsx → components/ActionSheet/index.tsx} +16 -8
- package/src/components/Alert/Alert.test.tsx +61 -0
- package/src/{Alert.tsx → components/Alert/index.tsx} +14 -11
- package/src/components/Avatar/Avatar.test.tsx +52 -0
- package/src/{Avatar.tsx → components/Avatar/index.tsx} +15 -13
- package/src/components/Badge/Badge.test.tsx +30 -0
- package/src/{Badge.tsx → components/Badge/index.tsx} +17 -9
- package/src/components/BodyText/BodyText.test.tsx +30 -0
- package/src/{BodyText.tsx → components/BodyText/index.tsx} +9 -5
- package/src/components/Breadcrumb/Breadcrumb.test.tsx +37 -0
- package/src/{Breadcrumb.tsx → components/Breadcrumb/index.tsx} +13 -4
- package/src/components/Button/Button.test.tsx +45 -0
- package/src/{Button.tsx → components/Button/index.tsx} +36 -17
- package/src/components/Checkbox/Checkbox.test.tsx +57 -0
- package/src/{Checkbox.tsx → components/Checkbox/index.tsx} +37 -15
- package/src/components/Chip/Chip.test.tsx +40 -0
- package/src/{Chip.tsx → components/Chip/index.tsx} +18 -9
- package/src/components/Divider/Divider.test.tsx +28 -0
- package/src/{Divider.tsx → components/Divider/index.tsx} +30 -20
- package/src/components/EmptyState/EmptyState.test.tsx +60 -0
- package/src/{EmptyState.tsx → components/EmptyState/index.tsx} +17 -7
- package/src/components/HeaderText/HeaderText.test.tsx +43 -0
- package/src/{HeaderText.tsx → components/HeaderText/index.tsx} +9 -5
- package/src/components/Input/Input.test.tsx +43 -0
- package/src/{Input.tsx → components/Input/index.tsx} +31 -11
- package/src/components/List/List.test.tsx +59 -0
- package/src/{List.tsx → components/List/index.tsx} +23 -7
- package/src/components/Pagination/Pagination.test.tsx +56 -0
- package/src/{Pagination.tsx → components/Pagination/index.tsx} +29 -13
- package/src/components/PlanCard/PlanCard.test.tsx +44 -0
- package/src/{PlanCard.tsx → components/PlanCard/index.tsx} +46 -17
- package/src/components/ProgressTracker/ProgressTracker.test.tsx +47 -0
- package/src/{ProgressTracker.tsx → components/ProgressTracker/index.tsx} +16 -25
- package/src/components/PromotionCard/PromotionCard.test.tsx +73 -0
- package/src/components/PromotionCard/index.tsx +538 -0
- package/src/components/RadioButton/RadioButton.test.tsx +50 -0
- package/src/{RadioButton.tsx → components/RadioButton/index.tsx} +21 -10
- package/src/components/ScreenContainer/ScreenContainer.test.tsx +29 -0
- package/src/components/ScreenContainer/index.tsx +41 -0
- package/src/components/SegmentedControl/SegmentedControl.test.tsx +60 -0
- package/src/{SegmentedControl.tsx → components/SegmentedControl/index.tsx} +15 -7
- package/src/components/SelectionCard/SelectionCard.test.tsx +48 -0
- package/src/components/SelectionCard/index.tsx +213 -0
- package/src/components/ServiceCard/ServiceCard.test.tsx +61 -0
- package/src/{Card.tsx → components/ServiceCard/index.tsx} +106 -49
- package/src/components/Slider/Slider.test.tsx +33 -0
- package/src/{Slider.tsx → components/Slider/index.tsx} +26 -36
- package/src/components/Snackbar/Snackbar.test.tsx +61 -0
- package/src/{Snackbar.tsx → components/Snackbar/index.tsx} +13 -7
- package/src/components/Switch/Switch.test.tsx +54 -0
- package/src/{Switch.tsx → components/Switch/index.tsx} +16 -5
- package/src/components/Tooltip/Tooltip.test.tsx +30 -0
- package/src/{Tooltip.tsx → components/Tooltip/index.tsx} +11 -7
- package/src/icons/AtomIcon.tsx +32 -0
- package/src/icons/BinocularsIcon.tsx +45 -0
- package/src/icons/FileChartColumnIncreasingIcon.tsx +44 -0
- package/src/icons/FlagIcon.tsx +20 -0
- package/src/icons/GlobeIcon.tsx +20 -0
- package/src/icons/GraduationCapIcon.tsx +32 -0
- package/src/icons/HandHeartIcon.tsx +38 -0
- package/src/icons/HandshakeIcon.tsx +39 -0
- package/src/icons/HazeIcon.tsx +42 -0
- package/src/icons/HeartHandshakeIcon.tsx +20 -0
- package/src/icons/HourglassIcon.tsx +33 -0
- package/src/icons/IdCardIcon.tsx +44 -0
- package/src/icons/MessageCirclePlusIcon.tsx +32 -0
- package/src/icons/MountainSnowIcon.tsx +26 -0
- package/src/icons/SnowflakeIcon.tsx +86 -0
- package/src/icons/StethoscopeIcon.tsx +34 -0
- package/src/icons/UserRoundIcon.tsx +26 -0
- package/src/icons/WheatIcon.tsx +62 -0
- package/src/icons/index.ts +63 -0
- package/src/icons/types.ts +45 -0
- package/src/index.ts +159 -71
- package/src/tokens.ts +135 -15
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
import { fireEvent, render } from '@testing-library/react-native';
|
|
4
|
+
|
|
5
|
+
import { Breadcrumb } from './index';
|
|
6
|
+
|
|
7
|
+
const items = [
|
|
8
|
+
{ label: 'Home', onPress: jest.fn() },
|
|
9
|
+
{ label: 'Settings', onPress: jest.fn() },
|
|
10
|
+
{ label: 'Profile' },
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
describe('Breadcrumb', () => {
|
|
14
|
+
it('renders all item labels', () => {
|
|
15
|
+
const { getByText } = render(<Breadcrumb items={items} />);
|
|
16
|
+
expect(getByText('Home')).toBeTruthy();
|
|
17
|
+
expect(getByText('Settings')).toBeTruthy();
|
|
18
|
+
expect(getByText('Profile')).toBeTruthy();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('calls onPress for a tappable item', () => {
|
|
22
|
+
const onPress = jest.fn();
|
|
23
|
+
const { getByText } = render(
|
|
24
|
+
<Breadcrumb items={[{ label: 'Home', onPress }, { label: 'Current' }]} />,
|
|
25
|
+
);
|
|
26
|
+
fireEvent.press(getByText('Home'));
|
|
27
|
+
expect(onPress).toHaveBeenCalledTimes(1);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it.each(['sm', 'lg'] as const)(
|
|
31
|
+
'renders size "%s" without crashing',
|
|
32
|
+
(size) => {
|
|
33
|
+
const { getByText } = render(<Breadcrumb items={items} size={size} />);
|
|
34
|
+
expect(getByText('Home')).toBeTruthy();
|
|
35
|
+
},
|
|
36
|
+
);
|
|
37
|
+
});
|
|
@@ -1,13 +1,18 @@
|
|
|
1
|
-
import { Ionicons } from '@expo/vector-icons';
|
|
2
1
|
import React from 'react';
|
|
2
|
+
|
|
3
3
|
import {
|
|
4
4
|
Pressable,
|
|
5
5
|
ScrollView,
|
|
6
|
+
StyleProp,
|
|
6
7
|
StyleSheet,
|
|
7
8
|
Text,
|
|
8
9
|
View,
|
|
10
|
+
ViewStyle,
|
|
9
11
|
} from 'react-native';
|
|
10
|
-
|
|
12
|
+
|
|
13
|
+
import { Ionicons } from '@expo/vector-icons';
|
|
14
|
+
|
|
15
|
+
import { sys } from '../../tokens';
|
|
11
16
|
|
|
12
17
|
export type BreadcrumbSize = 'sm' | 'lg';
|
|
13
18
|
|
|
@@ -30,6 +35,7 @@ export interface BreadcrumbItem {
|
|
|
30
35
|
export interface BreadcrumbProps {
|
|
31
36
|
items: BreadcrumbItem[];
|
|
32
37
|
size?: BreadcrumbSize;
|
|
38
|
+
style?: StyleProp<ViewStyle>;
|
|
33
39
|
}
|
|
34
40
|
|
|
35
41
|
const { colorRoles: cr, dimensions: dim, typeScale: ts } = sys;
|
|
@@ -55,7 +61,7 @@ const SIZE_TOKENS = {
|
|
|
55
61
|
},
|
|
56
62
|
};
|
|
57
63
|
|
|
58
|
-
export function Breadcrumb({ items, size = 'lg' }: BreadcrumbProps) {
|
|
64
|
+
export function Breadcrumb({ items, size = 'lg', style }: BreadcrumbProps) {
|
|
59
65
|
const s = SIZE_TOKENS[size];
|
|
60
66
|
|
|
61
67
|
return (
|
|
@@ -64,6 +70,7 @@ export function Breadcrumb({ items, size = 'lg' }: BreadcrumbProps) {
|
|
|
64
70
|
horizontal
|
|
65
71
|
showsHorizontalScrollIndicator={false}
|
|
66
72
|
contentContainerStyle={styles.row}
|
|
73
|
+
style={style}
|
|
67
74
|
>
|
|
68
75
|
{items.map((item, index) => {
|
|
69
76
|
const isLast = index === items.length - 1;
|
|
@@ -81,7 +88,9 @@ export function Breadcrumb({ items, size = 'lg' }: BreadcrumbProps) {
|
|
|
81
88
|
styles.content,
|
|
82
89
|
{
|
|
83
90
|
paddingHorizontal: s.paddingH,
|
|
84
|
-
paddingVertical: item.isHome
|
|
91
|
+
paddingVertical: item.isHome
|
|
92
|
+
? dim.spacing.padding.sysPadding2
|
|
93
|
+
: s.paddingV,
|
|
85
94
|
gap: item.isHome ? 0 : dim.spacing.padding.sysPadding4,
|
|
86
95
|
},
|
|
87
96
|
item.disabled && styles.disabled,
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
import { fireEvent, render } from '@testing-library/react-native';
|
|
4
|
+
|
|
5
|
+
import { Button } from './index';
|
|
6
|
+
|
|
7
|
+
describe('Button', () => {
|
|
8
|
+
it('renders label', () => {
|
|
9
|
+
const { getByText } = render(<Button label="Submit" />);
|
|
10
|
+
expect(getByText('Submit')).toBeTruthy();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('calls onPress when tapped', () => {
|
|
14
|
+
const onPress = jest.fn();
|
|
15
|
+
const { getByRole } = render(<Button label="Submit" onPress={onPress} />);
|
|
16
|
+
fireEvent.press(getByRole('button'));
|
|
17
|
+
expect(onPress).toHaveBeenCalledTimes(1);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('is accessible as disabled when disabled prop is set', () => {
|
|
21
|
+
const { getByRole } = render(<Button label="Submit" disabled />);
|
|
22
|
+
expect(getByRole('button').props.accessibilityState.disabled).toBe(true);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it.each(['filled', 'tonal', 'outlined', 'elevated', 'text'] as const)(
|
|
26
|
+
'renders variant "%s" without crashing',
|
|
27
|
+
(variant) => {
|
|
28
|
+
const { getByText } = render(<Button label="Button" variant={variant} />);
|
|
29
|
+
expect(getByText('Button')).toBeTruthy();
|
|
30
|
+
},
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
it.each(['sm', 'md', 'lg', 'xl'] as const)(
|
|
34
|
+
'renders size "%s" without crashing',
|
|
35
|
+
(size) => {
|
|
36
|
+
const { getByText } = render(<Button label="Button" size={size} />);
|
|
37
|
+
expect(getByText('Button')).toBeTruthy();
|
|
38
|
+
},
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
it('renders in full width mode', () => {
|
|
42
|
+
const { getByText } = render(<Button label="Full" fullWidth />);
|
|
43
|
+
expect(getByText('Full')).toBeTruthy();
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
|
|
2
3
|
import {
|
|
3
4
|
ActivityIndicator,
|
|
4
5
|
Pressable,
|
|
6
|
+
type PressableProps,
|
|
5
7
|
StyleSheet,
|
|
6
8
|
Text,
|
|
7
9
|
View,
|
|
8
|
-
type PressableProps,
|
|
9
10
|
} from 'react-native';
|
|
10
|
-
|
|
11
|
+
|
|
12
|
+
import { sys } from '../../tokens';
|
|
11
13
|
|
|
12
14
|
export type ButtonVariant =
|
|
13
15
|
| 'filled'
|
|
@@ -20,7 +22,10 @@ export type ButtonVariant =
|
|
|
20
22
|
|
|
21
23
|
export type ButtonSize = 'sm' | 'md' | 'lg' | 'xl';
|
|
22
24
|
|
|
23
|
-
export interface ButtonProps extends Omit<
|
|
25
|
+
export interface ButtonProps extends Omit<
|
|
26
|
+
PressableProps,
|
|
27
|
+
'children' | 'style'
|
|
28
|
+
> {
|
|
24
29
|
variant?: ButtonVariant;
|
|
25
30
|
size?: ButtonSize;
|
|
26
31
|
label: string;
|
|
@@ -153,18 +158,18 @@ export function Button({
|
|
|
153
158
|
...(iconOnly
|
|
154
159
|
? { width: s.height, height: s.height }
|
|
155
160
|
: variant === 'text'
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
161
|
+
? {
|
|
162
|
+
// Text variant is content-height (no fixed container). Figma:
|
|
163
|
+
// sm=26px, md=28px, lg=31px, xl=32px — each resolves to
|
|
164
|
+
// lineHeight + 2×paddingV, not the standard fixed height.
|
|
165
|
+
paddingHorizontal: 0,
|
|
166
|
+
paddingVertical: s.paddingV,
|
|
167
|
+
}
|
|
168
|
+
: {
|
|
169
|
+
height: s.height,
|
|
170
|
+
paddingHorizontal: paddingH,
|
|
171
|
+
paddingVertical: s.paddingV,
|
|
172
|
+
}),
|
|
168
173
|
...(fullWidth && !iconOnly ? { alignSelf: 'stretch' } : {}),
|
|
169
174
|
opacity: isDisabled ? 0.48 : pressed ? 0.82 : 1,
|
|
170
175
|
...(variant === 'elevated'
|
|
@@ -183,7 +188,14 @@ export function Button({
|
|
|
183
188
|
{loading ? (
|
|
184
189
|
<ActivityIndicator size={s.iconSize} color={v.label} />
|
|
185
190
|
) : leadingIcon ? (
|
|
186
|
-
<View
|
|
191
|
+
<View
|
|
192
|
+
style={{
|
|
193
|
+
width: s.iconSize,
|
|
194
|
+
height: s.iconSize,
|
|
195
|
+
alignItems: 'center',
|
|
196
|
+
justifyContent: 'center',
|
|
197
|
+
}}
|
|
198
|
+
>
|
|
187
199
|
{leadingIcon}
|
|
188
200
|
</View>
|
|
189
201
|
) : null}
|
|
@@ -204,7 +216,14 @@ export function Button({
|
|
|
204
216
|
)}
|
|
205
217
|
|
|
206
218
|
{trailingIcon && (
|
|
207
|
-
<View
|
|
219
|
+
<View
|
|
220
|
+
style={{
|
|
221
|
+
width: s.iconSize,
|
|
222
|
+
height: s.iconSize,
|
|
223
|
+
alignItems: 'center',
|
|
224
|
+
justifyContent: 'center',
|
|
225
|
+
}}
|
|
226
|
+
>
|
|
208
227
|
{trailingIcon}
|
|
209
228
|
</View>
|
|
210
229
|
)}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
import { fireEvent, render } from '@testing-library/react-native';
|
|
4
|
+
|
|
5
|
+
import { Checkbox } from './index';
|
|
6
|
+
|
|
7
|
+
describe('Checkbox', () => {
|
|
8
|
+
it('renders label', () => {
|
|
9
|
+
const { getByText } = render(<Checkbox label="Accept terms" />);
|
|
10
|
+
expect(getByText('Accept terms')).toBeTruthy();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('renders description when provided', () => {
|
|
14
|
+
const { getByText } = render(
|
|
15
|
+
<Checkbox label="Subscribe" description="Get weekly updates" />,
|
|
16
|
+
);
|
|
17
|
+
expect(getByText('Get weekly updates')).toBeTruthy();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('calls onChange when pressed', () => {
|
|
21
|
+
const onChange = jest.fn();
|
|
22
|
+
const { getByRole } = render(
|
|
23
|
+
<Checkbox label="Check me" checked={false} onChange={onChange} />,
|
|
24
|
+
);
|
|
25
|
+
fireEvent.press(getByRole('checkbox'));
|
|
26
|
+
expect(onChange).toHaveBeenCalledWith(true);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('does not call onChange when disabled', () => {
|
|
30
|
+
const onChange = jest.fn();
|
|
31
|
+
const { getByRole } = render(
|
|
32
|
+
<Checkbox
|
|
33
|
+
label="Disabled"
|
|
34
|
+
checked={false}
|
|
35
|
+
onChange={onChange}
|
|
36
|
+
disabled
|
|
37
|
+
/>,
|
|
38
|
+
);
|
|
39
|
+
fireEvent.press(getByRole('checkbox'));
|
|
40
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('renders indeterminate state', () => {
|
|
44
|
+
const { getByRole } = render(
|
|
45
|
+
<Checkbox label="Indeterminate" checked="indeterminate" />,
|
|
46
|
+
);
|
|
47
|
+
expect(getByRole('checkbox')).toBeTruthy();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it.each(['sm', 'md'] as const)(
|
|
51
|
+
'renders size "%s" without crashing',
|
|
52
|
+
(size) => {
|
|
53
|
+
const { getByText } = render(<Checkbox label="Size test" size={size} />);
|
|
54
|
+
expect(getByText('Size test')).toBeTruthy();
|
|
55
|
+
},
|
|
56
|
+
);
|
|
57
|
+
});
|
|
@@ -1,7 +1,17 @@
|
|
|
1
|
-
import { Ionicons } from '@expo/vector-icons';
|
|
2
1
|
import React, { useState } from 'react';
|
|
3
|
-
|
|
4
|
-
import {
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Pressable,
|
|
5
|
+
StyleProp,
|
|
6
|
+
StyleSheet,
|
|
7
|
+
Text,
|
|
8
|
+
View,
|
|
9
|
+
ViewStyle,
|
|
10
|
+
} from 'react-native';
|
|
11
|
+
|
|
12
|
+
import { Ionicons } from '@expo/vector-icons';
|
|
13
|
+
|
|
14
|
+
import { sys } from '../../tokens';
|
|
5
15
|
|
|
6
16
|
export type CheckboxSize = 'sm' | 'md';
|
|
7
17
|
export type CheckboxCheckedState = boolean | 'indeterminate';
|
|
@@ -15,13 +25,26 @@ export interface CheckboxProps {
|
|
|
15
25
|
description?: string;
|
|
16
26
|
disabled?: boolean;
|
|
17
27
|
invalid?: boolean;
|
|
28
|
+
style?: StyleProp<ViewStyle>;
|
|
18
29
|
}
|
|
19
30
|
|
|
20
31
|
const { colorRoles: cr, dimensions: dim, typeScale: ts } = sys;
|
|
21
32
|
|
|
22
33
|
const SIZE_TOKENS = {
|
|
23
|
-
sm: {
|
|
24
|
-
|
|
34
|
+
sm: {
|
|
35
|
+
box: 20,
|
|
36
|
+
hitArea: 36,
|
|
37
|
+
fontSize: ts.bodySmall.sysFontSize,
|
|
38
|
+
lineHeight: ts.bodySmall.sysLineHeight,
|
|
39
|
+
iconSize: 13,
|
|
40
|
+
},
|
|
41
|
+
md: {
|
|
42
|
+
box: 24,
|
|
43
|
+
hitArea: 40,
|
|
44
|
+
fontSize: ts.bodyMedium.sysFontSize,
|
|
45
|
+
lineHeight: ts.bodyMedium.sysLineHeight,
|
|
46
|
+
iconSize: 16,
|
|
47
|
+
},
|
|
25
48
|
};
|
|
26
49
|
|
|
27
50
|
export function Checkbox({
|
|
@@ -33,18 +56,20 @@ export function Checkbox({
|
|
|
33
56
|
description,
|
|
34
57
|
disabled = false,
|
|
35
58
|
invalid = false,
|
|
59
|
+
style,
|
|
36
60
|
}: CheckboxProps) {
|
|
37
|
-
const [internalChecked, setInternalChecked] =
|
|
61
|
+
const [internalChecked, setInternalChecked] =
|
|
62
|
+
useState<boolean>(defaultChecked);
|
|
38
63
|
const isControlled = checkedProp !== undefined;
|
|
39
|
-
const checkedState: CheckboxCheckedState = isControlled
|
|
64
|
+
const checkedState: CheckboxCheckedState = isControlled
|
|
65
|
+
? checkedProp
|
|
66
|
+
: internalChecked;
|
|
40
67
|
|
|
41
68
|
const isChecked = checkedState === true;
|
|
42
69
|
const isIndeterminate = checkedState === 'indeterminate';
|
|
43
70
|
const isSelected = isChecked || isIndeterminate;
|
|
44
71
|
|
|
45
72
|
const s = SIZE_TOKENS[size];
|
|
46
|
-
const hitPad = (s.hitArea - s.box) / 2;
|
|
47
|
-
|
|
48
73
|
function handlePress() {
|
|
49
74
|
if (disabled) return;
|
|
50
75
|
// Indeterminate → true on activation (matches HTML spec)
|
|
@@ -56,8 +81,8 @@ export function Checkbox({
|
|
|
56
81
|
const borderColor = invalid
|
|
57
82
|
? cr.error.sysError
|
|
58
83
|
: isSelected
|
|
59
|
-
|
|
60
|
-
|
|
84
|
+
? 'transparent'
|
|
85
|
+
: cr.outline.sysOutlineFixed;
|
|
61
86
|
|
|
62
87
|
const boxBg = isSelected
|
|
63
88
|
? invalid
|
|
@@ -74,10 +99,7 @@ export function Checkbox({
|
|
|
74
99
|
checked: isIndeterminate ? 'mixed' : isChecked,
|
|
75
100
|
disabled,
|
|
76
101
|
}}
|
|
77
|
-
style={(
|
|
78
|
-
styles.root,
|
|
79
|
-
disabled && styles.disabled,
|
|
80
|
-
]}
|
|
102
|
+
style={() => [styles.root, disabled && styles.disabled, style]}
|
|
81
103
|
>
|
|
82
104
|
{({ pressed }) => (
|
|
83
105
|
<>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
import { fireEvent, render } from '@testing-library/react-native';
|
|
4
|
+
|
|
5
|
+
import { Chip } from './index';
|
|
6
|
+
|
|
7
|
+
describe('Chip', () => {
|
|
8
|
+
it('renders label', () => {
|
|
9
|
+
const { getByText } = render(<Chip label="Filter" />);
|
|
10
|
+
expect(getByText('Filter')).toBeTruthy();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('calls onPress when tapped', () => {
|
|
14
|
+
const onPress = jest.fn();
|
|
15
|
+
const { getByText } = render(<Chip label="Filter" onPress={onPress} />);
|
|
16
|
+
fireEvent.press(getByText('Filter'));
|
|
17
|
+
expect(onPress).toHaveBeenCalledTimes(1);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('renders selected state', () => {
|
|
21
|
+
const { toJSON } = render(<Chip label="Selected" selected />);
|
|
22
|
+
expect(toJSON()).toBeTruthy();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it.each(['sm', 'md', 'lg', 'xl'] as const)(
|
|
26
|
+
'renders size "%s" without crashing',
|
|
27
|
+
(size) => {
|
|
28
|
+
const { getByText } = render(<Chip label="Chip" size={size} />);
|
|
29
|
+
expect(getByText('Chip')).toBeTruthy();
|
|
30
|
+
},
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
it.each(['neutral', 'informative', 'positive', 'danger', 'warning'] as const)(
|
|
34
|
+
'renders usage "%s" without crashing',
|
|
35
|
+
(usage) => {
|
|
36
|
+
const { getByText } = render(<Chip label="Chip" usage={usage} />);
|
|
37
|
+
expect(getByText('Chip')).toBeTruthy();
|
|
38
|
+
},
|
|
39
|
+
);
|
|
40
|
+
});
|
|
@@ -1,15 +1,25 @@
|
|
|
1
|
-
import { Ionicons } from '@expo/vector-icons';
|
|
2
1
|
import React from 'react';
|
|
2
|
+
|
|
3
3
|
import {
|
|
4
4
|
Pressable,
|
|
5
|
+
StyleProp,
|
|
5
6
|
StyleSheet,
|
|
6
7
|
Text,
|
|
7
8
|
View,
|
|
9
|
+
ViewStyle,
|
|
8
10
|
} from 'react-native';
|
|
9
|
-
|
|
11
|
+
|
|
12
|
+
import { Ionicons } from '@expo/vector-icons';
|
|
13
|
+
|
|
14
|
+
import { sys } from '../../tokens';
|
|
10
15
|
|
|
11
16
|
export type ChipSize = 'sm' | 'md' | 'lg' | 'xl';
|
|
12
|
-
export type ChipUsage =
|
|
17
|
+
export type ChipUsage =
|
|
18
|
+
| 'neutral'
|
|
19
|
+
| 'informative'
|
|
20
|
+
| 'positive'
|
|
21
|
+
| 'danger'
|
|
22
|
+
| 'warning';
|
|
13
23
|
|
|
14
24
|
export interface ChipProps {
|
|
15
25
|
label: string;
|
|
@@ -22,6 +32,7 @@ export interface ChipProps {
|
|
|
22
32
|
onDismiss?: () => void;
|
|
23
33
|
/** Renders a small count badge after the label */
|
|
24
34
|
badge?: number | string;
|
|
35
|
+
style?: StyleProp<ViewStyle>;
|
|
25
36
|
}
|
|
26
37
|
|
|
27
38
|
const { colorRoles: cr, dimensions: dim, typeScale: ts } = sys;
|
|
@@ -163,6 +174,7 @@ export function Chip({
|
|
|
163
174
|
dismissible = false,
|
|
164
175
|
onDismiss,
|
|
165
176
|
badge,
|
|
177
|
+
style,
|
|
166
178
|
}: ChipProps) {
|
|
167
179
|
const s = SIZE_TOKENS[size];
|
|
168
180
|
const c = getUsageColors(usage);
|
|
@@ -182,6 +194,7 @@ export function Chip({
|
|
|
182
194
|
backgroundColor: c.bg,
|
|
183
195
|
},
|
|
184
196
|
c.elevated && styles.elevated,
|
|
197
|
+
style,
|
|
185
198
|
]}
|
|
186
199
|
>
|
|
187
200
|
{/* Leading icon */}
|
|
@@ -260,11 +273,7 @@ export function Chip({
|
|
|
260
273
|
overflow: 'hidden',
|
|
261
274
|
})}
|
|
262
275
|
>
|
|
263
|
-
<Ionicons
|
|
264
|
-
name="close"
|
|
265
|
-
size={s.closeIconSize}
|
|
266
|
-
color={c.text}
|
|
267
|
-
/>
|
|
276
|
+
<Ionicons name="close" size={s.closeIconSize} color={c.text} />
|
|
268
277
|
</Pressable>
|
|
269
278
|
)}
|
|
270
279
|
</View>
|
|
@@ -282,7 +291,7 @@ const styles = StyleSheet.create({
|
|
|
282
291
|
elevated: {
|
|
283
292
|
shadowColor: '#000',
|
|
284
293
|
shadowOffset: { width: 0, height: 2 },
|
|
285
|
-
shadowOpacity: 0.
|
|
294
|
+
shadowOpacity: 0.1,
|
|
286
295
|
shadowRadius: 8,
|
|
287
296
|
elevation: 2,
|
|
288
297
|
// overflow must be visible for shadow on iOS — override here
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
import { render } from '@testing-library/react-native';
|
|
4
|
+
|
|
5
|
+
import { Divider } from './index';
|
|
6
|
+
|
|
7
|
+
describe('Divider', () => {
|
|
8
|
+
it('renders without crashing', () => {
|
|
9
|
+
const { toJSON } = render(<Divider />);
|
|
10
|
+
expect(toJSON()).toBeTruthy();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it.each(['full', 'inset', 'middle'] as const)(
|
|
14
|
+
'renders variant "%s" without crashing',
|
|
15
|
+
(variant) => {
|
|
16
|
+
const { toJSON } = render(<Divider variant={variant} />);
|
|
17
|
+
expect(toJSON()).toBeTruthy();
|
|
18
|
+
},
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
it.each(['thin', 'medium', 'thick'] as const)(
|
|
22
|
+
'renders weight "%s" without crashing',
|
|
23
|
+
(weight) => {
|
|
24
|
+
const { toJSON } = render(<Divider weight={weight} />);
|
|
25
|
+
expect(toJSON()).toBeTruthy();
|
|
26
|
+
},
|
|
27
|
+
);
|
|
28
|
+
});
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
|
|
3
|
-
import {
|
|
2
|
+
|
|
3
|
+
import { StyleProp, View, ViewStyle } from 'react-native';
|
|
4
|
+
|
|
5
|
+
import { sys } from '../../tokens';
|
|
4
6
|
|
|
5
7
|
export type DividerVariant = 'horizontal' | 'vertical';
|
|
6
8
|
export type DividerWeight = 'thin' | 'thick';
|
|
@@ -9,6 +11,7 @@ export interface DividerProps {
|
|
|
9
11
|
variant?: DividerVariant;
|
|
10
12
|
weight?: DividerWeight;
|
|
11
13
|
dashed?: boolean;
|
|
14
|
+
style?: StyleProp<ViewStyle>;
|
|
12
15
|
}
|
|
13
16
|
|
|
14
17
|
const { colorRoles: cr, dimensions: dim } = sys;
|
|
@@ -17,6 +20,7 @@ export function Divider({
|
|
|
17
20
|
variant = 'horizontal',
|
|
18
21
|
weight = 'thin',
|
|
19
22
|
dashed = false,
|
|
23
|
+
style,
|
|
20
24
|
}: DividerProps) {
|
|
21
25
|
const thickness =
|
|
22
26
|
weight === 'thin'
|
|
@@ -28,15 +32,18 @@ export function Divider({
|
|
|
28
32
|
if (variant === 'horizontal') {
|
|
29
33
|
return (
|
|
30
34
|
<View
|
|
31
|
-
style={
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
35
|
+
style={[
|
|
36
|
+
{
|
|
37
|
+
// Zero height — the line is drawn as a top border
|
|
38
|
+
height: 0,
|
|
39
|
+
// Stretch to fill parent width
|
|
40
|
+
alignSelf: 'stretch',
|
|
41
|
+
borderTopWidth: thickness,
|
|
42
|
+
borderTopColor: color,
|
|
43
|
+
borderStyle: dashed ? 'dashed' : 'solid',
|
|
44
|
+
},
|
|
45
|
+
style,
|
|
46
|
+
]}
|
|
40
47
|
accessible={false}
|
|
41
48
|
/>
|
|
42
49
|
);
|
|
@@ -45,15 +52,18 @@ export function Divider({
|
|
|
45
52
|
// Vertical
|
|
46
53
|
return (
|
|
47
54
|
<View
|
|
48
|
-
style={
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
style={[
|
|
56
|
+
{
|
|
57
|
+
// Zero width — the line is drawn as a left border
|
|
58
|
+
width: 0,
|
|
59
|
+
// Stretch to fill parent height
|
|
60
|
+
alignSelf: 'stretch',
|
|
61
|
+
borderLeftWidth: thickness,
|
|
62
|
+
borderLeftColor: color,
|
|
63
|
+
borderStyle: dashed ? 'dashed' : 'solid',
|
|
64
|
+
},
|
|
65
|
+
style,
|
|
66
|
+
]}
|
|
57
67
|
accessible={false}
|
|
58
68
|
/>
|
|
59
69
|
);
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
import { fireEvent, render } from '@testing-library/react-native';
|
|
4
|
+
|
|
5
|
+
import { EmptyState } from './index';
|
|
6
|
+
|
|
7
|
+
describe('EmptyState', () => {
|
|
8
|
+
it('renders title and description', () => {
|
|
9
|
+
const { getByText } = render(
|
|
10
|
+
<EmptyState
|
|
11
|
+
title="Nothing here"
|
|
12
|
+
description="Add something to get started"
|
|
13
|
+
/>,
|
|
14
|
+
);
|
|
15
|
+
expect(getByText('Nothing here')).toBeTruthy();
|
|
16
|
+
expect(getByText('Add something to get started')).toBeTruthy();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('renders action button and handles press', () => {
|
|
20
|
+
const onAction = jest.fn();
|
|
21
|
+
const { getByText } = render(
|
|
22
|
+
<EmptyState
|
|
23
|
+
title="No results"
|
|
24
|
+
description="Try a different search"
|
|
25
|
+
actionLabel="Clear filters"
|
|
26
|
+
onAction={onAction}
|
|
27
|
+
/>,
|
|
28
|
+
);
|
|
29
|
+
fireEvent.press(getByText('Clear filters'));
|
|
30
|
+
expect(onAction).toHaveBeenCalledTimes(1);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it.each(['desktop', 'mobile'] as const)(
|
|
34
|
+
'renders viewport "%s" without crashing',
|
|
35
|
+
(viewport) => {
|
|
36
|
+
const { getByText } = render(
|
|
37
|
+
<EmptyState
|
|
38
|
+
title="Empty"
|
|
39
|
+
description="Nothing to show"
|
|
40
|
+
viewport={viewport}
|
|
41
|
+
/>,
|
|
42
|
+
);
|
|
43
|
+
expect(getByText('Empty')).toBeTruthy();
|
|
44
|
+
},
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
it.each(['icon', 'illustration'] as const)(
|
|
48
|
+
'renders style "%s" without crashing',
|
|
49
|
+
(emptyStyle) => {
|
|
50
|
+
const { getByText } = render(
|
|
51
|
+
<EmptyState
|
|
52
|
+
title="Empty"
|
|
53
|
+
description="Nothing"
|
|
54
|
+
emptyStyle={emptyStyle}
|
|
55
|
+
/>,
|
|
56
|
+
);
|
|
57
|
+
expect(getByText('Empty')).toBeTruthy();
|
|
58
|
+
},
|
|
59
|
+
);
|
|
60
|
+
});
|