@ledgerhq/lumen-ui-rnative 0.1.1 → 0.1.3

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.
Files changed (101) hide show
  1. package/dist/package.json +3 -3
  2. package/dist/src/i18n/locales/de.json +3 -0
  3. package/dist/src/i18n/locales/en.json +3 -0
  4. package/dist/src/i18n/locales/es.json +3 -0
  5. package/dist/src/i18n/locales/fr.json +3 -0
  6. package/dist/src/i18n/locales/ja.json +3 -0
  7. package/dist/src/i18n/locales/ko.json +3 -0
  8. package/dist/src/i18n/locales/pt.json +3 -0
  9. package/dist/src/i18n/locales/ru.json +3 -0
  10. package/dist/src/i18n/locales/th.json +3 -0
  11. package/dist/src/i18n/locales/tr.json +3 -0
  12. package/dist/src/i18n/locales/zh.json +3 -0
  13. package/dist/src/lib/Animations/constants.d.ts +28 -0
  14. package/dist/src/lib/Animations/constants.d.ts.map +1 -0
  15. package/dist/src/lib/Animations/constants.js +27 -0
  16. package/dist/src/lib/Animations/index.d.ts +1 -0
  17. package/dist/src/lib/Animations/index.d.ts.map +1 -1
  18. package/dist/src/lib/Animations/index.js +1 -0
  19. package/dist/src/lib/Components/AmountDisplay/AmountDisplay.d.ts +1 -1
  20. package/dist/src/lib/Components/AmountDisplay/AmountDisplay.d.ts.map +1 -1
  21. package/dist/src/lib/Components/AmountDisplay/AmountDisplay.js +76 -5
  22. package/dist/src/lib/Components/AmountDisplay/AmountDisplay.stories.d.ts +1 -0
  23. package/dist/src/lib/Components/AmountDisplay/AmountDisplay.stories.d.ts.map +1 -1
  24. package/dist/src/lib/Components/AmountDisplay/AmountDisplay.stories.js +25 -2
  25. package/dist/src/lib/Components/AmountDisplay/types.d.ts +20 -25
  26. package/dist/src/lib/Components/AmountDisplay/types.d.ts.map +1 -1
  27. package/dist/src/lib/Components/AmountDisplay/types.js +1 -1
  28. package/dist/src/lib/Components/SegmentedControl/SegmentedControl.d.ts +10 -0
  29. package/dist/src/lib/Components/SegmentedControl/SegmentedControl.d.ts.map +1 -0
  30. package/dist/src/lib/Components/SegmentedControl/SegmentedControl.js +114 -0
  31. package/dist/src/lib/Components/SegmentedControl/SegmentedControl.stories.d.ts +58 -0
  32. package/dist/src/lib/Components/SegmentedControl/SegmentedControl.stories.d.ts.map +1 -0
  33. package/dist/src/lib/Components/SegmentedControl/SegmentedControl.stories.js +61 -0
  34. package/dist/src/lib/Components/SegmentedControl/SegmentedControlContext.d.ts +11 -0
  35. package/dist/src/lib/Components/SegmentedControl/SegmentedControlContext.d.ts.map +1 -0
  36. package/dist/src/lib/Components/SegmentedControl/SegmentedControlContext.js +7 -0
  37. package/dist/src/lib/Components/SegmentedControl/index.d.ts +3 -0
  38. package/dist/src/lib/Components/SegmentedControl/index.d.ts.map +1 -0
  39. package/dist/src/lib/Components/SegmentedControl/index.js +1 -0
  40. package/dist/src/lib/Components/SegmentedControl/types.d.ts +45 -0
  41. package/dist/src/lib/Components/SegmentedControl/types.d.ts.map +1 -0
  42. package/dist/src/lib/Components/SegmentedControl/types.js +1 -0
  43. package/dist/src/lib/Components/TabBar/TabBar.js +1 -0
  44. package/dist/src/lib/Components/TabBar/types.d.ts +0 -1
  45. package/dist/src/lib/Components/TabBar/types.d.ts.map +1 -1
  46. package/dist/src/lib/Components/index.d.ts +1 -0
  47. package/dist/src/lib/Components/index.d.ts.map +1 -1
  48. package/dist/src/lib/Components/index.js +1 -0
  49. package/dist/src/styles/theme/resolvers/resolveFontWeights.js +1 -1
  50. package/package.json +3 -3
  51. package/src/i18n/locales/de.json +3 -0
  52. package/src/i18n/locales/en.json +3 -0
  53. package/src/i18n/locales/es.json +3 -0
  54. package/src/i18n/locales/fr.json +3 -0
  55. package/src/i18n/locales/ja.json +3 -0
  56. package/src/i18n/locales/ko.json +3 -0
  57. package/src/i18n/locales/pt.json +3 -0
  58. package/src/i18n/locales/ru.json +3 -0
  59. package/src/i18n/locales/th.json +3 -0
  60. package/src/i18n/locales/tr.json +3 -0
  61. package/src/i18n/locales/zh.json +3 -0
  62. package/src/lib/Animations/constants.ts +31 -0
  63. package/src/lib/Animations/index.ts +1 -0
  64. package/src/lib/Components/AmountDisplay/AmountDisplay.mdx +7 -1
  65. package/src/lib/Components/AmountDisplay/AmountDisplay.stories.tsx +29 -2
  66. package/src/lib/Components/AmountDisplay/AmountDisplay.test.tsx +101 -51
  67. package/src/lib/Components/AmountDisplay/AmountDisplay.tsx +175 -24
  68. package/src/lib/Components/AmountDisplay/types.ts +22 -25
  69. package/src/lib/Components/SegmentedControl/SegmentedControl.mdx +159 -0
  70. package/src/lib/Components/SegmentedControl/SegmentedControl.stories.tsx +102 -0
  71. package/src/lib/Components/SegmentedControl/SegmentedControl.test.tsx +57 -0
  72. package/src/lib/Components/SegmentedControl/SegmentedControl.tsx +202 -0
  73. package/src/lib/Components/SegmentedControl/SegmentedControlContext.tsx +17 -0
  74. package/src/lib/Components/SegmentedControl/index.ts +2 -0
  75. package/src/lib/Components/SegmentedControl/types.ts +46 -0
  76. package/src/lib/Components/TabBar/TabBar.tsx +1 -0
  77. package/src/lib/Components/TabBar/types.ts +0 -1
  78. package/src/lib/Components/index.ts +1 -0
  79. package/src/styles/theme/createStylesheetTheme.test.ts +1 -1
  80. package/src/styles/theme/resolvers/resolveFontWeights.test.ts +9 -6
  81. package/src/styles/theme/resolvers/resolveFontWeights.ts +1 -1
  82. package/dist/src/lib/Components/Banner/Banner.figma.d.ts +0 -2
  83. package/dist/src/lib/Components/Banner/Banner.figma.d.ts.map +0 -1
  84. package/dist/src/lib/Components/Banner/Banner.figma.js +0 -45
  85. package/dist/src/lib/Components/Checkbox/Checkbox.figma.d.ts +0 -2
  86. package/dist/src/lib/Components/Checkbox/Checkbox.figma.d.ts.map +0 -1
  87. package/dist/src/lib/Components/Checkbox/Checkbox.figma.js +0 -32
  88. package/dist/src/lib/Components/InteractiveIcon/InteractiveIcon.figma.d.ts +0 -2
  89. package/dist/src/lib/Components/InteractiveIcon/InteractiveIcon.figma.d.ts.map +0 -1
  90. package/dist/src/lib/Components/InteractiveIcon/InteractiveIcon.figma.js +0 -26
  91. package/dist/src/lib/Components/Switch/Switch.figma.d.ts +0 -2
  92. package/dist/src/lib/Components/Switch/Switch.figma.d.ts.map +0 -1
  93. package/dist/src/lib/Components/Switch/Switch.figma.js +0 -32
  94. package/dist/src/lib/Components/Tile/Tile.figma.d.ts +0 -2
  95. package/dist/src/lib/Components/Tile/Tile.figma.d.ts.map +0 -1
  96. package/dist/src/lib/Components/Tile/Tile.figma.js +0 -28
  97. package/src/lib/Components/Banner/Banner.figma.tsx +0 -59
  98. package/src/lib/Components/Checkbox/Checkbox.figma.tsx +0 -49
  99. package/src/lib/Components/InteractiveIcon/InteractiveIcon.figma.tsx +0 -42
  100. package/src/lib/Components/Switch/Switch.figma.tsx +0 -47
  101. package/src/lib/Components/Tile/Tile.figma.tsx +0 -53
