@ledgerhq/lumen-ui-rnative 0.1.40 → 0.1.42

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 (44) hide show
  1. package/dist/module/lib/Components/AmountInput/AmountInput.js +49 -159
  2. package/dist/module/lib/Components/AmountInput/AmountInput.js.map +1 -1
  3. package/dist/module/lib/Components/AmountInput/useAmountInputAnimations/useAmountInputAnimations.js +149 -0
  4. package/dist/module/lib/Components/AmountInput/useAmountInputAnimations/useAmountInputAnimations.js.map +1 -0
  5. package/dist/module/lib/Components/AmountInput/useAmountInputFormatting/useAmountInputFormatting.js +22 -0
  6. package/dist/module/lib/Components/AmountInput/useAmountInputFormatting/useAmountInputFormatting.js.map +1 -0
  7. package/dist/module/lib/Components/AmountInput/useAmountInputFormatting/useAmountInputFormatting.test.js +59 -0
  8. package/dist/module/lib/Components/AmountInput/useAmountInputFormatting/useAmountInputFormatting.test.js.map +1 -0
  9. package/dist/module/lib/Components/Banner/Banner.js +5 -9
  10. package/dist/module/lib/Components/Banner/Banner.js.map +1 -1
  11. package/dist/module/lib/Components/BottomSheet/BottomSheetHeader.js +0 -3
  12. package/dist/module/lib/Components/BottomSheet/BottomSheetHeader.js.map +1 -1
  13. package/dist/module/lib/Components/Icon/Icon.test.js +1 -1
  14. package/dist/module/lib/Components/SegmentedControl/SegmentedControl.test.js +162 -66
  15. package/dist/module/lib/Components/SegmentedControl/SegmentedControl.test.js.map +1 -1
  16. package/dist/module/lib/Components/SegmentedControl/usePillLayout.js +19 -15
  17. package/dist/module/lib/Components/SegmentedControl/usePillLayout.js.map +1 -1
  18. package/dist/module/lib/Components/Trend/Trend.js +19 -16
  19. package/dist/module/lib/Components/Trend/Trend.js.map +1 -1
  20. package/dist/typescript/src/lib/Components/AmountInput/AmountInput.d.ts +1 -1
  21. package/dist/typescript/src/lib/Components/AmountInput/AmountInput.d.ts.map +1 -1
  22. package/dist/typescript/src/lib/Components/AmountInput/useAmountInputAnimations/useAmountInputAnimations.d.ts +16 -0
  23. package/dist/typescript/src/lib/Components/AmountInput/useAmountInputAnimations/useAmountInputAnimations.d.ts.map +1 -0
  24. package/dist/typescript/src/lib/Components/AmountInput/useAmountInputFormatting/useAmountInputFormatting.d.ts +18 -0
  25. package/dist/typescript/src/lib/Components/AmountInput/useAmountInputFormatting/useAmountInputFormatting.d.ts.map +1 -0
  26. package/dist/typescript/src/lib/Components/Banner/Banner.d.ts.map +1 -1
  27. package/dist/typescript/src/lib/Components/Banner/types.d.ts +3 -3
  28. package/dist/typescript/src/lib/Components/Banner/types.d.ts.map +1 -1
  29. package/dist/typescript/src/lib/Components/BottomSheet/BottomSheetHeader.d.ts +1 -1
  30. package/dist/typescript/src/lib/Components/BottomSheet/BottomSheetHeader.d.ts.map +1 -1
  31. package/dist/typescript/src/lib/Components/SegmentedControl/usePillLayout.d.ts.map +1 -1
  32. package/dist/typescript/src/lib/Components/Trend/Trend.d.ts.map +1 -1
  33. package/package.json +2 -2
  34. package/src/lib/Components/AmountInput/AmountInput.tsx +55 -115
  35. package/src/lib/Components/AmountInput/useAmountInputAnimations/useAmountInputAnimations.ts +100 -0
  36. package/src/lib/Components/AmountInput/useAmountInputFormatting/useAmountInputFormatting.test.ts +62 -0
  37. package/src/lib/Components/AmountInput/useAmountInputFormatting/useAmountInputFormatting.ts +48 -0
  38. package/src/lib/Components/Banner/Banner.tsx +8 -11
  39. package/src/lib/Components/Banner/types.ts +3 -3
  40. package/src/lib/Components/BottomSheet/BottomSheetHeader.tsx +0 -4
  41. package/src/lib/Components/Icon/Icon.test.tsx +1 -1
  42. package/src/lib/Components/SegmentedControl/SegmentedControl.test.tsx +152 -60
  43. package/src/lib/Components/SegmentedControl/usePillLayout.ts +21 -12
  44. package/src/lib/Components/Trend/Trend.tsx +24 -22
