@ledgerhq/lumen-ui-rnative 0.0.68 → 0.0.70
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/.storybook/mocks/blur.tsx +1 -2
- package/dist/package.json +2 -2
- package/dist/src/i18n/locales/de.json +4 -0
- package/dist/src/i18n/locales/en.json +4 -0
- package/dist/src/i18n/locales/es.json +4 -0
- package/dist/src/i18n/locales/fr.json +4 -0
- package/dist/src/i18n/locales/ja.json +4 -0
- package/dist/src/i18n/locales/ko.json +4 -0
- package/dist/src/i18n/locales/pt.json +4 -0
- package/dist/src/i18n/locales/ru.json +4 -0
- package/dist/src/i18n/locales/th.json +4 -0
- package/dist/src/i18n/locales/tr.json +4 -0
- package/dist/src/i18n/locales/zh.json +4 -0
- package/dist/src/lib/Components/Avatar/Avatar.d.ts +19 -0
- package/dist/src/lib/Components/Avatar/Avatar.d.ts.map +1 -0
- package/dist/src/lib/Components/Avatar/Avatar.js +81 -0
- package/dist/src/lib/Components/Avatar/Avatar.stories.d.ts +22 -0
- package/dist/src/lib/Components/Avatar/Avatar.stories.d.ts.map +1 -0
- package/dist/src/lib/Components/Avatar/Avatar.stories.js +72 -0
- package/dist/src/lib/Components/Avatar/index.d.ts +3 -0
- package/dist/src/lib/Components/Avatar/index.d.ts.map +1 -0
- package/dist/src/lib/Components/Avatar/index.js +2 -0
- package/dist/src/lib/Components/Avatar/types.d.ts +26 -0
- package/dist/src/lib/Components/Avatar/types.d.ts.map +1 -0
- package/dist/src/lib/Components/Avatar/types.js +1 -0
- package/dist/src/lib/Components/CardButton/CardButton.js +3 -3
- package/dist/src/lib/Components/PageIndicator/PageIndicator.d.ts.map +1 -1
- package/dist/src/lib/Components/PageIndicator/PageIndicator.js +3 -2
- package/dist/src/lib/Components/PageIndicator/PageIndicator.stories.js +4 -4
- package/dist/src/lib/Components/PageIndicator/types.d.ts +1 -1
- package/dist/src/lib/Components/index.d.ts +1 -0
- package/dist/src/lib/Components/index.d.ts.map +1 -1
- package/dist/src/lib/Components/index.js +1 -0
- package/dist/src/lib/Symbols/Icons/Chart5.d.ts +35 -0
- package/dist/src/lib/Symbols/Icons/Chart5.d.ts.map +1 -0
- package/dist/src/lib/Symbols/Icons/Chart5.js +34 -0
- package/dist/src/lib/Symbols/Icons/Chart5Fill.d.ts +35 -0
- package/dist/src/lib/Symbols/Icons/Chart5Fill.d.ts.map +1 -0
- package/dist/src/lib/Symbols/Icons/Chart5Fill.js +34 -0
- package/dist/src/lib/Symbols/Icons/CurveDown.d.ts +35 -0
- package/dist/src/lib/Symbols/Icons/CurveDown.d.ts.map +1 -0
- package/dist/src/lib/Symbols/Icons/CurveDown.js +34 -0
- package/dist/src/lib/Symbols/Icons/CurveUp.d.ts +35 -0
- package/dist/src/lib/Symbols/Icons/CurveUp.d.ts.map +1 -0
- package/dist/src/lib/Symbols/Icons/CurveUp.js +34 -0
- package/dist/src/lib/Symbols/Icons/Target.d.ts +35 -0
- package/dist/src/lib/Symbols/Icons/Target.d.ts.map +1 -0
- package/dist/src/lib/Symbols/Icons/Target.js +34 -0
- package/dist/src/lib/Symbols/index.d.ts +5 -0
- package/dist/src/lib/Symbols/index.d.ts.map +1 -1
- package/dist/src/lib/Symbols/index.js +5 -0
- package/package.json +3 -3
- package/src/i18n/locales/de.json +4 -0
- package/src/i18n/locales/en.json +4 -0
- package/src/i18n/locales/es.json +4 -0
- package/src/i18n/locales/fr.json +4 -0
- package/src/i18n/locales/ja.json +4 -0
- package/src/i18n/locales/ko.json +4 -0
- package/src/i18n/locales/pt.json +4 -0
- package/src/i18n/locales/ru.json +4 -0
- package/src/i18n/locales/th.json +4 -0
- package/src/i18n/locales/tr.json +4 -0
- package/src/i18n/locales/zh.json +4 -0
- package/src/lib/Components/Avatar/Avatar.mdx +323 -0
- package/src/lib/Components/Avatar/Avatar.stories.tsx +127 -0
- package/src/lib/Components/Avatar/Avatar.test.tsx +215 -0
- package/src/lib/Components/Avatar/Avatar.tsx +132 -0
- package/src/lib/Components/Avatar/index.ts +2 -0
- package/src/lib/Components/Avatar/types.ts +26 -0
- package/src/lib/Components/CardButton/CardButton.tsx +3 -3
- package/src/lib/Components/PageIndicator/PageIndicator.mdx +7 -4
- package/src/lib/Components/PageIndicator/PageIndicator.stories.tsx +5 -5
- package/src/lib/Components/PageIndicator/PageIndicator.test.tsx +14 -14
- package/src/lib/Components/PageIndicator/PageIndicator.tsx +6 -2
- package/src/lib/Components/PageIndicator/types.ts +1 -1
- package/src/lib/Components/index.ts +1 -0
- package/src/lib/Symbols/Icons/Chart5.tsx +53 -0
- package/src/lib/Symbols/Icons/Chart5Fill.tsx +42 -0
- package/src/lib/Symbols/Icons/CurveDown.tsx +69 -0
- package/src/lib/Symbols/Icons/CurveUp.tsx +68 -0
- package/src/lib/Symbols/Icons/Target.tsx +45 -0
- package/src/lib/Symbols/index.ts +5 -0
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import { Meta, Canvas, Controls } from '@storybook/addon-docs/blocks';
|
|
2
|
+
import * as AvatarStories from './Avatar.stories';
|
|
3
|
+
import { Avatar } from './Avatar';
|
|
4
|
+
import { CustomTabs, Tab } from '../../../../.storybook/components';
|
|
5
|
+
import { DoVsDontRow, DoBlockItem, DontBlockItem } from '../../../../.storybook/components/DoVsDont';
|
|
6
|
+
import CommonRulesDoAndDont from '../../../../.storybook/components/DoVsDont/CommonRulesDoAndDont.mdx';
|
|
7
|
+
import { Box } from '../Utility';
|
|
8
|
+
|
|
9
|
+
<Meta title='Communication/Avatar' of={AvatarStories} />
|
|
10
|
+
|
|
11
|
+
# 👤 Avatar
|
|
12
|
+
|
|
13
|
+
<CustomTabs>
|
|
14
|
+
<Tab label="Overview">
|
|
15
|
+
|
|
16
|
+
## Introduction
|
|
17
|
+
|
|
18
|
+
The Avatar component displays a circular user profile image with automatic fallback to a User icon when the image is unavailable or fails to load. It supports an optional notification indicator and two size variants. This React Native implementation ensures consistent behavior across mobile platforms while maintaining design system coherence.
|
|
19
|
+
|
|
20
|
+
> View in [Figma](https://www.figma.com/design/JxaLVMTWirCpU0rsbZ30k7/2.-Components-Library?node-id=10546-614&p=f&m=dev).
|
|
21
|
+
|
|
22
|
+
## Anatomy
|
|
23
|
+
|
|
24
|
+
<Canvas of={AvatarStories.Base} />
|
|
25
|
+
|
|
26
|
+
- **Image/Fallback Icon**: The circular user image or User icon fallback displayed when image is unavailable
|
|
27
|
+
- **Notification Indicator (optional)**: A red dot positioned at the top-right corner to indicate notifications
|
|
28
|
+
- **Container**: Rounded full background with muted styling
|
|
29
|
+
|
|
30
|
+
## Properties
|
|
31
|
+
|
|
32
|
+
### Overview
|
|
33
|
+
|
|
34
|
+
<Canvas of={AvatarStories.Base} />
|
|
35
|
+
<Controls of={AvatarStories.Base} />
|
|
36
|
+
|
|
37
|
+
### Size
|
|
38
|
+
|
|
39
|
+
Avatars come in two different sizes:
|
|
40
|
+
|
|
41
|
+
- **sm** (40px)
|
|
42
|
+
- **md** (48px, default)
|
|
43
|
+
|
|
44
|
+
<Canvas of={AvatarStories.SizeShowcase} />
|
|
45
|
+
|
|
46
|
+
### States
|
|
47
|
+
|
|
48
|
+
The Avatar component handles two primary states automatically:
|
|
49
|
+
|
|
50
|
+
- **With Image**: Displays the provided image when `src` is valid and loads successfully
|
|
51
|
+
- **Fallback**: Shows a User icon when `src` is undefined or the image fails to load
|
|
52
|
+
|
|
53
|
+
<Canvas of={AvatarStories.FallbackShowcase} />
|
|
54
|
+
|
|
55
|
+
### Notification Indicator
|
|
56
|
+
|
|
57
|
+
An optional notification indicator can be displayed to show status or alerts:
|
|
58
|
+
|
|
59
|
+
- **showNotification**: Boolean prop to toggle the notification dot (default: false)
|
|
60
|
+
|
|
61
|
+
<Canvas of={AvatarStories.NotificationShowcase} />
|
|
62
|
+
|
|
63
|
+
### As Interactive Trigger
|
|
64
|
+
|
|
65
|
+
Avatars are commonly used as interactive triggers for navigation or actions:
|
|
66
|
+
|
|
67
|
+
<Canvas of={AvatarStories.InteractiveShowcase} />
|
|
68
|
+
|
|
69
|
+
## Accessibility
|
|
70
|
+
|
|
71
|
+
To be implemented:
|
|
72
|
+
|
|
73
|
+
- **Color contrast**
|
|
74
|
+
- **Text zoom support**
|
|
75
|
+
- **Semantic alt text for images**
|
|
76
|
+
- **Screen reader announcements for notification state**
|
|
77
|
+
- **Touch target sizing** (minimum 44px)
|
|
78
|
+
|
|
79
|
+
</Tab>
|
|
80
|
+
<Tab label="Implementation">
|
|
81
|
+
|
|
82
|
+
## Setup
|
|
83
|
+
|
|
84
|
+
Install and set up the library with our [Setup Guide →](?path=/docs/getting-started-setup--docs).
|
|
85
|
+
|
|
86
|
+
### Basic Usage
|
|
87
|
+
|
|
88
|
+
```tsx
|
|
89
|
+
import { Avatar } from '@ledgerhq/lumen-ui-rnative';
|
|
90
|
+
|
|
91
|
+
function MyComponent() {
|
|
92
|
+
return <Avatar src="https://example.com/photo.jpg" size="md" />;
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Fallback Handling
|
|
97
|
+
|
|
98
|
+
The Avatar component automatically displays a fallback User icon when the image source is undefined or fails to load:
|
|
99
|
+
|
|
100
|
+
```tsx
|
|
101
|
+
import { Avatar } from '@ledgerhq/lumen-ui-rnative';
|
|
102
|
+
|
|
103
|
+
function MyComponent() {
|
|
104
|
+
// Fallback will be shown automatically
|
|
105
|
+
return <Avatar size="md" alt="User profile" />;
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### With Notification
|
|
110
|
+
|
|
111
|
+
Display a notification indicator to show status or alerts:
|
|
112
|
+
|
|
113
|
+
```tsx
|
|
114
|
+
import { Avatar } from '@ledgerhq/lumen-ui-rnative';
|
|
115
|
+
|
|
116
|
+
function MyComponent() {
|
|
117
|
+
const hasNotifications = true;
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<Avatar
|
|
121
|
+
src="https://example.com/photo.jpg"
|
|
122
|
+
showNotification={hasNotifications}
|
|
123
|
+
size="md"
|
|
124
|
+
alt="User profile"
|
|
125
|
+
/>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Different Sizes
|
|
131
|
+
|
|
132
|
+
Choose between small and medium sizes based on your layout needs:
|
|
133
|
+
|
|
134
|
+
```tsx
|
|
135
|
+
import { Avatar, Box } from '@ledgerhq/lumen-ui-rnative';
|
|
136
|
+
|
|
137
|
+
function MyComponent() {
|
|
138
|
+
return (
|
|
139
|
+
<Box lx={{ flexDirection: 'row', alignItems: 'center', gap: 's8' }}>
|
|
140
|
+
<Avatar src="https://example.com/photo.jpg" size="sm" />
|
|
141
|
+
<Avatar src="https://example.com/photo.jpg" size="md" />
|
|
142
|
+
</Box>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### As Interactive Trigger
|
|
148
|
+
|
|
149
|
+
Avatars are commonly used as interactive triggers for navigation or actions. Wrap the Avatar in a `Pressable` component:
|
|
150
|
+
|
|
151
|
+
```tsx
|
|
152
|
+
import { Avatar } from '@ledgerhq/lumen-ui-rnative';
|
|
153
|
+
import { Pressable, Linking } from 'react-native';
|
|
154
|
+
|
|
155
|
+
function UserProfile() {
|
|
156
|
+
const handlePress = () => {
|
|
157
|
+
// Navigate or perform action
|
|
158
|
+
console.log('Avatar pressed');
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
return (
|
|
162
|
+
<Pressable
|
|
163
|
+
onPress={handlePress}
|
|
164
|
+
style={({ pressed }) => ({
|
|
165
|
+
borderRadius: 9999,
|
|
166
|
+
opacity: pressed ? 0.7 : 1,
|
|
167
|
+
backgroundColor: pressed ? 'rgba(0, 0, 0, 0.05)' : 'transparent',
|
|
168
|
+
})}
|
|
169
|
+
>
|
|
170
|
+
<Avatar
|
|
171
|
+
src="https://example.com/photo.jpg"
|
|
172
|
+
size="md"
|
|
173
|
+
alt="User menu"
|
|
174
|
+
showNotification
|
|
175
|
+
/>
|
|
176
|
+
</Pressable>
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
**Key points:**
|
|
182
|
+
- Wrap Avatar in a `Pressable` component for touch interactions
|
|
183
|
+
- Use `borderRadius: 9999` to maintain circular touch area
|
|
184
|
+
- Add opacity changes for visual feedback on press
|
|
185
|
+
- Provide appropriate `alt` text for accessibility
|
|
186
|
+
|
|
187
|
+
### Custom Styling
|
|
188
|
+
|
|
189
|
+
While the component comes with predefined styles, you can extend them using the `lx` prop for token-based styling:
|
|
190
|
+
|
|
191
|
+
```tsx
|
|
192
|
+
<Avatar
|
|
193
|
+
src="https://example.com/photo.jpg"
|
|
194
|
+
size="md"
|
|
195
|
+
lx={{ marginRight: 's8' }} // Layout positioning with design tokens
|
|
196
|
+
/>
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
You can also use the `style` prop for escape-hatch styling when needed:
|
|
200
|
+
|
|
201
|
+
```tsx
|
|
202
|
+
<Avatar
|
|
203
|
+
src="https://example.com/photo.jpg"
|
|
204
|
+
size="md"
|
|
205
|
+
style={{ marginRight: 8 }} // Raw style values
|
|
206
|
+
/>
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### Complete Example
|
|
210
|
+
|
|
211
|
+
Here's a comprehensive example showing all Avatar features in a user profile context:
|
|
212
|
+
|
|
213
|
+
```tsx
|
|
214
|
+
import { Avatar, Box, Text } from '@ledgerhq/lumen-ui-rnative';
|
|
215
|
+
import { Pressable } from 'react-native';
|
|
216
|
+
import { useState } from 'react';
|
|
217
|
+
|
|
218
|
+
function UserProfileHeader() {
|
|
219
|
+
const [user, setUser] = useState({
|
|
220
|
+
name: 'John Doe',
|
|
221
|
+
avatar: 'https://example.com/photo.jpg',
|
|
222
|
+
hasNotifications: true,
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
const handleProfilePress = () => {
|
|
226
|
+
console.log('Opening user menu...');
|
|
227
|
+
// Navigate to profile or show menu
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
return (
|
|
231
|
+
<Box lx={{ flexDirection: 'row', alignItems: 'center', gap: 's12', padding: 's16' }}>
|
|
232
|
+
<Pressable
|
|
233
|
+
onPress={handleProfilePress}
|
|
234
|
+
style={({ pressed }) => ({
|
|
235
|
+
borderRadius: 9999,
|
|
236
|
+
opacity: pressed ? 0.7 : 1,
|
|
237
|
+
backgroundColor: pressed ? 'rgba(0, 0, 0, 0.05)' : 'transparent',
|
|
238
|
+
})}
|
|
239
|
+
>
|
|
240
|
+
<Avatar
|
|
241
|
+
src={user.avatar}
|
|
242
|
+
size="md"
|
|
243
|
+
showNotification={user.hasNotifications}
|
|
244
|
+
alt={`${user.name} profile picture`}
|
|
245
|
+
/>
|
|
246
|
+
</Pressable>
|
|
247
|
+
<Box lx={{ flexDirection: 'column' }}>
|
|
248
|
+
<Text>{user.name}</Text>
|
|
249
|
+
{user.hasNotifications && (
|
|
250
|
+
<Text style={{ fontSize: 12, color: '#666' }}>
|
|
251
|
+
You have new notifications
|
|
252
|
+
</Text>
|
|
253
|
+
)}
|
|
254
|
+
</Box>
|
|
255
|
+
</Box>
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
## Do's and Don'ts
|
|
261
|
+
|
|
262
|
+
The following guidelines ensure consistent usage of the Avatar component and maintain design system principles.
|
|
263
|
+
|
|
264
|
+
<Box lx={{ flexDirection: 'column', gap: 's24' }}>
|
|
265
|
+
<DoVsDontRow>
|
|
266
|
+
<DoBlockItem
|
|
267
|
+
title='Provide descriptive alt text'
|
|
268
|
+
description='Use meaningful alt text for better accessibility and screen reader support'
|
|
269
|
+
>
|
|
270
|
+
|
|
271
|
+
{/* prettier-ignore */}
|
|
272
|
+
```tsx
|
|
273
|
+
<Avatar
|
|
274
|
+
src="https://example.com/photo.jpg"
|
|
275
|
+
alt="John Doe profile picture"
|
|
276
|
+
size="md"
|
|
277
|
+
/>
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
</DoBlockItem>
|
|
281
|
+
<DontBlockItem
|
|
282
|
+
title="Don't omit alt text"
|
|
283
|
+
description='Missing alt text reduces accessibility for screen reader users'
|
|
284
|
+
>
|
|
285
|
+
|
|
286
|
+
{/* prettier-ignore */}
|
|
287
|
+
```tsx
|
|
288
|
+
<Avatar
|
|
289
|
+
src="https://example.com/photo.jpg"
|
|
290
|
+
size="md"
|
|
291
|
+
/>
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
</DontBlockItem>
|
|
295
|
+
|
|
296
|
+
</DoVsDontRow>
|
|
297
|
+
|
|
298
|
+
<CommonRulesDoAndDont />
|
|
299
|
+
</Box>
|
|
300
|
+
|
|
301
|
+
## Platform Considerations
|
|
302
|
+
|
|
303
|
+
### iOS Specific
|
|
304
|
+
|
|
305
|
+
- Avatars automatically adapt to iOS design guidelines
|
|
306
|
+
- Touch feedback uses appropriate opacity transitions
|
|
307
|
+
- Respects iOS accessibility settings like reduced motion
|
|
308
|
+
|
|
309
|
+
### Android Specific
|
|
310
|
+
|
|
311
|
+
- Material Design touch feedback is supported through React Native's Pressable
|
|
312
|
+
- Handles Android back button interactions appropriately
|
|
313
|
+
- Respects Android accessibility services
|
|
314
|
+
|
|
315
|
+
### Cross-Platform
|
|
316
|
+
|
|
317
|
+
- Consistent visual appearance across platforms
|
|
318
|
+
- Platform-appropriate touch feedback via Pressable
|
|
319
|
+
- Unified API regardless of platform
|
|
320
|
+
- Images load using React Native's Image component with cross-platform optimization
|
|
321
|
+
|
|
322
|
+
</Tab>
|
|
323
|
+
</CustomTabs>
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { Meta, StoryObj } from '@storybook/react-native-web-vite';
|
|
2
|
+
import { View, Text, Pressable, Linking } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { Box } from '../Utility';
|
|
5
|
+
import { Avatar } from './Avatar';
|
|
6
|
+
|
|
7
|
+
const meta = {
|
|
8
|
+
component: Avatar,
|
|
9
|
+
title: 'Communication/Avatar',
|
|
10
|
+
parameters: {
|
|
11
|
+
docs: {
|
|
12
|
+
source: {
|
|
13
|
+
language: 'tsx',
|
|
14
|
+
format: true,
|
|
15
|
+
type: 'code',
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
} satisfies Meta<typeof Avatar>;
|
|
20
|
+
|
|
21
|
+
export default meta;
|
|
22
|
+
type Story = StoryObj<typeof meta>;
|
|
23
|
+
|
|
24
|
+
const exampleSrc =
|
|
25
|
+
'https://plus.unsplash.com/premium_photo-1689551670902-19b441a6afde?q=80&w=774&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D';
|
|
26
|
+
|
|
27
|
+
export const Base: Story = {
|
|
28
|
+
args: {
|
|
29
|
+
src: exampleSrc,
|
|
30
|
+
alt: 'avatar',
|
|
31
|
+
size: 'md',
|
|
32
|
+
showNotification: false,
|
|
33
|
+
},
|
|
34
|
+
render: (args) => <Avatar {...args} />,
|
|
35
|
+
parameters: {
|
|
36
|
+
docs: {
|
|
37
|
+
source: {
|
|
38
|
+
code: `<Avatar src="https://example.com" size="md" alt="avatar" showNotification={false} />`,
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const SizeShowcase: Story = {
|
|
45
|
+
render: () => (
|
|
46
|
+
<Box
|
|
47
|
+
lx={{
|
|
48
|
+
alignItems: 'stretch',
|
|
49
|
+
flexDirection: 'row',
|
|
50
|
+
gap: 's16',
|
|
51
|
+
}}
|
|
52
|
+
>
|
|
53
|
+
<View style={{ alignItems: 'center', justifyContent: 'flex-end' }}>
|
|
54
|
+
<Avatar
|
|
55
|
+
src={exampleSrc}
|
|
56
|
+
alt='avatar'
|
|
57
|
+
size='sm'
|
|
58
|
+
showNotification={false}
|
|
59
|
+
/>
|
|
60
|
+
<Text style={{ marginTop: 4 }}>sm</Text>
|
|
61
|
+
</View>
|
|
62
|
+
<View style={{ alignItems: 'center', justifyContent: 'flex-end' }}>
|
|
63
|
+
<Avatar
|
|
64
|
+
src={exampleSrc}
|
|
65
|
+
alt='avatar'
|
|
66
|
+
size='md'
|
|
67
|
+
showNotification={false}
|
|
68
|
+
/>
|
|
69
|
+
<Text style={{ marginTop: 4 }}>md</Text>
|
|
70
|
+
</View>
|
|
71
|
+
</Box>
|
|
72
|
+
),
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export const FallbackShowcase: Story = {
|
|
76
|
+
args: {
|
|
77
|
+
src: 'https://brokenLink.random',
|
|
78
|
+
size: 'md',
|
|
79
|
+
alt: 'Fallback example',
|
|
80
|
+
showNotification: false,
|
|
81
|
+
},
|
|
82
|
+
render: (args) => <Avatar {...args} />,
|
|
83
|
+
parameters: {
|
|
84
|
+
docs: {
|
|
85
|
+
source: {
|
|
86
|
+
code: `<Avatar src="https://brokenLink.random" size="md" alt="Fallback example" showNotification={false} />`,
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export const NotificationShowcase: Story = {
|
|
93
|
+
render: () => (
|
|
94
|
+
<Box
|
|
95
|
+
lx={{
|
|
96
|
+
flexDirection: 'row',
|
|
97
|
+
gap: 's16',
|
|
98
|
+
}}
|
|
99
|
+
>
|
|
100
|
+
<Avatar
|
|
101
|
+
src={exampleSrc}
|
|
102
|
+
alt='avatar'
|
|
103
|
+
size='md'
|
|
104
|
+
showNotification={false}
|
|
105
|
+
/>
|
|
106
|
+
<Avatar src={exampleSrc} alt='avatar' size='md' showNotification={true} />
|
|
107
|
+
</Box>
|
|
108
|
+
),
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const onPressRedirect = () =>
|
|
112
|
+
Linking.openURL('https://shop.ledger.com/pages/ledger-nano-gen5');
|
|
113
|
+
|
|
114
|
+
export const InteractiveShowcase: Story = {
|
|
115
|
+
render: () => (
|
|
116
|
+
<Pressable
|
|
117
|
+
onPress={onPressRedirect}
|
|
118
|
+
style={({ pressed }) => ({
|
|
119
|
+
borderRadius: 9999,
|
|
120
|
+
opacity: pressed ? 0.7 : 1,
|
|
121
|
+
backgroundColor: pressed ? 'rgba(0, 0, 0, 0.05)' : 'transparent',
|
|
122
|
+
})}
|
|
123
|
+
>
|
|
124
|
+
<Avatar src={exampleSrc} size='md' showNotification />
|
|
125
|
+
</Pressable>
|
|
126
|
+
),
|
|
127
|
+
};
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { describe, it, expect } from '@jest/globals';
|
|
2
|
+
import { ledgerLiveThemes } from '@ledgerhq/lumen-design-core';
|
|
3
|
+
import { render, waitFor } from '@testing-library/react-native';
|
|
4
|
+
import React from 'react';
|
|
5
|
+
import { ThemeProvider } from '../ThemeProvider/ThemeProvider';
|
|
6
|
+
import { Avatar } from './Avatar';
|
|
7
|
+
|
|
8
|
+
const { colors, sizes } = ledgerLiveThemes.dark;
|
|
9
|
+
|
|
10
|
+
const TestWrapper = ({ children }: { children: React.ReactNode }) => (
|
|
11
|
+
<ThemeProvider themes={ledgerLiveThemes} colorScheme='dark' locale='en'>
|
|
12
|
+
{children}
|
|
13
|
+
</ThemeProvider>
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
describe('Avatar Component', () => {
|
|
17
|
+
const testSrc =
|
|
18
|
+
'https://plus.unsplash.com/premium_photo-1689551670902-19b441a6afde?q=80&w=774&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D';
|
|
19
|
+
it('should render correctly with minimal props', () => {
|
|
20
|
+
const { getByLabelText } = render(
|
|
21
|
+
<TestWrapper>
|
|
22
|
+
<Avatar src={testSrc} />
|
|
23
|
+
</TestWrapper>,
|
|
24
|
+
);
|
|
25
|
+
getByLabelText('avatar');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should render with custom alt text', () => {
|
|
29
|
+
const { getByLabelText } = render(
|
|
30
|
+
<TestWrapper>
|
|
31
|
+
<Avatar src={testSrc} alt='user profile' />
|
|
32
|
+
</TestWrapper>,
|
|
33
|
+
);
|
|
34
|
+
getByLabelText('user profile');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should render with different sizes', () => {
|
|
38
|
+
const { getByTestId, rerender } = render(
|
|
39
|
+
<TestWrapper>
|
|
40
|
+
<Avatar testID='avatar-id' size='sm' />
|
|
41
|
+
</TestWrapper>,
|
|
42
|
+
);
|
|
43
|
+
expect(getByTestId('avatar-id').props.style.width).toBe(sizes.s40);
|
|
44
|
+
expect(getByTestId('avatar-id').props.style.height).toBe(sizes.s40);
|
|
45
|
+
|
|
46
|
+
rerender(
|
|
47
|
+
<TestWrapper>
|
|
48
|
+
<Avatar testID='avatar-id' size='md' />
|
|
49
|
+
</TestWrapper>,
|
|
50
|
+
);
|
|
51
|
+
expect(getByTestId('avatar-id').props.style.width).toBe(sizes.s48);
|
|
52
|
+
expect(getByTestId('avatar-id').props.style.height).toBe(sizes.s48);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should render fallback icon when no src is provided', () => {
|
|
56
|
+
const { getByLabelText, getByTestId } = render(
|
|
57
|
+
<TestWrapper>
|
|
58
|
+
<Avatar />
|
|
59
|
+
</TestWrapper>,
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const avatarContainer = getByLabelText('avatar');
|
|
63
|
+
expect(avatarContainer).toBeTruthy();
|
|
64
|
+
expect(avatarContainer.props.accessibilityRole).toBe('image');
|
|
65
|
+
|
|
66
|
+
expect(getByTestId('avatar-fallback-icon')).toBeTruthy();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should render image when src is provided', () => {
|
|
70
|
+
const { getByLabelText, getByTestId } = render(
|
|
71
|
+
<TestWrapper>
|
|
72
|
+
<Avatar src='https://example.com/avatar.jpg' alt='user avatar' />
|
|
73
|
+
</TestWrapper>,
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const avatarContainer = getByLabelText('user avatar');
|
|
77
|
+
expect(avatarContainer).toBeTruthy();
|
|
78
|
+
expect(avatarContainer.props.accessibilityRole).toBe('image');
|
|
79
|
+
|
|
80
|
+
const image = getByTestId('avatar-image');
|
|
81
|
+
expect(image.props.source).toEqual({
|
|
82
|
+
uri: 'https://example.com/avatar.jpg',
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should render fallback icon on image error', async () => {
|
|
87
|
+
const { getByLabelText, getByTestId, queryByTestId, rerender } = render(
|
|
88
|
+
<TestWrapper>
|
|
89
|
+
<Avatar src='https://example.com/invalid.jpg' alt='broken image' />
|
|
90
|
+
</TestWrapper>,
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
const avatarContainer = getByLabelText('broken image');
|
|
94
|
+
expect(avatarContainer).toBeTruthy();
|
|
95
|
+
|
|
96
|
+
const image = getByTestId('avatar-image');
|
|
97
|
+
expect(image).toBeTruthy();
|
|
98
|
+
|
|
99
|
+
image.props.onError();
|
|
100
|
+
|
|
101
|
+
rerender(
|
|
102
|
+
<TestWrapper>
|
|
103
|
+
<Avatar src='https://example.com/invalid.jpg' alt='broken image' />
|
|
104
|
+
</TestWrapper>,
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
await waitFor(() => {
|
|
108
|
+
expect(queryByTestId('avatar-image')).toBeNull();
|
|
109
|
+
expect(getByTestId('avatar-fallback-icon')).toBeTruthy();
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('should reset error state when src changes', async () => {
|
|
114
|
+
const { getByLabelText, getByTestId, rerender } = render(
|
|
115
|
+
<TestWrapper>
|
|
116
|
+
<Avatar src='https://example.com/avatar1.jpg' alt='avatar image' />
|
|
117
|
+
</TestWrapper>,
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
const avatarContainer = getByLabelText('avatar image');
|
|
121
|
+
expect(avatarContainer).toBeTruthy();
|
|
122
|
+
|
|
123
|
+
const image = getByTestId('avatar-image');
|
|
124
|
+
image.props.onError();
|
|
125
|
+
|
|
126
|
+
rerender(
|
|
127
|
+
<TestWrapper>
|
|
128
|
+
<Avatar src='https://example.com/avatar2.jpg' alt='avatar image' />
|
|
129
|
+
</TestWrapper>,
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
await waitFor(() => {
|
|
133
|
+
const newImage = getByTestId('avatar-image');
|
|
134
|
+
expect(newImage.props.source).toEqual({
|
|
135
|
+
uri: 'https://example.com/avatar2.jpg',
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('should show notification indicator when showNotification is true', () => {
|
|
141
|
+
const { getByTestId } = render(
|
|
142
|
+
<TestWrapper>
|
|
143
|
+
<Avatar testID='avatar-id' showNotification />
|
|
144
|
+
</TestWrapper>,
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
const avatar = getByTestId('avatar-id');
|
|
148
|
+
const notificationIndicator = avatar.props.children[0];
|
|
149
|
+
|
|
150
|
+
expect(notificationIndicator).toBeTruthy();
|
|
151
|
+
expect(notificationIndicator.props.style.backgroundColor).toBe(
|
|
152
|
+
colors.bg.errorStrong,
|
|
153
|
+
);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('should not show notification indicator by default', () => {
|
|
157
|
+
const { getByTestId } = render(
|
|
158
|
+
<TestWrapper>
|
|
159
|
+
<Avatar testID='avatar-id' />
|
|
160
|
+
</TestWrapper>,
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
const avatar = getByTestId('avatar-id');
|
|
164
|
+
const notificationIndicator = avatar.props.children[0];
|
|
165
|
+
|
|
166
|
+
expect(notificationIndicator).toBe(false);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('should apply correct notification indicator size based on avatar size', () => {
|
|
170
|
+
const { getByTestId, rerender } = render(
|
|
171
|
+
<TestWrapper>
|
|
172
|
+
<Avatar testID='avatar-id' size='sm' showNotification />
|
|
173
|
+
</TestWrapper>,
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
let avatar = getByTestId('avatar-id');
|
|
177
|
+
let notificationIndicator = avatar.props.children[0];
|
|
178
|
+
expect(notificationIndicator.props.style.width).toBe(sizes.s10);
|
|
179
|
+
expect(notificationIndicator.props.style.height).toBe(sizes.s10);
|
|
180
|
+
|
|
181
|
+
rerender(
|
|
182
|
+
<TestWrapper>
|
|
183
|
+
<Avatar testID='avatar-id' size='md' showNotification />
|
|
184
|
+
</TestWrapper>,
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
avatar = getByTestId('avatar-id');
|
|
188
|
+
notificationIndicator = avatar.props.children[0];
|
|
189
|
+
expect(notificationIndicator.props.style.width).toBe(sizes.s12);
|
|
190
|
+
expect(notificationIndicator.props.style.height).toBe(sizes.s12);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('should apply custom styles', () => {
|
|
194
|
+
const customStyle = { borderWidth: 2 };
|
|
195
|
+
const { getByTestId } = render(
|
|
196
|
+
<TestWrapper>
|
|
197
|
+
<Avatar testID='avatar-id' style={customStyle} />
|
|
198
|
+
</TestWrapper>,
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
const avatar = getByTestId('avatar-id');
|
|
202
|
+
expect(avatar.props.style.borderWidth).toBe(2);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('should pass additional props to the Box component', () => {
|
|
206
|
+
const { getByTestId } = render(
|
|
207
|
+
<TestWrapper>
|
|
208
|
+
<Avatar testID='custom-test-id' accessibilityLabel='Profile Avatar' />
|
|
209
|
+
</TestWrapper>,
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
const avatar = getByTestId('custom-test-id');
|
|
213
|
+
expect(avatar.props.accessibilityLabel).toBe('Profile Avatar');
|
|
214
|
+
});
|
|
215
|
+
});
|