@@ -12,6 +12,8 @@ const TestWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => (
12
12
  </ThemeProvider>
13
13
  );
14
14
 
15
+ const hidden = { includeHiddenElements: true } as const;
16
+
15
17
  describe('AmountDisplay', () => {
16
18
  const createFormatter =
17
19
  (overrides: Partial<FormattedValue> = {}) =>
@@ -24,17 +26,65 @@ describe('AmountDisplay', () => {
24
26
  ...overrides,
25
27
  });
26
28
 
29
+ type TestNode = {
30
+ type: unknown;
31
+ props: {
32
+ accessibilityValue?: { text?: string };
33
+ [key: string]: unknown;
34
+ };
35
+ };
36
+
37
+ const getDigitStripValues = (): number[] => {
38
+ return screen.root
39
+ .findAll(
40
+ (node: TestNode) =>
41
+ typeof node.type !== 'function' &&
42
+ node.props.accessibilityValue?.text !== undefined,
43
+ )
44
+ .map((node: TestNode) => Number(node.props.accessibilityValue?.text));
45
+ };
46
+
27
47
  it('renders with basic formatter', () => {
28
48
  const formatter = createFormatter();
29
49
  render(
30
50
  <TestWrapper>
31
- <AmountDisplay value={1234.56} formatter={formatter} />
51
+ <AmountDisplay value={1234.56} formatter={formatter} hidden={false} />
52
+ </TestWrapper>,
53
+ );
54
+
55
+ expect(screen.getByText('USD', hidden)).toBeTruthy();
56
+ expect(screen.getByText('.', hidden)).toBeTruthy();
57
+ expect(getDigitStripValues()).toEqual([1, 2, 3, 4, 5, 6]);
58
+ });
59
+
60
+ it('handles zero value', () => {
61
+ const formatter = createFormatter({
62
+ integerPart: '0',
63
+ decimalPart: '00',
64
+ });
65
+ render(
66
+ <TestWrapper>
67
+ <AmountDisplay value={0} formatter={formatter} />
68
+ </TestWrapper>,
69
+ );
70
+
71
+ expect(getDigitStripValues()).toEqual([0, 0, 0]);
72
+ expect(screen.getByText('.', hidden)).toBeTruthy();
73
+ });
74
+
75
+ it('handles large numbers', () => {
76
+ const formatter = createFormatter({
77
+ integerPart: '1234567',
78
+ decimalPart: '89',
79
+ });
80
+ render(
81
+ <TestWrapper>
82
+ <AmountDisplay value={1234567.89} formatter={formatter} />
32
83
  </TestWrapper>,
33
84
  );
34
85
 
35
- expect(screen.getByText('USD')).toBeTruthy();
36
- expect(screen.getByText('1234')).toBeTruthy();
37
- expect(screen.getByText('.56')).toBeTruthy();
86
+ expect(getDigitStripValues()).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9]);
87
+ expect(screen.getByText('.', hidden)).toBeTruthy();
38
88
  });