@@ -12,72 +12,164 @@ const TestWrapper = ({ children }: { children: React.ReactNode }) => (
12
12
  );
13
13
 
14
14
  describe('SegmentedControl', () => {
15
- it('renders segments with labels', () => {
16
- const { getByText } = render(
17
- <TestWrapper>
18
- <SegmentedControl
19
- selectedValue='send'
20
- onSelectedChange={() => {
21
- /* empty */
22
- }}
23
- accessibilityLabel='Transaction type'
24
- >
25
- <SegmentedControlButton value='send'>Send</SegmentedControlButton>
26
- <SegmentedControlButton value='receive'>
27
- Receive
28
- </SegmentedControlButton>
29
- </SegmentedControl>
30
- </TestWrapper>,
31
- );
32
- expect(getByText('Send')).toBeTruthy();
33
- expect(getByText('Receive')).toBeTruthy();
15
+ describe('Rendering', () => {
16
+ it('renders segments with labels', () => {
17
+ const { getByText } = render(
18
+ <TestWrapper>
19
+ <SegmentedControl
20
+ selectedValue='send'
21
+ onSelectedChange={() => {
22
+ /* empty */
23
+ }}
24
+ accessibilityLabel='Transaction type'
25
+ >
26
+ <SegmentedControlButton value='send'>Send</SegmentedControlButton>
27
+ <SegmentedControlButton value='receive'>
28
+ Receive
29
+ </SegmentedControlButton>
30
+ </SegmentedControl>
31
+ </TestWrapper>,
32
+ );
33
+ expect(getByText('Send')).toBeTruthy();
34
+ expect(getByText('Receive')).toBeTruthy();
35
+ });
36
+
37
+ it('renders trailingContent inside segment buttons', () => {
38
+ const { getByLabelText } = render(
39
+ <TestWrapper>
40
+ <SegmentedControl
41
+ selectedValue='tokens'
42
+ onSelectedChange={() => {
43
+ /* empty */
44
+ }}
45
+ accessibilityLabel='Asset section'
46
+ >
47
+ <SegmentedControlButton
48
+ value='tokens'
49
+ trailingContent={
50
+ <DotCount value={3} accessibilityLabel='3 tokens' />
51
+ }
52
+ >
53
+ Tokens
54
+ </SegmentedControlButton>
55
+ <SegmentedControlButton value='nfts'>NFTs</SegmentedControlButton>
56
+ </SegmentedControl>
57
+ </TestWrapper>,
58
+ );
59
+
60
+ expect(getByLabelText('3 tokens')).toBeTruthy();
61
+ });
34
62
  });
35
63
 
36
- it('calls onSelectedChange with segment value when a segment is pressed', () => {
37
- const onSelectedChange = jest.fn();
38
- const { getByText } = render(
39
- <TestWrapper>
40
- <SegmentedControl
41
- selectedValue='send'
42
- onSelectedChange={onSelectedChange}
43
- accessibilityLabel='Transaction type'
44
- >
45
- <SegmentedControlButton value='send'>Send</SegmentedControlButton>
46
- <SegmentedControlButton value='receive'>
47
- Receive
48
- </SegmentedControlButton>
49
- </SegmentedControl>
50
- </TestWrapper>,
51
- );
64
+ describe('States', () => {
65
+ it('marks the selected segment with accessibilityState', () => {
66
+ const { getByTestId } = render(
67
+ <TestWrapper>
68
+ <SegmentedControl
69
+ selectedValue='receive'
70
+ onSelectedChange={() => {}}
71
+ accessibilityLabel='Transaction type'
72
+ >
73
+ <SegmentedControlButton value='send' testID='seg-send'>
74
+ Send
75
+ </SegmentedControlButton>
76
+ <SegmentedControlButton value='receive' testID='seg-receive'>
77
+ Receive
78
+ </SegmentedControlButton>
79
+ </SegmentedControl>
80
+ </TestWrapper>,
81
+ );
52
82
 
53
- fireEvent.press(getByText('Receive'));
83
+ expect(getByTestId('seg-send').props.accessibilityState).toMatchObject({
84
+ selected: false,
85
+ });
86
+ expect(getByTestId('seg-receive').props.accessibilityState).toMatchObject(
87
+ { selected: true },
88
+ );
89
+ });
54
90
 
55
- expect(onSelectedChange).toHaveBeenCalledWith('receive');
56
- });
91
+ it('marks a pre-selected non-first segment as selected on initial render (fixed layout)', () => {
92
+ const { getByTestId } = render(
93
+ <TestWrapper>
94
+ <SegmentedControl
95
+ selectedValue='blame'
96
+ onSelectedChange={() => {}}
97
+ tabLayout='fixed'
98
+ accessibilityLabel='File view'
99
+ >
100
+ <SegmentedControlButton value='preview' testID='seg-preview'>
101
+ Preview
102
+ </SegmentedControlButton>
103
+ <SegmentedControlButton value='raw' testID='seg-raw'>
104
+ Raw
105
+ </SegmentedControlButton>
106
+ <SegmentedControlButton value='blame' testID='seg-blame'>
107
+ Blame
108
+ </SegmentedControlButton>
109
+ </SegmentedControl>
110
+ </TestWrapper>,
111
+ );
112
+
113
+ expect(getByTestId('seg-preview').props.accessibilityState).toMatchObject(
114
+ { selected: false },
115
+ );
116
+ expect(getByTestId('seg-blame').props.accessibilityState).toMatchObject({
117
+ selected: true,
118
+ });
119
+ });
120
+
121
+ it('marks a pre-selected non-first segment as selected on initial render (fit layout)', () => {
122
+ const { getByTestId } = render(
123
+ <TestWrapper>
124
+ <SegmentedControl
125
+ selectedValue='blame'
126
+ onSelectedChange={() => {}}
127
+ tabLayout='fit'
128
+ accessibilityLabel='File view'
129
+ >
130
+ <SegmentedControlButton value='preview' testID='seg-preview'>
131
+ Preview
132
+ </SegmentedControlButton>
133
+ <SegmentedControlButton value='raw' testID='seg-raw'>
134
+ Raw
135
+ </SegmentedControlButton>
136
+ <SegmentedControlButton value='blame' testID='seg-blame'>
137
+ Blame
138
+ </SegmentedControlButton>
139
+ </SegmentedControl>
140
+ </TestWrapper>,
141
+ );
142
+
143
+ expect(getByTestId('seg-preview').props.accessibilityState).toMatchObject(
144
+ { selected: false },
145
+ );
146
+ expect(getByTestId('seg-blame').props.accessibilityState).toMatchObject({
147
+ selected: true,
148
+ });
149
+ });
57
150
 
58
- it('renders trailingContent inside segment buttons', () => {
59
- const { getByLabelText } = render(
60
- <TestWrapper>
61
- <SegmentedControl
62
- selectedValue='tokens'
63
- onSelectedChange={() => {
64
- /* empty */
65
- }}
66
- accessibilityLabel='Asset section'
67
- >
68
- <SegmentedControlButton
69
- value='tokens'
70
- trailingContent={
71
- <DotCount value={3} accessibilityLabel='3 tokens' />
72
- }
151
+ it('can change selection away from a pre-selected non-first segment (fit layout)', () => {
152
+ const onSelectedChange = jest.fn();
153
+ const { getByText } = render(
154
+ <TestWrapper>
155
+ <SegmentedControl
156
+ selectedValue='blame'
157
+ onSelectedChange={onSelectedChange}
158
+ tabLayout='fit'
159
+ accessibilityLabel='File view'
73
160
  >
74
- Tokens
75
- </SegmentedControlButton>
76
- <SegmentedControlButton value='nfts'>NFTs</SegmentedControlButton>
77
- </SegmentedControl>
78
- </TestWrapper>,
79
- );
161
+ <SegmentedControlButton value='preview'>
162
+ Preview
163
+ </SegmentedControlButton>
164
+ <SegmentedControlButton value='raw'>Raw</SegmentedControlButton>
165
+ <SegmentedControlButton value='blame'>Blame</SegmentedControlButton>
166
+ </SegmentedControl>
167
+ </TestWrapper>,
168
+ );
169
+
170
+ fireEvent.press(getByText('Preview'));
80
171
 
81
- expect(getByLabelText('3 tokens')).toBeTruthy();
172
+ expect(onSelectedChange).toHaveBeenCalledWith('preview');
173
+ });
82
174
  });
83
175
  });
@@ -6,6 +6,7 @@ import React, {
6
6
  useEffect,
7
7
  useMemo,
8
8
  useRef,
9
+ useState,
9
10
  } from 'react';
10
11
  import type { LayoutChangeEvent } from 'react-native';
11
12
  import {
@@ -49,7 +50,9 @@ export function usePillLayout({
49
50
  const pillWidth = useSharedValue(0);
50
51
  const pillHeight = useSharedValue(0);
51
52
  const hasLayoutRef = useRef(false);
53
+ const animatePillRef = useRef(false);
52
54
  const buttonLayoutsRef = useRef(new Map<string, ButtonLayout>());
55
+ const [layoutReady, setLayoutReady] = useState(false);
53
56
 
54
57
  const timingConfig = useTimingConfig({
55
58
  duration: 300,
@@ -67,9 +70,7 @@ export function usePillLayout({
67
70
 
68
71
  if (!hasLayoutRef.current) {
69
72
  hasLayoutRef.current = true;
70
- if (selectedIndex >= 0) {
71
- pillTranslateX.value = selectedIndex * slotWidth;
72
- }
73
+ setLayoutReady(true);
73
74
  }
74
75
  }
75
76
  };
@@ -78,31 +79,38 @@ export function usePillLayout({
78
79
  (value: string, layout: ButtonLayout): void => {
79
80
  buttonLayoutsRef.current.set(value, layout);
80
81
 
81
- if (tabLayout === 'fit' && !hasLayoutRef.current) {
82
+ if (
83
+ tabLayout === 'fit' &&
84
+ !hasLayoutRef.current &&
85
+ value === selectedValue
86
+ ) {
82
87
  hasLayoutRef.current = true;
83
- if (value === selectedValue) {
84
- pillTranslateX.value = layout.x;
85
- pillWidth.value = layout.width;
86
- }
88
+ setLayoutReady(true);
87
89
  }
88
90
  },
89
- [tabLayout, selectedValue, pillTranslateX, pillWidth],
91
+ [tabLayout, selectedValue],
90
92
  );
91
93
 
92
94
  useEffect(() => {
93
95
  if (!hasLayoutRef.current) return;
94
96
 
97
+ const skipAnimation = !animatePillRef.current;
98
+ if (skipAnimation) {
99
+ animatePillRef.current = true;
100
+ }
101
+ const config = skipAnimation ? { duration: 0 } : timingConfig;
102
+
95
103
  if (tabLayout === 'fit') {
96
104
  const layout = buttonLayoutsRef.current.get(selectedValue);
97
105
  if (layout) {
98
- pillTranslateX.value = withTiming(layout.x, timingConfig);
99
- pillWidth.value = withTiming(layout.width, timingConfig);
106
+ pillTranslateX.value = withTiming(layout.x, config);
107
+ pillWidth.value = withTiming(layout.width, config);
100
108
  }
101
109
  } else {
102
110
  if (selectedIndex >= 0 && pillWidth.value > 0) {
103
111
  pillTranslateX.value = withTiming(
104
112
  selectedIndex * pillWidth.value,
105
- timingConfig,
113
+ config,
106
114
  );
107
115
  }
108
116
  }
@@ -113,6 +121,7 @@ export function usePillLayout({
113
121
  pillWidth,
114
122
  pillTranslateX,
115
123
  timingConfig,
124
+ layoutReady,
116
125
  ]);
117
126
 
118
127
  const animatedPillStyle = useAnimatedStyle(
@@ -3,7 +3,7 @@ import { StyleSheet } from 'react-native';
3
3
  import { useCommonTranslation } from '../../../i18n';
4
4
  import type { LumenTextStyle } from '../../../styles';
5
5
  import { useStyleSheet } from '../../../styles';
6
- import { Minus, TriangleDown, TriangleUp } from '../../Symbols';
6
+ import { TriangleDown, TriangleUp } from '../../Symbols';
7
7
  import type { IconSize } from '../Icon';
8
8
  import { Box, Text } from '../Utility';
9
9
  import type { TrendProps } from './types';
@@ -17,6 +17,23 @@ function getVariant(value: number): TrendVariant {
17
17
  return value > 0 ? 'positive' : 'negative';
18
18
  }
19
19
 
20
+ const iconMap = {
21
+ positive: TriangleUp,
22
+ negative: TriangleDown,
23
+ neutral: null,
24
+ };
25
+
26
+ const iconSizeMap: Record<NonNullable<TrendProps['size']>, IconSize> = {
27
+ md: 16,
28
+ sm: 12,
29
+ };
30
+
31
+ const iconColorMap: Record<TrendVariant, LumenTextStyle['color']> = {
32
+ positive: 'success',
33
+ negative: 'error',
34
+ neutral: 'muted',
35
+ };
36
+
20
37
  export function Trend({
21
38
  value,
22
39
  size = 'md',
@@ -35,26 +52,9 @@ export function Trend({
35
52
 
36
53
  const styles = useStyles({ size, variant, disabled });
37
54
 
38
- const Icon = {
39
- positive: TriangleUp,
40
- negative: TriangleDown,
41
- neutral: Minus,
42
- }[variant];
43
-
44
- const iconSize = (
45
- {
46
- md: 16,
47
- sm: 12,
48
- } as const
49
- )[size] as IconSize;
50
-
51
- const iconColor = (
52
- {
53
- positive: 'success',
54
- negative: 'error',
55
- neutral: 'muted',
56
- } as const
57
- )[variant] as LumenTextStyle['color'];
55
+ const Icon = iconMap[variant];
56
+ const iconSize = iconSizeMap[size];
57
+ const iconColor = iconColorMap[variant];
58
58
 
59
59
  const absoluteFormattedValue = `${Math.abs(value).toFixed(2)}%`;
60
60
  const formattedValue =
@@ -71,7 +71,9 @@ export function Trend({
71
71
  style={[styles.container, style]}
72
72
  {...props}
73
73
  >
74
- <Icon size={iconSize} color={disabled ? 'disabled' : iconColor} />
74
+ {Icon && (
75
+ <Icon size={iconSize} color={disabled ? 'disabled' : iconColor} />
76
+ )}
75
77
  <Text style={styles.text}>{formattedValue}</Text>
76
78
  </Box>
77
79
  );