@ledgerhq/lumen-ui-rnative 0.1.18 → 0.1.20
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/lib/Components/DotSymbol/DotSymbol.js +141 -0
- package/dist/module/lib/Components/DotSymbol/DotSymbol.js.map +1 -0
- package/dist/module/lib/Components/DotSymbol/DotSymbol.mdx +54 -0
- package/dist/module/lib/Components/DotSymbol/DotSymbol.stories.js +301 -0
- package/dist/module/lib/Components/DotSymbol/DotSymbol.stories.js.map +1 -0
- package/dist/module/lib/Components/DotSymbol/DotSymbol.test.js +134 -0
- package/dist/module/lib/Components/DotSymbol/DotSymbol.test.js.map +1 -0
- package/dist/module/lib/Components/DotSymbol/index.js +5 -0
- package/dist/module/lib/Components/DotSymbol/index.js.map +1 -0
- package/dist/module/lib/Components/DotSymbol/types.js +4 -0
- package/dist/module/lib/Components/DotSymbol/types.js.map +1 -0
- package/dist/module/lib/Components/MediaImage/MediaImage.js +1 -11
- package/dist/module/lib/Components/MediaImage/MediaImage.js.map +1 -1
- package/dist/module/lib/Components/MediaImage/index.js +1 -1
- package/dist/module/lib/Components/MediaImage/index.js.map +1 -1
- package/dist/module/lib/Components/NavBar/NavBar.js +0 -2
- package/dist/module/lib/Components/NavBar/NavBar.js.map +1 -1
- package/dist/module/lib/Components/Select/Select.stories.js +1 -0
- package/dist/module/lib/Components/Select/Select.stories.js.map +1 -1
- package/dist/module/lib/Components/index.js +1 -0
- package/dist/module/lib/Components/index.js.map +1 -1
- package/dist/typescript/src/lib/Components/DotSymbol/DotSymbol.d.ts +21 -0
- package/dist/typescript/src/lib/Components/DotSymbol/DotSymbol.d.ts.map +1 -0
- package/dist/typescript/src/lib/Components/DotSymbol/index.d.ts +3 -0
- package/dist/typescript/src/lib/Components/DotSymbol/index.d.ts.map +1 -0
- package/dist/typescript/src/lib/Components/DotSymbol/types.d.ts +34 -0
- package/dist/typescript/src/lib/Components/DotSymbol/types.d.ts.map +1 -0
- package/dist/typescript/src/lib/Components/MediaImage/MediaImage.d.ts +1 -2
- package/dist/typescript/src/lib/Components/MediaImage/MediaImage.d.ts.map +1 -1
- package/dist/typescript/src/lib/Components/MediaImage/index.d.ts +1 -1
- package/dist/typescript/src/lib/Components/MediaImage/index.d.ts.map +1 -1
- package/dist/typescript/src/lib/Components/index.d.ts +1 -0
- package/dist/typescript/src/lib/Components/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/lib/Components/DotSymbol/DotSymbol.mdx +54 -0
- package/src/lib/Components/DotSymbol/DotSymbol.stories.tsx +191 -0
- package/src/lib/Components/DotSymbol/DotSymbol.test.tsx +119 -0
- package/src/lib/Components/DotSymbol/DotSymbol.tsx +161 -0
- package/src/lib/Components/DotSymbol/index.ts +2 -0
- package/src/lib/Components/DotSymbol/types.ts +40 -0
- package/src/lib/Components/MediaImage/MediaImage.tsx +6 -17
- package/src/lib/Components/MediaImage/index.ts +1 -1
- package/src/lib/Components/NavBar/NavBar.tsx +0 -3
- package/src/lib/Components/Select/Select.stories.tsx +1 -0
- package/src/lib/Components/index.ts +1 -0
|
@@ -0,0 +1,119 @@
|
|
|
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 { Text } from 'react-native';
|
|
5
|
+
import { ThemeProvider } from '../ThemeProvider/ThemeProvider';
|
|
6
|
+
import { DotSymbol } from './DotSymbol';
|
|
7
|
+
|
|
8
|
+
const TestWrapper = ({ children }: { children: React.ReactNode }) => (
|
|
9
|
+
<ThemeProvider themes={ledgerLiveThemes} colorScheme='dark' locale='en'>
|
|
10
|
+
{children}
|
|
11
|
+
</ThemeProvider>
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
describe('DotSymbol Component', () => {
|
|
15
|
+
const dotSrc = 'https://crypto-icons.ledger.com/BTC.png';
|
|
16
|
+
|
|
17
|
+
it('should render children and dot image', () => {
|
|
18
|
+
const { getByText, getByTestId } = render(
|
|
19
|
+
<TestWrapper>
|
|
20
|
+
<DotSymbol src={dotSrc} alt='Bitcoin'>
|
|
21
|
+
<Text>Child</Text>
|
|
22
|
+
</DotSymbol>
|
|
23
|
+
</TestWrapper>,
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
expect(getByText('Child')).toBeTruthy();
|
|
27
|
+
|
|
28
|
+
const img = getByTestId('dot-symbol-img');
|
|
29
|
+
expect(img.props.source).toEqual({ uri: dotSrc });
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should render without children', () => {
|
|
33
|
+
const { getByTestId } = render(
|
|
34
|
+
<TestWrapper>
|
|
35
|
+
<DotSymbol src={dotSrc} />
|
|
36
|
+
</TestWrapper>,
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
expect(getByTestId('dot-symbol-img')).toBeTruthy();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should render fallback when image fails to load', async () => {
|
|
43
|
+
const { getByTestId, queryByTestId, rerender } = render(
|
|
44
|
+
<TestWrapper>
|
|
45
|
+
<DotSymbol src='https://broken-link.com/404.png' />
|
|
46
|
+
</TestWrapper>,
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const img = getByTestId('dot-symbol-img');
|
|
50
|
+
img.props.onError();
|
|
51
|
+
|
|
52
|
+
rerender(
|
|
53
|
+
<TestWrapper>
|
|
54
|
+
<DotSymbol src='https://broken-link.com/404.png' />
|
|
55
|
+
</TestWrapper>,
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
await waitFor(() => {
|
|
59
|
+
expect(queryByTestId('dot-symbol-img')).toBeNull();
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should reset error state when src changes', async () => {
|
|
64
|
+
const { getByTestId, rerender } = render(
|
|
65
|
+
<TestWrapper>
|
|
66
|
+
<DotSymbol src='https://broken-link.com/404.png' />
|
|
67
|
+
</TestWrapper>,
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const img = getByTestId('dot-symbol-img');
|
|
71
|
+
img.props.onError();
|
|
72
|
+
|
|
73
|
+
rerender(
|
|
74
|
+
<TestWrapper>
|
|
75
|
+
<DotSymbol src={dotSrc} />
|
|
76
|
+
</TestWrapper>,
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
await waitFor(() => {
|
|
80
|
+
const newImg = getByTestId('dot-symbol-img');
|
|
81
|
+
expect(newImg.props.source).toEqual({ uri: dotSrc });
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should set accessibility label from alt prop', () => {
|
|
86
|
+
const { getByLabelText } = render(
|
|
87
|
+
<TestWrapper>
|
|
88
|
+
<DotSymbol src={dotSrc} alt='Bitcoin network' />
|
|
89
|
+
</TestWrapper>,
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
expect(getByLabelText('Bitcoin network')).toBeTruthy();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should apply custom styles', () => {
|
|
96
|
+
const { getByTestId } = render(
|
|
97
|
+
<TestWrapper>
|
|
98
|
+
<DotSymbol testID='ds' src={dotSrc} style={{ marginTop: 10 }} />
|
|
99
|
+
</TestWrapper>,
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
const root = getByTestId('ds');
|
|
103
|
+
expect(root.props.style.marginTop).toBe(10);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('should pass additional props', () => {
|
|
107
|
+
const { getByTestId } = render(
|
|
108
|
+
<TestWrapper>
|
|
109
|
+
<DotSymbol testID='custom-dot' src={dotSrc} />
|
|
110
|
+
</TestWrapper>,
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
expect(getByTestId('custom-dot')).toBeTruthy();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('should have correct displayName', () => {
|
|
117
|
+
expect(DotSymbol.displayName).toBe('DotSymbol');
|
|
118
|
+
});
|
|
119
|
+
});
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { Image, StyleSheet } from 'react-native';
|
|
3
|
+
import { useStyleSheet } from '../../../styles';
|
|
4
|
+
import { MediaImageSize } from '../MediaImage';
|
|
5
|
+
import { SpotSize } from '../Spot';
|
|
6
|
+
import { Box } from '../Utility';
|
|
7
|
+
import { DotSymbolPin, DotSymbolProps, DotSymbolSize } from './types';
|
|
8
|
+
|
|
9
|
+
type BorderRadiusKey = 'xs' | 'sm' | 'md' | 'lg' | 'full';
|
|
10
|
+
|
|
11
|
+
const shapeRadiusMap: Record<DotSymbolSize, BorderRadiusKey> = {
|
|
12
|
+
8: 'xs',
|
|
13
|
+
10: 'xs',
|
|
14
|
+
12: 'xs',
|
|
15
|
+
16: 'sm',
|
|
16
|
+
20: 'sm',
|
|
17
|
+
24: 'md',
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const offsetBySize: Record<DotSymbolSize, number> = {
|
|
21
|
+
8: -2,
|
|
22
|
+
10: -2,
|
|
23
|
+
12: -2,
|
|
24
|
+
16: -3,
|
|
25
|
+
20: -3,
|
|
26
|
+
24: -3,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const mediaImageDotSizeMap: Record<MediaImageSize, DotSymbolSize> = {
|
|
30
|
+
12: 8,
|
|
31
|
+
16: 8,
|
|
32
|
+
20: 8,
|
|
33
|
+
24: 10,
|
|
34
|
+
32: 12,
|
|
35
|
+
40: 16,
|
|
36
|
+
48: 20,
|
|
37
|
+
56: 24,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const spotDotSizeMap: Record<SpotSize, DotSymbolSize> = {
|
|
41
|
+
32: 12,
|
|
42
|
+
40: 16,
|
|
43
|
+
48: 20,
|
|
44
|
+
56: 24,
|
|
45
|
+
72: 24,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const pinAxisMap: Record<DotSymbolPin, [vertical: string, horizontal: string]> =
|
|
49
|
+
{
|
|
50
|
+
'top-start': ['top', 'left'],
|
|
51
|
+
'top-end': ['top', 'right'],
|
|
52
|
+
'bottom-start': ['bottom', 'left'],
|
|
53
|
+
'bottom-end': ['bottom', 'right'],
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const getPinOffset = (
|
|
57
|
+
pin: DotSymbolPin,
|
|
58
|
+
size: DotSymbolSize,
|
|
59
|
+
): Record<string, number> => {
|
|
60
|
+
const [v, h] = pinAxisMap[pin];
|
|
61
|
+
const offset = offsetBySize[size];
|
|
62
|
+
return { [v]: offset, [h]: offset };
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const useStyles = ({
|
|
66
|
+
size,
|
|
67
|
+
shape,
|
|
68
|
+
pin,
|
|
69
|
+
}: {
|
|
70
|
+
size: DotSymbolSize;
|
|
71
|
+
shape: 'square' | 'circle';
|
|
72
|
+
pin: DotSymbolPin;
|
|
73
|
+
}) => {
|
|
74
|
+
return useStyleSheet(
|
|
75
|
+
(t) => {
|
|
76
|
+
const sizeValue = t.sizes[`s${size}` as keyof typeof t.sizes] as number;
|
|
77
|
+
const radius =
|
|
78
|
+
shape === 'circle'
|
|
79
|
+
? t.borderRadius.full
|
|
80
|
+
: t.borderRadius[shapeRadiusMap[size]];
|
|
81
|
+
const pinOffset = getPinOffset(pin, size);
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
dot: {
|
|
85
|
+
position: 'absolute',
|
|
86
|
+
zIndex: 10,
|
|
87
|
+
width: sizeValue,
|
|
88
|
+
height: sizeValue,
|
|
89
|
+
borderRadius: radius,
|
|
90
|
+
borderWidth: 1,
|
|
91
|
+
backgroundColor: t.colors.bg.muted,
|
|
92
|
+
borderColor: t.colors.border.baseInverted,
|
|
93
|
+
overflow: 'hidden',
|
|
94
|
+
...pinOffset,
|
|
95
|
+
},
|
|
96
|
+
image: {
|
|
97
|
+
width: '100%',
|
|
98
|
+
height: '100%',
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
},
|
|
102
|
+
[size, shape, pin],
|
|
103
|
+
);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* A wrapper component that positions a small image indicator at a configurable
|
|
108
|
+
* corner of a child element like MediaImage or Spot.
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* import { DotSymbol } from '@ledgerhq/lumen-ui-rnative';
|
|
112
|
+
*
|
|
113
|
+
* <DotSymbol src="https://example.com/eth.png" alt="Ethereum" pin="bottom-end">
|
|
114
|
+
* <MediaImage src="https://example.com/usdc.png" alt="USDC" size={48} />
|
|
115
|
+
* </DotSymbol>
|
|
116
|
+
*/
|
|
117
|
+
export const DotSymbol = ({
|
|
118
|
+
children,
|
|
119
|
+
src,
|
|
120
|
+
alt,
|
|
121
|
+
pin = 'bottom-end',
|
|
122
|
+
size = 20,
|
|
123
|
+
shape = 'circle',
|
|
124
|
+
lx = {},
|
|
125
|
+
style,
|
|
126
|
+
ref,
|
|
127
|
+
...rest
|
|
128
|
+
}: DotSymbolProps) => {
|
|
129
|
+
const styles = useStyles({ size, shape, pin });
|
|
130
|
+
const [error, setError] = useState(false);
|
|
131
|
+
|
|
132
|
+
useEffect(() => {
|
|
133
|
+
setError(false);
|
|
134
|
+
}, [src]);
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<Box
|
|
138
|
+
ref={ref}
|
|
139
|
+
lx={lx}
|
|
140
|
+
style={StyleSheet.flatten([{ position: 'relative' }, style])}
|
|
141
|
+
accessibilityRole='image'
|
|
142
|
+
accessibilityLabel={alt}
|
|
143
|
+
{...rest}
|
|
144
|
+
>
|
|
145
|
+
{children}
|
|
146
|
+
<Box style={styles.dot}>
|
|
147
|
+
{!error && (
|
|
148
|
+
<Image
|
|
149
|
+
source={{ uri: src }}
|
|
150
|
+
style={styles.image}
|
|
151
|
+
accessible={false}
|
|
152
|
+
onError={() => setError(true)}
|
|
153
|
+
testID='dot-symbol-img'
|
|
154
|
+
/>
|
|
155
|
+
)}
|
|
156
|
+
</Box>
|
|
157
|
+
</Box>
|
|
158
|
+
);
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
DotSymbol.displayName = 'DotSymbol';
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { StyledViewProps } from '../../../styles';
|
|
3
|
+
|
|
4
|
+
export type DotSymbolSize = 8 | 10 | 12 | 16 | 20 | 24;
|
|
5
|
+
|
|
6
|
+
export type DotSymbolPin =
|
|
7
|
+
| 'top-start'
|
|
8
|
+
| 'top-end'
|
|
9
|
+
| 'bottom-start'
|
|
10
|
+
| 'bottom-end';
|
|
11
|
+
|
|
12
|
+
export type DotSymbolProps = {
|
|
13
|
+
/**
|
|
14
|
+
* Image source URL for the dot indicator.
|
|
15
|
+
*/
|
|
16
|
+
src: string;
|
|
17
|
+
/**
|
|
18
|
+
* Alternative text for the dot image.
|
|
19
|
+
*/
|
|
20
|
+
alt?: string;
|
|
21
|
+
/**
|
|
22
|
+
* Corner placement of the dot indicator.
|
|
23
|
+
* @default 'bottom-end'
|
|
24
|
+
*/
|
|
25
|
+
pin?: DotSymbolPin;
|
|
26
|
+
/**
|
|
27
|
+
* The size of the dot indicator in pixels.
|
|
28
|
+
* @default 20
|
|
29
|
+
*/
|
|
30
|
+
size?: DotSymbolSize;
|
|
31
|
+
/**
|
|
32
|
+
* The shape of the dot indicator.
|
|
33
|
+
* @default 'circle'
|
|
34
|
+
*/
|
|
35
|
+
shape?: 'square' | 'circle';
|
|
36
|
+
/**
|
|
37
|
+
* The wrapped component (e.g. MediaImage or Spot).
|
|
38
|
+
*/
|
|
39
|
+
children?: ReactNode;
|
|
40
|
+
} & Omit<StyledViewProps, 'children'>;
|
|
@@ -17,17 +17,6 @@ const borderRadiusMap: Record<MediaImageSize, BorderRadiusKey> = {
|
|
|
17
17
|
56: 'lg',
|
|
18
18
|
};
|
|
19
19
|
|
|
20
|
-
export const mediaImageDotSizeMap: Record<MediaImageSize, number> = {
|
|
21
|
-
12: 8,
|
|
22
|
-
16: 8,
|
|
23
|
-
20: 8,
|
|
24
|
-
24: 10,
|
|
25
|
-
32: 12,
|
|
26
|
-
40: 16,
|
|
27
|
-
48: 20,
|
|
28
|
-
56: 24,
|
|
29
|
-
} as const;
|
|
30
|
-
|
|
31
20
|
const useStyles = ({
|
|
32
21
|
size,
|
|
33
22
|
shape,
|
|
@@ -48,14 +37,14 @@ const useStyles = ({
|
|
|
48
37
|
width: sizeValue,
|
|
49
38
|
height: sizeValue,
|
|
50
39
|
borderRadius: radius,
|
|
51
|
-
overflow: 'hidden'
|
|
52
|
-
alignItems: 'center'
|
|
53
|
-
justifyContent: 'center'
|
|
54
|
-
backgroundColor: t.colors.bg.
|
|
40
|
+
overflow: 'hidden',
|
|
41
|
+
alignItems: 'center',
|
|
42
|
+
justifyContent: 'center',
|
|
43
|
+
backgroundColor: t.colors.bg.muted,
|
|
55
44
|
},
|
|
56
45
|
image: {
|
|
57
|
-
width: '100%'
|
|
58
|
-
height: '100%'
|
|
46
|
+
width: '100%',
|
|
47
|
+
height: '100%',
|
|
59
48
|
},
|
|
60
49
|
};
|
|
61
50
|
},
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { MediaImage
|
|
1
|
+
export { MediaImage } from './MediaImage';
|
|
2
2
|
export * from './types';
|