39
89
 
40
90
  it('renders currency at start position', () => {
@@ -45,8 +95,7 @@ describe('AmountDisplay', () => {
45
95
  </TestWrapper>,
46
96
  );
47
97
 
48
- expect(screen.getByText('USD')).toBeTruthy();
49
- expect(screen.getByText('1234')).toBeTruthy();
98
+ expect(screen.getByText('USD', hidden)).toBeTruthy();
50
99
  });
51
100
 
52
101
  it('renders currency at end position', () => {
@@ -57,8 +106,7 @@ describe('AmountDisplay', () => {
57
106
  </TestWrapper>,
58
107
  );
59
108
 
60
- expect(screen.getByText('USD')).toBeTruthy();
61
- expect(screen.getByText('.56')).toBeTruthy();
109
+ expect(screen.getByText('USD', hidden)).toBeTruthy();
62
110
  });
63
111
 
64
112
  it('renders without decimal part', () => {
@@ -69,8 +117,7 @@ describe('AmountDisplay', () => {
69
117
  </TestWrapper>,
70
118
  );
71
119
 
72
- expect(screen.getByText('1234')).toBeTruthy();
73
- expect(screen.queryByText(/\./)).toBeNull();
120
+ expect(screen.queryByText('.', hidden)).toBeNull();
74
121
  });
75
122
 
76
123
  it('uses comma as decimal separator', () => {
@@ -81,18 +128,18 @@ describe('AmountDisplay', () => {
81
128
  </TestWrapper>,
82
129
  );
83
130
 
84
- expect(screen.getByText(',56')).toBeTruthy();
131
+ expect(screen.getByText(',', hidden)).toBeTruthy();
85
132
  });
86
133
 
87
- it('uses default dot separator when not specified', () => {
88
- const formatter = createFormatter({ decimalSeparator: undefined });
134
+ it('uses default dot separator', () => {
135
+ const formatter = createFormatter();
89
136
  render(
90
137
  <TestWrapper>
91
138
  <AmountDisplay value={1234.56} formatter={formatter} />
92
139
  </TestWrapper>,
93
140
  );
94
141
 
95
- expect(screen.getByText('.56')).toBeTruthy();
142
+ expect(screen.getByText('.', hidden)).toBeTruthy();
96
143
  });
97
144
 
98
145
  it('forwards additional props', () => {
@@ -110,85 +157,88 @@ describe('AmountDisplay', () => {
110
157
  expect(screen.getByTestId('amount-display')).toBeTruthy();
111
158
  });
112
159
 
113
- it('handles zero value', () => {
114
- const formatter = createFormatter({
115
- integerPart: '0',
116
- decimalPart: '00',
117
- });
160
+ it('displays bullet points when hidden is true', () => {
161
+ const formatter = createFormatter();
118
162
  render(
119
163
  <TestWrapper>
120
- <AmountDisplay value={0} formatter={formatter} />
164
+ <AmountDisplay value={1234.56} formatter={formatter} hidden={true} />
121
165
  </TestWrapper>,
122
166
  );
123
167
 
124
- expect(screen.getByText('0')).toBeTruthy();
125
- expect(screen.getByText('.00')).toBeTruthy();
168
+ expect(screen.getByText('••••', hidden)).toBeTruthy();
169
+ expect(screen.getByText('USD', hidden)).toBeTruthy();
170
+ expect(screen.queryByText('.', hidden)).toBeNull();
126
171
  });
127
172
 
128
- it('handles large numbers', () => {
129
- const formatter = createFormatter({
130
- integerPart: '1,234,567',
131
- decimalPart: '89',
132
- });
173
+ it('renders correctly when loading is true', () => {
174
+ const formatter = createFormatter();
133
175
  render(
134
176
  <TestWrapper>
135
- <AmountDisplay value={1234567.89} formatter={formatter} />
177
+ <AmountDisplay value={1234.56} formatter={formatter} loading={true} />
136
178
  </TestWrapper>,
137
179
  );
138
180
 
139
- expect(screen.getByText('1,234,567')).toBeTruthy();
140
- expect(screen.getByText('.89')).toBeTruthy();
181
+ expect(screen.getByText('USD', hidden)).toBeTruthy();
182
+ expect(screen.getByText('.', hidden)).toBeTruthy();
141
183
  });
142
184
 
143
- it('displays bullet points when hidden is true', () => {
185
+ it('renders correctly when loading is false', () => {
144
186
  const formatter = createFormatter();
145
187
  render(
146
188
  <TestWrapper>
147
- <AmountDisplay value={1234.56} formatter={formatter} hidden={true} />
189
+ <AmountDisplay value={1234.56} formatter={formatter} loading={false} />
148
190
  </TestWrapper>,
149
191
  );
150
192
 
151
- expect(screen.getByText('••••')).toBeTruthy();
152
- expect(screen.getByText('USD')).toBeTruthy();
153
- expect(screen.queryByText('1234')).toBeNull();
154
- expect(screen.queryByText('.56')).toBeNull();
193
+ expect(screen.getByText('USD', hidden)).toBeTruthy();
194
+ expect(screen.getByText('.', hidden)).toBeTruthy();
155
195
  });
156
196
 
157
- it('displays amount normally when hidden is false', () => {
158
- const formatter = createFormatter();
197
+ it('sets accessibilityLabel with currency at start', () => {
198
+ const formatter = createFormatter({ currencyPosition: 'start' });
159
199
  render(
160
200
  <TestWrapper>
161
- <AmountDisplay value={1234.56} formatter={formatter} hidden={false} />
201
+ <AmountDisplay value={1234.56} formatter={formatter} />
162
202
  </TestWrapper>,
163
203
  );
164
204
 
165
- expect(screen.getByText('1234')).toBeTruthy();
166
- expect(screen.getByText('.56')).toBeTruthy();
167
- expect(screen.queryByText('••••')).toBeNull();
205
+ expect(screen.getByLabelText('USD 1234.56')).toBeTruthy();
168
206
  });
169
207
 
170
- it('hides decimal part and shows only bullets when hidden', () => {
208
+ it('sets accessibilityLabel with currency at end', () => {
171
209
  const formatter = createFormatter({ currencyPosition: 'end' });
172
210
  render(
173
211
  <TestWrapper>
174
- <AmountDisplay value={1234.56} formatter={formatter} hidden={true} />
212
+ <AmountDisplay value={1234.56} formatter={formatter} />
175
213
  </TestWrapper>,
176
214
  );
177
215
 
178
- expect(screen.getByText('••••')).toBeTruthy();
179
- expect(screen.queryByText('.56')).toBeNull();
216
+ expect(screen.getByLabelText('1234.56 USD')).toBeTruthy();
180
217
  });
181
218
 
182
- it('renders correctly when loading is true', () => {
219
+ it('sets accessibilityLabel to "Amount hidden" when hidden', () => {
183
220
  const formatter = createFormatter();
184
221
  render(
185
222
  <TestWrapper>
186
- <AmountDisplay value={1234.56} formatter={formatter} loading={true} />
223
+ <AmountDisplay value={1234.56} formatter={formatter} hidden={true} />
224
+ </TestWrapper>,
225
+ );
226
+
227
+ expect(screen.getByLabelText('Amount hidden')).toBeTruthy();
228
+ });
229
+
230
+ it('renders group separators', () => {
231
+ const formatter = createFormatter({
232
+ integerPart: '1,234,567',
233
+ decimalPart: '89',
234
+ decimalSeparator: '.',
235
+ });
236
+ render(
237
+ <TestWrapper>
238
+ <AmountDisplay value={1234567.89} formatter={formatter} />
187
239
  </TestWrapper>,
188
240
  );
189
241
 
190
- expect(screen.getByText('USD')).toBeTruthy();
191
- expect(screen.getByText('1234')).toBeTruthy();
192
- expect(screen.getByText('.56')).toBeTruthy();
242
+ expect(screen.getAllByText(',', hidden)).toHaveLength(2);
193
243
  });
194
244
  });
@@ -1,15 +1,67 @@
1
- import { View, Text } from 'react-native';
1
+ import { useSplitText, buildAriaLabel } from '@ledgerhq/lumen-utils-shared';
2
+ import { memo, useEffect } from 'react';
3
+ import { Text, View } from 'react-native';
4
+ import Animated, {
5
+ Easing,
6
+ useAnimatedStyle,
7
+ useSharedValue,
8
+ withTiming,
9
+ } from 'react-native-reanimated';
10
+ import { useCommonTranslation } from '../../../i18n';
2
11
  import { useStyleSheet } from '../../../styles';
3
12
  import { Pulse } from '../../Animations/Pulse';
4
13
  import { Box } from '../Utility';
5
- import { AmountDisplayProps } from './types';
14
+ import {
15
+ AmountDisplayProps,
16
+ DigitStripListProps,
17
+ DigitStripProps,
18
+ DIGITS,
19
+ } from './types';
20
+
21
+ const INTEGER_DIGIT_WIDTHS = {
22
+ 0: 24.5,
23
+ 1: 15,
24
+ 2: 23,
25
+ 3: 24,
26
+ 4: 25,
27
+ 5: 23,
28
+ 6: 24.5,
29
+ 7: 21.5,
30
+ 8: 24,
31
+ 9: 24,
32
+ };
33
+
34
+ const DECIMAL_DIGIT_WIDTHS = {
35
+ 0: 17,
36
+ 1: 10.5,
37
+ 2: 16,
38
+ 3: 16.5,
39
+ 4: 17.2,
40
+ 5: 15.7,
41
+ 6: 17,
42
+ 7: 14.7,
43
+ 8: 16.5,
44
+ 9: 16.5,
45
+ };
46
+
47
+ const TIMING_CONFIG = {
48
+ duration: 600,
49
+ easing: Easing.inOut(Easing.ease),
50
+ };
6
51
 
7
52
  const useStyles = () => {
8
53
  return useStyleSheet(
9
54
  (t) => ({
10
55
  container: {
11
56
  flexDirection: 'row',
12
- alignItems: 'baseline',
57
+ alignItems: 'flex-end',
58
+ },
59
+ integerPartContainer: {
60
+ flexDirection: 'row',
61
+ },
62
+ decimalPartContainer: {
63
+ flexDirection: 'row',
64
+ paddingBottom: t.spacings.s2,
13
65
  },
14
66
  integerText: {
15
67
  ...t.typographies.heading1SemiBold,
@@ -38,6 +90,73 @@ const useStyles = () => {
38
90
  );
39
91
  };
40
92
 
93
+ const DigitStrip = memo(
94
+ ({ value, textStyle, animate, type }: DigitStripProps) => {
95
+ const targetWidth = (
96
+ type === 'integer' ? INTEGER_DIGIT_WIDTHS : DECIMAL_DIGIT_WIDTHS
97
+ )[value];
98
+ const lineHeight = textStyle.lineHeight;
99
+ const translateY = useSharedValue(-value * lineHeight);
100
+ const width = useSharedValue<number>(targetWidth);
101
+
102
+ useEffect(() => {
103
+ if (animate) {
104
+ translateY.value = withTiming(-value * lineHeight, TIMING_CONFIG);
105
+ width.value = withTiming(targetWidth, TIMING_CONFIG);
106
+ } else {
107
+ translateY.value = -value * lineHeight;
108
+ width.value = targetWidth;
109
+ }
110
+ }, [value, lineHeight, translateY, animate, width, targetWidth]);
111
+
112
+ const animatedStyle = useAnimatedStyle(
113
+ () => ({
114
+ transform: [{ translateY: translateY.value }],
115
+ }),
116
+ [translateY],
117
+ );
118
+
119
+ return (
120
+ <Animated.View
121
+ style={{ height: lineHeight, overflow: 'hidden', width: width }}
122
+ accessibilityValue={{ text: String(value) }}
123
+ >
124
+ <Animated.View style={[animatedStyle, { alignItems: 'center' }]}>
125
+ {DIGITS.map((d) => (
126
+ <Text key={d} style={textStyle}>
127
+ {d}
128
+ </Text>
129
+ ))}
130
+ </Animated.View>
131
+ </Animated.View>
132
+ );
133
+ },
134
+ );
135
+
136
+ const DigitStripList = memo(
137
+ ({ items, textStyle, animate, type }: DigitStripListProps) => {
138
+ return items.map((item, index) => {
139
+ const key = items.length - index;
140
+ if (item.type === 'separator') {
141
+ return (
142
+ <Text key={key} style={textStyle}>
143
+ {item.value}
144
+ </Text>
145
+ );
146
+ }
147
+ return (
148
+ <DigitStrip
149
+ key={key}
150
+ value={Number(item.value) as DigitStripProps['value']}
151
+ animate={animate}
152
+ textStyle={textStyle}
153
+ type={type}
154
+ />
155
+ );
156
+ });
157
+ },
158
+ );
159
+
41
160
  /**
42
161
  * AmountDisplay - Renders formatted monetary amounts with flexible currency positioning and decimal formatting.
43
162
  *
@@ -76,34 +195,66 @@ export const AmountDisplay = ({
76
195
  formatter,
77
196
  hidden = false,
78
197
  loading = false,
198
+ animate = true,
79
199
  ...props
80
200
  }: AmountDisplayProps) => {
81
201
  const styles = useStyles();
202
+ const { t } = useCommonTranslation();
82
203
  const parts = formatter(value);
204
+ const splitDigits = useSplitText(parts);
205
+ const ariaLabel = buildAriaLabel(
206
+ parts,
207
+ hidden,
208
+ t('components.amountDisplay.amountHiddenAriaLabel'),
209
+ );
83
210
 
84
211
  return (
85
- <Box {...props}>
212
+ <Box
213
+ accessibilityLabel={ariaLabel}
214
+ accessibilityState={{ busy: loading }}
215
+ {...props}
216
+ >
86
217
  <Pulse animate={loading}>
87
- <View style={styles.container}>
88
- {(parts.currencyPosition === undefined ||
89
- parts.currencyPosition === 'start') && (
90
- <Text style={[styles.currencyStartText, styles.spacingStart]}>
91
- {parts.currencyText}
92
- </Text>
93
- )}
94
- <Text style={styles.integerText}>
95
- {hidden ? '••••' : parts.integerPart}
96
- </Text>
97
- {parts.decimalPart && !hidden && (
98
- <Text style={styles.decimalText}>
99
- {(parts.decimalSeparator || '.') + parts.decimalPart}
100
- </Text>
101
- )}
102
- {parts.currencyPosition === 'end' && (
103
- <Text style={[styles.currencyEndText, styles.spacingEnd]}>
104
- {parts.currencyText}
105
- </Text>
106
- )}
218
+ <View
219
+ style={styles.container}
220
+ accessibilityElementsHidden
221
+ importantForAccessibility='no-hide-descendants'
222
+ >
223
+ <View style={styles.integerPartContainer}>
224
+ {parts.currencyPosition === 'start' && (
225
+ <Text style={[styles.currencyStartText, styles.spacingStart]}>
226
+ {parts.currencyText}
227
+ </Text>
228
+ )}
229
+ {hidden ? (
230
+ <Text style={styles.integerText}>••••</Text>
231
+ ) : (
232
+ <DigitStripList
233
+ items={splitDigits.integerPart}
234
+ textStyle={styles.integerText}
235
+ animate={animate}
236
+ type='integer'
237
+ />
238
+ )}
239
+ </View>
240
+ <View style={styles.decimalPartContainer}>
241
+ {!hidden && parts.decimalPart && (
242
+ <Text style={styles.decimalText}>{parts.decimalSeparator}</Text>
243
+ )}
244
+ {parts.decimalPart && !hidden && (
245
+ <DigitStripList
246
+ items={splitDigits.decimalPart}
247
+ textStyle={styles.decimalText}
248
+ animate={animate}
249
+ type='decimal'
250
+ />
251
+ )}
252
+ {parts.currencyPosition === 'end' && (
253
+ <Text style={[styles.currencyEndText, styles.spacingEnd]}>
254
+ {parts.currencyText}
255
+ </Text>
256
+ )}
257
+ </View>
107
258
  </View>
108
259
  </Pulse>
109
260
  </Box>
@@ -1,32 +1,24 @@
1
- import { ViewProps } from 'react-native';
1
+ import type { FormattedValue, SplitChar } from '@ledgerhq/lumen-utils-shared';
2
+ import { ViewProps, TextStyle } from 'react-native';
2
3
  import { StyledViewProps } from '../../../styles';
3
4
 
4
- export type FormattedValue = {
5
- /**
6
- * The whole number portion of the amount (e.g., "1234" from 1234.56)
7
- */
8
- integerPart: string;
9
- /**
10
- * The fractional portion of the amount without the separator (e.g., "56" from 1234.56)
11
- * @optional
12
- */
13
- decimalPart?: string;
14
- /**
15
- * The currency text or symbol (e.g., "$", "USD", "€", "BTC")
16
- */
17
- currencyText: string;
18
- /**
19
- * The character which separates integer and fractional parts.
20
- */
21
- decimalSeparator: '.' | ',';
22
- /**
23
- * Position of the currency text relative to the amount.
24
- * @optional
25
- * @default 'start'
26
- */
27
- currencyPosition?: 'start' | 'end';
5
+ export type { FormattedValue };
6
+
7
+ export const DIGITS = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] as const;
8
+
9
+ type IntegerDigit = (typeof DIGITS)[number];
10
+
11
+ export type DigitStripProps = {
12
+ value: IntegerDigit;
13
+ animate: boolean;
14
+ textStyle: TextStyle & { lineHeight: number };
15
+ type: 'integer' | 'decimal';
28
16
  };
29
17
 
18
+ export type DigitStripListProps = {
19
+ items: SplitChar[];
20
+ } & Omit<DigitStripProps, 'value'>;
21
+
30
22
  /**
31
23
  * Props for the AmountDisplay component.
32
24
  */
@@ -51,4 +43,9 @@ export type AmountDisplayProps = ViewProps & {
51
43
  * @default false
52
44
  */
53
45
  loading?: boolean;
46
+ /**
47
+ * Whether the odometer animation should play on value change or not
48
+ * @default true
49
+ */
50
+ animate?: boolean;
54
51
  } & Omit<StyledViewProps, 'children'>;