@ledgerhq/lumen-ui-rnative 0.1.28 → 0.1.30
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/dist/module/i18n/locales/de.json +2 -1
- package/dist/module/i18n/locales/en.json +2 -1
- package/dist/module/i18n/locales/es.json +2 -1
- package/dist/module/i18n/locales/fr.json +2 -1
- package/dist/module/i18n/locales/ja.json +3 -2
- package/dist/module/i18n/locales/ko.json +3 -2
- package/dist/module/i18n/locales/pt.json +2 -1
- package/dist/module/i18n/locales/ru.json +2 -1
- package/dist/module/i18n/locales/th.json +2 -1
- package/dist/module/i18n/locales/tr.json +3 -2
- package/dist/module/i18n/locales/zh.json +3 -2
- package/dist/module/lib/Components/Avatar/Avatar.figma.js +2 -1
- package/dist/module/lib/Components/Avatar/Avatar.figma.js.map +1 -1
- package/dist/module/lib/Components/Avatar/Avatar.js +8 -2
- package/dist/module/lib/Components/Avatar/Avatar.js.map +1 -1
- package/dist/module/lib/Components/Avatar/Avatar.mdx +3 -2
- package/dist/module/lib/Components/Avatar/Avatar.stories.js +16 -0
- package/dist/module/lib/Components/Avatar/Avatar.stories.js.map +1 -1
- package/dist/module/lib/Components/Avatar/Avatar.test.js +17 -0
- package/dist/module/lib/Components/Avatar/Avatar.test.js.map +1 -1
- package/dist/module/lib/Components/Card/Card.stories.js +19 -19
- package/dist/module/lib/Components/Card/Card.stories.js.map +1 -1
- package/dist/module/lib/Components/DotIcon/DotIcon.js +1 -1
- package/dist/module/lib/Components/DotIndicator/DotIndicator.js +2 -1
- package/dist/module/lib/Components/DotIndicator/DotIndicator.js.map +1 -1
- package/dist/module/lib/Components/DotIndicator/DotIndicator.mdx +3 -2
- package/dist/module/lib/Components/DotIndicator/DotIndicator.stories.js +20 -4
- package/dist/module/lib/Components/DotIndicator/DotIndicator.stories.js.map +1 -1
- package/dist/module/lib/Components/DotIndicator/DotIndicator.test.js +10 -0
- package/dist/module/lib/Components/DotIndicator/DotIndicator.test.js.map +1 -1
- package/dist/module/lib/Components/ListItem/ListItem.stories.js +3 -3
- package/dist/module/lib/Components/ListItem/ListItem.stories.js.map +1 -1
- package/dist/module/lib/Components/MediaButton/MediaButton.stories.js +4 -4
- package/dist/module/lib/Components/MediaButton/MediaButton.stories.js.map +1 -1
- package/dist/module/lib/Components/MediaCard/MediaCard.stories.js +3 -3
- package/dist/module/lib/Components/MediaCard/MediaCard.stories.js.map +1 -1
- package/dist/module/lib/Components/MediaImage/MediaImage.js +41 -7
- package/dist/module/lib/Components/MediaImage/MediaImage.js.map +1 -1
- package/dist/module/lib/Components/MediaImage/MediaImage.mdx +38 -5
- package/dist/module/lib/Components/MediaImage/MediaImage.stories.js +92 -0
- package/dist/module/lib/Components/MediaImage/MediaImage.stories.js.map +1 -1
- package/dist/module/lib/Components/MediaImage/MediaImage.test.js +117 -0
- package/dist/module/lib/Components/MediaImage/MediaImage.test.js.map +1 -1
- package/dist/module/lib/Components/NavBar/NavBar.mdx +0 -1
- package/dist/module/lib/Components/NavBar/NavBar.stories.js +2 -2
- package/dist/module/lib/Components/NavBar/NavBar.stories.js.map +1 -1
- package/dist/module/lib/Components/OptionList/OptionList.stories.js +7 -7
- package/dist/module/lib/Components/OptionList/OptionList.stories.js.map +1 -1
- package/dist/typescript/src/lib/Components/Avatar/Avatar.d.ts.map +1 -1
- package/dist/typescript/src/lib/Components/Avatar/types.d.ts +1 -1
- package/dist/typescript/src/lib/Components/Avatar/types.d.ts.map +1 -1
- package/dist/typescript/src/lib/Components/DotIndicator/types.d.ts +1 -1
- package/dist/typescript/src/lib/Components/DotIndicator/types.d.ts.map +1 -1
- package/dist/typescript/src/lib/Components/MediaImage/MediaImage.d.ts +9 -3
- package/dist/typescript/src/lib/Components/MediaImage/MediaImage.d.ts.map +1 -1
- package/dist/typescript/src/lib/Components/MediaImage/types.d.ts +12 -0
- package/dist/typescript/src/lib/Components/MediaImage/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/i18n/locales/de.json +2 -1
- package/src/i18n/locales/en.json +2 -1
- package/src/i18n/locales/es.json +2 -1
- package/src/i18n/locales/fr.json +2 -1
- package/src/i18n/locales/ja.json +3 -2
- package/src/i18n/locales/ko.json +3 -2
- package/src/i18n/locales/pt.json +2 -1
- package/src/i18n/locales/ru.json +2 -1
- package/src/i18n/locales/th.json +2 -1
- package/src/i18n/locales/tr.json +3 -2
- package/src/i18n/locales/zh.json +3 -2
- package/src/lib/Components/Avatar/Avatar.figma.tsx +1 -0
- package/src/lib/Components/Avatar/Avatar.mdx +3 -2
- package/src/lib/Components/Avatar/Avatar.stories.tsx +9 -0
- package/src/lib/Components/Avatar/Avatar.test.tsx +17 -0
- package/src/lib/Components/Avatar/Avatar.tsx +4 -1
- package/src/lib/Components/Avatar/types.ts +1 -1
- package/src/lib/Components/Card/Card.stories.tsx +19 -19
- package/src/lib/Components/DotIcon/DotIcon.tsx +1 -1
- package/src/lib/Components/DotIndicator/DotIndicator.mdx +3 -2
- package/src/lib/Components/DotIndicator/DotIndicator.stories.tsx +11 -1
- package/src/lib/Components/DotIndicator/DotIndicator.test.tsx +10 -0
- package/src/lib/Components/DotIndicator/DotIndicator.tsx +2 -1
- package/src/lib/Components/DotIndicator/types.ts +1 -1
- package/src/lib/Components/ListItem/ListItem.stories.tsx +3 -3
- package/src/lib/Components/MediaButton/MediaButton.stories.tsx +4 -4
- package/src/lib/Components/MediaCard/MediaCard.stories.tsx +3 -3
- package/src/lib/Components/MediaImage/MediaImage.mdx +38 -5
- package/src/lib/Components/MediaImage/MediaImage.stories.tsx +32 -0
- package/src/lib/Components/MediaImage/MediaImage.test.tsx +108 -0
- package/src/lib/Components/MediaImage/MediaImage.tsx +37 -3
- package/src/lib/Components/MediaImage/types.ts +12 -0
- package/src/lib/Components/NavBar/NavBar.mdx +0 -1
- package/src/lib/Components/NavBar/NavBar.stories.tsx +2 -2
- package/src/lib/Components/OptionList/OptionList.stories.tsx +11 -11
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import CryptoIcon from '@ledgerhq/crypto-icons/native';
|
|
2
2
|
import type { Meta, StoryObj } from '@storybook/react-native-web-vite';
|
|
3
3
|
import { useState } from 'react';
|
|
4
4
|
import { Settings, ChevronRight, Wallet } from '../../Symbols';
|
|
@@ -102,7 +102,7 @@ export const DensityShowcase: Story = {
|
|
|
102
102
|
<Box lx={{ flexDirection: 'column', maxWidth: 's320', gap: 's8' }}>
|
|
103
103
|
<ListItem density='compact' onPress={() => {}}>
|
|
104
104
|
<ListItemLeading>
|
|
105
|
-
<CryptoIcon ledgerId='bitcoin' ticker='BTC' size=
|
|
105
|
+
<CryptoIcon ledgerId='bitcoin' ticker='BTC' size={24} />
|
|
106
106
|
<ListItemContent>
|
|
107
107
|
<ListItemTitle>Compact with icon</ListItemTitle>
|
|
108
108
|
</ListItemContent>
|
|
@@ -126,7 +126,7 @@ export const DensityShowcase: Story = {
|
|
|
126
126
|
|
|
127
127
|
<ListItem density='expanded' onPress={() => {}}>
|
|
128
128
|
<ListItemLeading>
|
|
129
|
-
<CryptoIcon ledgerId='bitcoin' ticker='BTC' size=
|
|
129
|
+
<CryptoIcon ledgerId='bitcoin' ticker='BTC' size={48} />
|
|
130
130
|
<ListItemContent>
|
|
131
131
|
<ListItemTitle>Expanded with icon</ListItemTitle>
|
|
132
132
|
<ListItemDescription>Additional information</ListItemDescription>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import CryptoIcon from '@ledgerhq/crypto-icons/native';
|
|
2
2
|
import type { Meta, StoryObj } from '@storybook/react-native-web-vite';
|
|
3
3
|
import { Settings, Star } from '../../Symbols';
|
|
4
4
|
import { Box } from '../Utility';
|
|
@@ -66,7 +66,7 @@ export const IconTypeShowcase: Story = {
|
|
|
66
66
|
Flat icon (md)
|
|
67
67
|
</MediaButton>
|
|
68
68
|
<MediaButton
|
|
69
|
-
icon={<CryptoIcon ledgerId='bitcoin' ticker='BTC' size=
|
|
69
|
+
icon={<CryptoIcon ledgerId='bitcoin' ticker='BTC' size={32} />}
|
|
70
70
|
iconType='rounded'
|
|
71
71
|
appearance='gray'
|
|
72
72
|
>
|
|
@@ -84,7 +84,7 @@ export const IconTypeShowcase: Story = {
|
|
|
84
84
|
Flat icon (sm)
|
|
85
85
|
</MediaButton>
|
|
86
86
|
<MediaButton
|
|
87
|
-
icon={<CryptoIcon ledgerId='bitcoin' ticker='BTC' size=
|
|
87
|
+
icon={<CryptoIcon ledgerId='bitcoin' ticker='BTC' size={24} />}
|
|
88
88
|
iconType='rounded'
|
|
89
89
|
appearance='gray'
|
|
90
90
|
size='sm'
|
|
@@ -119,7 +119,7 @@ export const AppearanceShowcase: Story = {
|
|
|
119
119
|
</MediaButton>
|
|
120
120
|
<MediaButton
|
|
121
121
|
appearance={appearance}
|
|
122
|
-
icon={<CryptoIcon ledgerId='bitcoin' ticker='BTC' size=
|
|
122
|
+
icon={<CryptoIcon ledgerId='bitcoin' ticker='BTC' size={32} />}
|
|
123
123
|
iconType='rounded'
|
|
124
124
|
>
|
|
125
125
|
{appearance}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import CryptoIcon from '@ledgerhq/crypto-icons/native';
|
|
2
2
|
import type { Meta, StoryObj } from '@storybook/react-native-web-vite';
|
|
3
3
|
import { useState } from 'react';
|
|
4
4
|
import { Button } from '../Button';
|
|
@@ -121,7 +121,7 @@ export const CompositionShowcase: Story = {
|
|
|
121
121
|
</MediaCard>
|
|
122
122
|
|
|
123
123
|
<MediaCard {...baseArgs}>
|
|
124
|
-
<CryptoIcon ledgerId='bitcoin' ticker='BTC' size=
|
|
124
|
+
<CryptoIcon ledgerId='bitcoin' ticker='BTC' size={32} />
|
|
125
125
|
<MediaCardTitle>With crypto icon</MediaCardTitle>
|
|
126
126
|
</MediaCard>
|
|
127
127
|
</Box>
|
|
@@ -143,7 +143,7 @@ export const CompositionShowcase: Story = {
|
|
|
143
143
|
|
|
144
144
|
{/* With crypto icon */}
|
|
145
145
|
<MediaCard imageUrl="/promo.jpg" onPress={() => {}} onClose={() => {}}>
|
|
146
|
-
<CryptoIcon ledgerId="bitcoin" ticker="BTC" size=
|
|
146
|
+
<CryptoIcon ledgerId="bitcoin" ticker="BTC" size={32} />
|
|
147
147
|
<MediaCardTitle>With crypto icon</MediaCardTitle>
|
|
148
148
|
</MediaCard>`,
|
|
149
149
|
},
|
|
@@ -14,7 +14,7 @@ import CommonRulesDoAndDont from '../../../../.storybook/components/DoVsDont/Com
|
|
|
14
14
|
|
|
15
15
|
## Introduction
|
|
16
16
|
|
|
17
|
-
MediaImage displays an image with consistent sizing and shape. When the image fails to load or no source is provided, a background placeholder is
|
|
17
|
+
MediaImage displays an image with consistent sizing and shape. When the image fails to load or no source is provided, it falls back to a single-letter initial (via `fallback`) or a muted background placeholder. While data is loading, a `loading` prop triggers a pulsing skeleton overlay. This React Native implementation ensures consistent behavior across mobile platforms.
|
|
18
18
|
|
|
19
19
|
> View in [Figma](https://www.figma.com/design/zSkvGGiqcnhywp2l3HTHxA/1.-Symbol-Library?node-id=6159-1866).
|
|
20
20
|
|
|
@@ -22,9 +22,11 @@ MediaImage displays an image with consistent sizing and shape. When the image fa
|
|
|
22
22
|
|
|
23
23
|
<Canvas of={MediaImageStories.Base} />
|
|
24
24
|
|
|
25
|
-
- **Container**: Sized wrapper with rounded corners and
|
|
25
|
+
- **Container**: Sized wrapper with rounded corners, overflow clipping, and a subtle 1px inset outline
|
|
26
26
|
- **Image**: Fills the container using React Native's `Image` component
|
|
27
|
-
- **Fallback**:
|
|
27
|
+
- **Fallback letter**: Single uppercased character shown when `fallback` is set and no image is available
|
|
28
|
+
- **Fallback placeholder**: Muted background shown when neither a valid image nor `fallback` is available
|
|
29
|
+
- **Loading skeleton**: Pulsing overlay shown while `loading` is `true`, regardless of `src`
|
|
28
30
|
|
|
29
31
|
## Properties
|
|
30
32
|
|
|
@@ -35,7 +37,7 @@ MediaImage displays an image with consistent sizing and shape. When the image fa
|
|
|
35
37
|
|
|
36
38
|
### Sizes
|
|
37
39
|
|
|
38
|
-
|
|
40
|
+
Nine sizes are available (12, 16, 20, 24, 32, 40, 48, 56, 64). Border radius scales with size.
|
|
39
41
|
|
|
40
42
|
<Canvas of={MediaImageStories.SizeShowcase} />
|
|
41
43
|
|
|
@@ -46,10 +48,25 @@ Eight sizes are available (12, 16, 20, 24, 32, 40, 48, 56). Border radius scales
|
|
|
46
48
|
- **square** (default): Rounded corners that scale with size
|
|
47
49
|
- **circle**: Fully rounded
|
|
48
50
|
|
|
51
|
+
### Fallback
|
|
52
|
+
|
|
53
|
+
When no `src` is provided or the image fails to load, the component degrades gracefully:
|
|
54
|
+
|
|
55
|
+
- If `fallback` is set, the first character is displayed uppercased at a size-appropriate font size.
|
|
56
|
+
- If `fallback` is not set, a muted background placeholder is shown.
|
|
57
|
+
|
|
58
|
+
<Canvas of={MediaImageStories.FallbackShowcase} />
|
|
59
|
+
|
|
60
|
+
### Loading state
|
|
61
|
+
|
|
62
|
+
When `loading` is `true`, a pulsing skeleton overlay (using the `Pulse` animation component) covers the entire component regardless of whether a `src` is provided.
|
|
63
|
+
|
|
64
|
+
<Canvas of={MediaImageStories.LoadingShowcase} />
|
|
65
|
+
|
|
49
66
|
## Accessibility
|
|
50
67
|
|
|
51
68
|
- The root element uses `accessibilityRole="image"` with `accessibilityLabel` derived from `alt`.
|
|
52
|
-
- The inner `Image`
|
|
69
|
+
- The inner `Image` and fallback elements are marked `accessible={false}` to avoid duplicate announcements.
|
|
53
70
|
- Always provide a meaningful `alt` prop so screen readers can announce the image.
|
|
54
71
|
|
|
55
72
|
</Tab>
|
|
@@ -75,6 +92,22 @@ function MyComponent() {
|
|
|
75
92
|
<MediaImage src='https://example.com/icon.png' alt='Ethereum' size={48} shape='circle' />
|
|
76
93
|
```
|
|
77
94
|
|
|
95
|
+
### With Fallback Letter
|
|
96
|
+
|
|
97
|
+
When no `src` is available or the image fails to load, pass a `fallback` string to show its first letter as a placeholder:
|
|
98
|
+
|
|
99
|
+
```tsx
|
|
100
|
+
<MediaImage fallback='Bitcoin' alt='Bitcoin' size={48} />
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Loading State
|
|
104
|
+
|
|
105
|
+
Show a pulsing skeleton while image data is being fetched:
|
|
106
|
+
|
|
107
|
+
```tsx
|
|
108
|
+
<MediaImage loading alt='Loading…' size={48} />
|
|
109
|
+
```
|
|
110
|
+
|
|
78
111
|
### Custom Styling
|
|
79
112
|
|
|
80
113
|
Use the `lx` prop for token-based layout adjustments:
|
|
@@ -54,3 +54,35 @@ export const ShapeShowcase: Story = {
|
|
|
54
54
|
</Box>
|
|
55
55
|
),
|
|
56
56
|
};
|
|
57
|
+
|
|
58
|
+
export const FallbackShowcase: Story = {
|
|
59
|
+
render: () => (
|
|
60
|
+
<Box lx={{ flexDirection: 'row', alignItems: 'flex-end', gap: 's16' }}>
|
|
61
|
+
<MediaImage fallback='Bitcoin' alt='Bitcoin' size={12} />
|
|
62
|
+
<MediaImage fallback='Bitcoin' alt='Bitcoin' size={16} />
|
|
63
|
+
<MediaImage fallback='Bitcoin' alt='Bitcoin' size={20} />
|
|
64
|
+
<MediaImage fallback='Bitcoin' alt='Bitcoin' size={24} />
|
|
65
|
+
<MediaImage fallback='Bitcoin' alt='Bitcoin' size={32} />
|
|
66
|
+
<MediaImage fallback='Bitcoin' alt='Bitcoin' size={40} />
|
|
67
|
+
<MediaImage fallback='Bitcoin' alt='Bitcoin' size={48} />
|
|
68
|
+
<MediaImage fallback='Bitcoin' alt='Bitcoin' size={56} />
|
|
69
|
+
<MediaImage fallback='Bitcoin' alt='Bitcoin' size={64} />
|
|
70
|
+
</Box>
|
|
71
|
+
),
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export const LoadingShowcase: Story = {
|
|
75
|
+
render: () => (
|
|
76
|
+
<Box lx={{ flexDirection: 'row', alignItems: 'flex-end', gap: 's16' }}>
|
|
77
|
+
<MediaImage loading alt='Loading' size={12} />
|
|
78
|
+
<MediaImage loading alt='Loading' size={16} />
|
|
79
|
+
<MediaImage loading alt='Loading' size={20} />
|
|
80
|
+
<MediaImage loading alt='Loading' size={24} />
|
|
81
|
+
<MediaImage loading alt='Loading' size={32} />
|
|
82
|
+
<MediaImage loading alt='Loading' size={40} />
|
|
83
|
+
<MediaImage loading alt='Loading' size={48} />
|
|
84
|
+
<MediaImage loading alt='Loading' size={56} />
|
|
85
|
+
<MediaImage loading alt='Loading' size={64} />
|
|
86
|
+
</Box>
|
|
87
|
+
),
|
|
88
|
+
};
|
|
@@ -46,6 +46,41 @@ describe('MediaImage Component', () => {
|
|
|
46
46
|
expect(queryByTestId('media-image-img')).toBeNull();
|
|
47
47
|
});
|
|
48
48
|
|
|
49
|
+
it('should render single-letter fallback (uppercased) when fallback is provided and src is missing', () => {
|
|
50
|
+
const { queryByTestId, getByText } = render(
|
|
51
|
+
<TestWrapper>
|
|
52
|
+
<MediaImage fallback='bitcoin' alt='BTC' />
|
|
53
|
+
</TestWrapper>,
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
expect(queryByTestId('media-image-img')).toBeNull();
|
|
57
|
+
expect(getByText('B')).toBeTruthy();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should render single-letter fallback when fallback is provided and src is empty string', () => {
|
|
61
|
+
const { getByText } = render(
|
|
62
|
+
<TestWrapper>
|
|
63
|
+
<MediaImage src='' fallback='ethereum' alt='ETH' />
|
|
64
|
+
</TestWrapper>,
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
expect(getByText('E')).toBeTruthy();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should size the fallback letter according to the size prop', () => {
|
|
71
|
+
const { getByText } = render(
|
|
72
|
+
<TestWrapper>
|
|
73
|
+
<MediaImage fallback='cardano' alt='ADA' size={32} />
|
|
74
|
+
</TestWrapper>,
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const fallback = getByText('C');
|
|
78
|
+
const flatStyle = Array.isArray(fallback.props.style)
|
|
79
|
+
? Object.assign({}, ...fallback.props.style)
|
|
80
|
+
: fallback.props.style;
|
|
81
|
+
expect(flatStyle.fontSize).toBe(16);
|
|
82
|
+
});
|
|
83
|
+
|
|
49
84
|
it('should render fallback when image fails to load', async () => {
|
|
50
85
|
const { getByTestId, queryByTestId, rerender } = render(
|
|
51
86
|
<TestWrapper>
|
|
@@ -67,6 +102,36 @@ describe('MediaImage Component', () => {
|
|
|
67
102
|
});
|
|
68
103
|
});
|
|
69
104
|
|
|
105
|
+
it('should render single-letter fallback when image fails to load and fallback is provided', async () => {
|
|
106
|
+
const { getByTestId, queryByTestId, queryByText, rerender } = render(
|
|
107
|
+
<TestWrapper>
|
|
108
|
+
<MediaImage
|
|
109
|
+
src='https://broken-link.com/404.png'
|
|
110
|
+
fallback='solana'
|
|
111
|
+
alt='SOL'
|
|
112
|
+
/>
|
|
113
|
+
</TestWrapper>,
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
const img = getByTestId('media-image-img');
|
|
117
|
+
img.props.onError();
|
|
118
|
+
|
|
119
|
+
rerender(
|
|
120
|
+
<TestWrapper>
|
|
121
|
+
<MediaImage
|
|
122
|
+
src='https://broken-link.com/404.png'
|
|
123
|
+
fallback='solana'
|
|
124
|
+
alt='SOL'
|
|
125
|
+
/>
|
|
126
|
+
</TestWrapper>,
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
await waitFor(() => {
|
|
130
|
+
expect(queryByTestId('media-image-img')).toBeNull();
|
|
131
|
+
expect(queryByText('S')).toBeTruthy();
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
70
135
|
it('should reset error state when src changes', async () => {
|
|
71
136
|
const { getByTestId, rerender } = render(
|
|
72
137
|
<TestWrapper>
|
|
@@ -176,4 +241,47 @@ describe('MediaImage Component', () => {
|
|
|
176
241
|
|
|
177
242
|
expect(getByTestId('custom-id')).toBeTruthy();
|
|
178
243
|
});
|
|
244
|
+
|
|
245
|
+
describe('loading state', () => {
|
|
246
|
+
it('should render the skeleton overlay when loading is true', () => {
|
|
247
|
+
const { getByTestId } = render(
|
|
248
|
+
<TestWrapper>
|
|
249
|
+
<MediaImage src={validSrc} alt='Test' loading />
|
|
250
|
+
</TestWrapper>,
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
expect(getByTestId('skeleton')).toBeTruthy();
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('should hide the image when loading is true even if src is valid', () => {
|
|
257
|
+
const { queryByTestId } = render(
|
|
258
|
+
<TestWrapper>
|
|
259
|
+
<MediaImage src={validSrc} alt='Test' loading />
|
|
260
|
+
</TestWrapper>,
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
expect(queryByTestId('media-image-img')).toBeNull();
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('should hide the fallback letter when loading is true', () => {
|
|
267
|
+
const { queryByText, getByTestId } = render(
|
|
268
|
+
<TestWrapper>
|
|
269
|
+
<MediaImage fallback='bitcoin' alt='BTC' loading />
|
|
270
|
+
</TestWrapper>,
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
expect(queryByText('B')).toBeNull();
|
|
274
|
+
expect(getByTestId('skeleton')).toBeTruthy();
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('should not render the skeleton when loading is false (default)', () => {
|
|
278
|
+
const { queryByTestId } = render(
|
|
279
|
+
<TestWrapper>
|
|
280
|
+
<MediaImage src={validSrc} alt='Test' />
|
|
281
|
+
</TestWrapper>,
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
expect(queryByTestId('skeleton')).toBeNull();
|
|
285
|
+
});
|
|
286
|
+
});
|
|
179
287
|
});
|
|
@@ -2,7 +2,8 @@ import { useEffect, useState } from 'react';
|
|
|
2
2
|
import { Image, StyleSheet } from 'react-native';
|
|
3
3
|
import { useStyleSheet } from '../../../styles';
|
|
4
4
|
import type { LumenStyleSheetTheme } from '../../../styles';
|
|
5
|
-
import {
|
|
5
|
+
import { Skeleton } from '../Skeleton';
|
|
6
|
+
import { Box, Text } from '../Utility';
|
|
6
7
|
import type { MediaImageProps, MediaImageSize, MediaImageShape } from './types';
|
|
7
8
|
|
|
8
9
|
type BorderRadiusKey = keyof LumenStyleSheetTheme['borderRadius'];
|
|
@@ -19,6 +20,18 @@ const borderRadiusMap: Record<MediaImageSize, BorderRadiusKey> = {
|
|
|
19
20
|
64: 'lg',
|
|
20
21
|
};
|
|
21
22
|
|
|
23
|
+
export const fontSizeMap: Record<MediaImageSize, number> = {
|
|
24
|
+
12: 10,
|
|
25
|
+
16: 10,
|
|
26
|
+
20: 12,
|
|
27
|
+
24: 14,
|
|
28
|
+
32: 16,
|
|
29
|
+
40: 18,
|
|
30
|
+
48: 24,
|
|
31
|
+
56: 24,
|
|
32
|
+
64: 24,
|
|
33
|
+
};
|
|
34
|
+
|
|
22
35
|
const useStyles = ({
|
|
23
36
|
size,
|
|
24
37
|
shape,
|
|
@@ -43,6 +56,10 @@ const useStyles = ({
|
|
|
43
56
|
alignItems: 'center',
|
|
44
57
|
justifyContent: 'center',
|
|
45
58
|
backgroundColor: t.colors.bg.muted,
|
|
59
|
+
outlineColor: t.colors.border.icon,
|
|
60
|
+
outlineWidth: 1,
|
|
61
|
+
outlineOffset: -1,
|
|
62
|
+
outlineStyle: 'solid',
|
|
46
63
|
},
|
|
47
64
|
image: {
|
|
48
65
|
width: '100%',
|
|
@@ -58,18 +75,25 @@ const useStyles = ({
|
|
|
58
75
|
* A generic media image component that displays an image with optional shape variants.
|
|
59
76
|
* Supports square and circular appearances with consistent sizing.
|
|
60
77
|
*
|
|
61
|
-
* When the image fails to load or no src is provided, displays a
|
|
78
|
+
* When the image fails to load or no src is provided, displays a fallback letter (if `fallback`
|
|
79
|
+
* is provided) or a muted background placeholder.
|
|
80
|
+
*
|
|
81
|
+
* While `loading` is true, a pulsing skeleton overlay is shown regardless of `src`.
|
|
62
82
|
*
|
|
63
83
|
* @example
|
|
64
84
|
* import { MediaImage } from '@ledgerhq/lumen-ui-rnative';
|
|
65
85
|
*
|
|
66
86
|
* <MediaImage src="https://example.com/icon.png" alt="Bitcoin" size={32} />
|
|
87
|
+
* <MediaImage fallback="Bitcoin" size={32} />
|
|
88
|
+
* <MediaImage loading size={32} />
|
|
67
89
|
*/
|
|
68
90
|
export const MediaImage = ({
|
|
69
91
|
src,
|
|
70
92
|
alt,
|
|
71
93
|
size = 48,
|
|
72
94
|
shape = 'square',
|
|
95
|
+
fallback,
|
|
96
|
+
loading = false,
|
|
73
97
|
lx = {},
|
|
74
98
|
style,
|
|
75
99
|
ref,
|
|
@@ -92,7 +116,17 @@ export const MediaImage = ({
|
|
|
92
116
|
accessibilityLabel={alt}
|
|
93
117
|
{...props}
|
|
94
118
|
>
|
|
95
|
-
{
|
|
119
|
+
{loading && <Skeleton style={StyleSheet.absoluteFillObject} />}
|
|
120
|
+
{!loading && shouldFallback && fallback && (
|
|
121
|
+
<Text
|
|
122
|
+
style={{ fontSize: fontSizeMap[size] }}
|
|
123
|
+
lx={{ color: 'base' }}
|
|
124
|
+
accessible={false}
|
|
125
|
+
>
|
|
126
|
+
{fallback[0]?.toUpperCase()}
|
|
127
|
+
</Text>
|
|
128
|
+
)}
|
|
129
|
+
{!loading && !shouldFallback && (
|
|
96
130
|
<Image
|
|
97
131
|
source={{ uri: src }}
|
|
98
132
|
style={styles.image}
|
|
@@ -24,4 +24,16 @@ export type MediaImageProps = {
|
|
|
24
24
|
* Alternative text for the image, used for accessibility.
|
|
25
25
|
*/
|
|
26
26
|
alt?: string;
|
|
27
|
+
/**
|
|
28
|
+
* Text used to derive a single-letter fallback when no `src` is provided or the image fails to load.
|
|
29
|
+
* The first character is displayed, uppercased.
|
|
30
|
+
* @optional
|
|
31
|
+
*/
|
|
32
|
+
fallback?: string;
|
|
33
|
+
/**
|
|
34
|
+
* When true, displays a pulsing skeleton placeholder instead of the image or fallback.
|
|
35
|
+
* @optional
|
|
36
|
+
* @default false
|
|
37
|
+
*/
|
|
38
|
+
loading?: boolean;
|
|
27
39
|
} & Omit<StyledViewProps, 'children'>;
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { Meta, Canvas, Controls } from '@storybook/addon-docs/blocks';
|
|
2
|
-
import { CryptoIcon } from '@ledgerhq/crypto-icons';
|
|
3
2
|
import { Box } from '../Utility';
|
|
4
3
|
import * as NavBarStories from './NavBar.stories';
|
|
5
4
|
import { NavBar, NavBarBackButton, NavBarContent } from './NavBar';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import CryptoIcon from '@ledgerhq/crypto-icons/native';
|
|
2
2
|
import type { Meta, StoryObj } from '@storybook/react-native-web-vite';
|
|
3
3
|
import { MoreHorizontal, Settings } from '../../Symbols';
|
|
4
4
|
import { IconButton } from '../IconButton';
|
|
@@ -111,7 +111,7 @@ export const WithCoinCapsule: Story = {
|
|
|
111
111
|
<NavBarContent>
|
|
112
112
|
<NavBarCoinCapsule
|
|
113
113
|
ticker='BTC'
|
|
114
|
-
icon={<CryptoIcon ledgerId='bitcoin' ticker='BTC' size=
|
|
114
|
+
icon={<CryptoIcon ledgerId='bitcoin' ticker='BTC' size={24} />}
|
|
115
115
|
/>
|
|
116
116
|
</NavBarContent>
|
|
117
117
|
<NavBarTrailing>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import CryptoIcon from '@ledgerhq/crypto-icons/native';
|
|
2
2
|
import type { Meta, StoryObj } from '@storybook/react-native-web-vite';
|
|
3
3
|
import { useState } from 'react';
|
|
4
4
|
import { Settings } from '../../Symbols';
|
|
@@ -132,9 +132,9 @@ export const Base: Story = {
|
|
|
132
132
|
<OptionListItem value={item.value}>
|
|
133
133
|
<OptionListItemLeading>
|
|
134
134
|
<CryptoIcon
|
|
135
|
-
ledgerId={item.meta?.ledgerId ?? ''}
|
|
135
|
+
ledgerId={(item.meta?.ledgerId as string) ?? ''}
|
|
136
136
|
ticker={ticker}
|
|
137
|
-
size=
|
|
137
|
+
size={32}
|
|
138
138
|
/>
|
|
139
139
|
</OptionListItemLeading>
|
|
140
140
|
<OptionListItemContent>
|
|
@@ -277,7 +277,7 @@ export const WithContentRow: Story = {
|
|
|
277
277
|
<CryptoIcon
|
|
278
278
|
ledgerId={meta.ledgerId}
|
|
279
279
|
ticker={meta.ticker}
|
|
280
|
-
size=
|
|
280
|
+
size={32}
|
|
281
281
|
/>
|
|
282
282
|
</OptionListItemLeading>
|
|
283
283
|
<OptionListItemContent>
|
|
@@ -453,7 +453,7 @@ export const GroupedWithContentRow: Story = {
|
|
|
453
453
|
<CryptoIcon
|
|
454
454
|
ledgerId={meta.ledgerId}
|
|
455
455
|
ticker={meta.ticker}
|
|
456
|
-
size=
|
|
456
|
+
size={32}
|
|
457
457
|
/>
|
|
458
458
|
</OptionListItemLeading>
|
|
459
459
|
<OptionListItemContent>
|
|
@@ -623,9 +623,9 @@ export const TriggerShowcase: Story = {
|
|
|
623
623
|
icon={
|
|
624
624
|
selectedCrypto?.meta ? (
|
|
625
625
|
<CryptoIcon
|
|
626
|
-
ledgerId={selectedCrypto.meta.ledgerId}
|
|
627
|
-
ticker={selectedCrypto.meta.ticker}
|
|
628
|
-
size=
|
|
626
|
+
ledgerId={selectedCrypto.meta.ledgerId as string}
|
|
627
|
+
ticker={selectedCrypto.meta.ticker as string}
|
|
628
|
+
size={32}
|
|
629
629
|
/>
|
|
630
630
|
) : undefined
|
|
631
631
|
}
|
|
@@ -688,9 +688,9 @@ export const TriggerShowcase: Story = {
|
|
|
688
688
|
<OptionListItem value={item.value}>
|
|
689
689
|
<OptionListItemLeading>
|
|
690
690
|
<CryptoIcon
|
|
691
|
-
ledgerId={item.meta?.ledgerId ?? ''}
|
|
691
|
+
ledgerId={(item.meta?.ledgerId as string) ?? ''}
|
|
692
692
|
ticker={ticker}
|
|
693
|
-
size=
|
|
693
|
+
size={32}
|
|
694
694
|
/>
|
|
695
695
|
</OptionListItemLeading>
|
|
696
696
|
<OptionListItemContent>
|
|
@@ -736,7 +736,7 @@ export const WithDefaultValue: Story = {
|
|
|
736
736
|
<CryptoIcon
|
|
737
737
|
ledgerId={meta.ledgerId}
|
|
738
738
|
ticker={meta.ticker}
|
|
739
|
-
size=
|
|
739
|
+
size={32}
|
|
740
740
|
/>
|
|
741
741
|
</OptionListItemLeading>
|
|
742
742
|
<OptionListItemContent>
|