@ledgerhq/lumen-ui-rnative 0.1.39 → 0.1.40
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/AmountDisplay/AmountDisplay.js +28 -11
- package/dist/module/lib/Components/AmountDisplay/AmountDisplay.js.map +1 -1
- package/dist/module/lib/Components/AmountDisplay/AmountDisplay.test.js +71 -0
- package/dist/module/lib/Components/AmountDisplay/AmountDisplay.test.js.map +1 -1
- package/dist/module/lib/Components/AmountInput/AmountInput.js +9 -4
- package/dist/module/lib/Components/AmountInput/AmountInput.js.map +1 -1
- package/dist/module/lib/Components/AmountInput/AmountInput.test.js +152 -0
- package/dist/module/lib/Components/AmountInput/AmountInput.test.js.map +1 -0
- package/dist/module/lib/Components/Avatar/Avatar.figma.js +5 -0
- package/dist/module/lib/Components/Avatar/Avatar.figma.js.map +1 -1
- package/dist/module/lib/Components/Avatar/Avatar.js +9 -2
- package/dist/module/lib/Components/Avatar/Avatar.js.map +1 -1
- package/dist/module/lib/Components/Avatar/Avatar.mdx +9 -0
- package/dist/module/lib/Components/Avatar/Avatar.stories.js +47 -0
- package/dist/module/lib/Components/Avatar/Avatar.stories.js.map +1 -1
- package/dist/module/lib/Components/Avatar/Avatar.test.js +23 -1
- package/dist/module/lib/Components/Avatar/Avatar.test.js.map +1 -1
- package/dist/module/lib/Components/Switch/Switch.js +144 -8
- package/dist/module/lib/Components/Switch/Switch.js.map +1 -1
- package/dist/typescript/src/lib/Components/AmountDisplay/AmountDisplay.d.ts.map +1 -1
- package/dist/typescript/src/lib/Components/AmountInput/AmountInput.d.ts.map +1 -1
- package/dist/typescript/src/lib/Components/Avatar/Avatar.d.ts +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 +6 -0
- package/dist/typescript/src/lib/Components/Avatar/types.d.ts.map +1 -1
- package/dist/typescript/src/lib/Components/Switch/Switch.d.ts +1 -1
- package/dist/typescript/src/lib/Components/Switch/Switch.d.ts.map +1 -1
- package/dist/typescript/src/lib/Components/Switch/types.d.ts +2 -1
- package/dist/typescript/src/lib/Components/Switch/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/lib/Components/AmountDisplay/AmountDisplay.test.tsx +92 -0
- package/src/lib/Components/AmountDisplay/AmountDisplay.tsx +37 -15
- package/src/lib/Components/AmountInput/AmountInput.test.tsx +166 -0
- package/src/lib/Components/AmountInput/AmountInput.tsx +6 -1
- package/src/lib/Components/Avatar/Avatar.figma.tsx +5 -0
- package/src/lib/Components/Avatar/Avatar.mdx +9 -0
- package/src/lib/Components/Avatar/Avatar.stories.tsx +41 -0
- package/src/lib/Components/Avatar/Avatar.test.tsx +31 -1
- package/src/lib/Components/Avatar/Avatar.tsx +17 -4
- package/src/lib/Components/Avatar/types.ts +6 -0
- package/src/lib/Components/Switch/Switch.tsx +132 -11
- package/src/lib/Components/Switch/types.ts +3 -1
- package/dist/module/lib/Components/Switch/BaseSwitch.js +0 -221
- package/dist/module/lib/Components/Switch/BaseSwitch.js.map +0 -1
- package/dist/typescript/src/lib/Components/Switch/BaseSwitch.d.ts +0 -13
- package/dist/typescript/src/lib/Components/Switch/BaseSwitch.d.ts.map +0 -1
- package/src/lib/Components/Switch/BaseSwitch.tsx +0 -249
|
@@ -164,6 +164,12 @@ const useAnimatedDigitStrip = ({
|
|
|
164
164
|
return { animatedStyle };
|
|
165
165
|
};
|
|
166
166
|
|
|
167
|
+
// Horizontal breathing room for the clip box so glyphs that are slightly wider
|
|
168
|
+
// than the measured `targetWidth` are not cut on their left/right edges. RN has
|
|
169
|
+
// no per-axis overflow, so we extend the clip box horizontally while keeping the
|
|
170
|
+
// vertical clip tight to `lineHeight`
|
|
171
|
+
const HORIZONTAL_CLIP_PADDING = 8;
|
|
172
|
+
|
|
167
173
|
const DigitStrip = memo(
|
|
168
174
|
({ value, textStyle, animate, widths }: DigitStripProps) => {
|
|
169
175
|
const targetWidth = widths[value];
|
|
@@ -179,23 +185,39 @@ const DigitStrip = memo(
|
|
|
179
185
|
|
|
180
186
|
return (
|
|
181
187
|
<Animated.View
|
|
182
|
-
style={{
|
|
188
|
+
style={{
|
|
189
|
+
height: lineHeight,
|
|
190
|
+
width: animate ? width : targetWidth,
|
|
191
|
+
}}
|
|
183
192
|
accessibilityValue={{ text: String(value) }}
|
|
184
193
|
>
|
|
185
|
-
<
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
194
|
+
<View
|
|
195
|
+
pointerEvents='none'
|
|
196
|
+
style={{
|
|
197
|
+
position: 'absolute',
|
|
198
|
+
top: 0,
|
|
199
|
+
bottom: 0,
|
|
200
|
+
left: -HORIZONTAL_CLIP_PADDING,
|
|
201
|
+
right: -HORIZONTAL_CLIP_PADDING,
|
|
202
|
+
overflow: 'hidden',
|
|
203
|
+
alignItems: 'center',
|
|
204
|
+
}}
|
|
205
|
+
>
|
|
206
|
+
<Animated.View style={[animatedStyle, { alignItems: 'center' }]}>
|
|
207
|
+
{DIGITS.map((d) => (
|
|
208
|
+
<Text
|
|
209
|
+
allowFontScaling={false}
|
|
210
|
+
key={d}
|
|
211
|
+
style={[
|
|
212
|
+
textStyle,
|
|
213
|
+
RuntimeConstants.isAndroid && { height: lineHeight },
|
|
214
|
+
]}
|
|
215
|
+
>
|
|
216
|
+
{d}
|
|
217
|
+
</Text>
|
|
218
|
+
))}
|
|
219
|
+
</Animated.View>
|
|
220
|
+
</View>
|
|
199
221
|
</Animated.View>
|
|
200
222
|
);
|
|
201
223
|
},
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { describe, expect, it, jest } from '@jest/globals';
|
|
2
|
+
import { ledgerLiveThemes } from '@ledgerhq/lumen-design-core';
|
|
3
|
+
import { fireEvent, render, screen } from '@testing-library/react-native';
|
|
4
|
+
import { ThemeProvider } from '../ThemeProvider/ThemeProvider';
|
|
5
|
+
import { AmountInput } from './AmountInput';
|
|
6
|
+
|
|
7
|
+
const hidden = { includeHiddenElements: true } as const;
|
|
8
|
+
|
|
9
|
+
const renderWithProvider = (component: React.ReactElement) =>
|
|
10
|
+
render(
|
|
11
|
+
<ThemeProvider themes={ledgerLiveThemes} colorScheme='dark' locale='en'>
|
|
12
|
+
{component}
|
|
13
|
+
</ThemeProvider>,
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
describe('AmountInput', () => {
|
|
17
|
+
describe('Rendering', () => {
|
|
18
|
+
it('renders with an empty value', () => {
|
|
19
|
+
renderWithProvider(
|
|
20
|
+
<AmountInput value='' onChangeText={jest.fn()} testID='input' />,
|
|
21
|
+
);
|
|
22
|
+
expect(screen.getByTestId('input')).toBeTruthy();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('renders the currency text', () => {
|
|
26
|
+
renderWithProvider(
|
|
27
|
+
<AmountInput value='' onChangeText={jest.fn()} currencyText='USD' />,
|
|
28
|
+
);
|
|
29
|
+
expect(screen.getByText('USD', hidden)).toBeTruthy();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('displays 0 when value is empty', () => {
|
|
33
|
+
renderWithProvider(<AmountInput value='' onChangeText={jest.fn()} />);
|
|
34
|
+
expect(screen.getByText('0', hidden)).toBeTruthy();
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('Interactions', () => {
|
|
39
|
+
it('calls onChangeText with formatted text on change', () => {
|
|
40
|
+
const onChangeText = jest.fn();
|
|
41
|
+
renderWithProvider(
|
|
42
|
+
<AmountInput
|
|
43
|
+
value=''
|
|
44
|
+
onChangeText={onChangeText}
|
|
45
|
+
testID='input'
|
|
46
|
+
thousandsSeparator={false}
|
|
47
|
+
/>,
|
|
48
|
+
);
|
|
49
|
+
fireEvent.changeText(screen.getByTestId('input'), '123.45');
|
|
50
|
+
expect(onChangeText).toHaveBeenCalledWith('123.45');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('formats with thousands separator on user input', () => {
|
|
54
|
+
const onChangeText = jest.fn();
|
|
55
|
+
renderWithProvider(
|
|
56
|
+
<AmountInput value='' onChangeText={onChangeText} testID='input' />,
|
|
57
|
+
);
|
|
58
|
+
fireEvent.changeText(screen.getByTestId('input'), '1000');
|
|
59
|
+
expect(onChangeText).toHaveBeenCalledWith('1 000');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('strips decimal input when allowDecimals is false', () => {
|
|
63
|
+
const onChangeText = jest.fn();
|
|
64
|
+
renderWithProvider(
|
|
65
|
+
<AmountInput
|
|
66
|
+
value=''
|
|
67
|
+
onChangeText={onChangeText}
|
|
68
|
+
allowDecimals={false}
|
|
69
|
+
thousandsSeparator={false}
|
|
70
|
+
testID='input'
|
|
71
|
+
/>,
|
|
72
|
+
);
|
|
73
|
+
fireEvent.changeText(screen.getByTestId('input'), '12.34');
|
|
74
|
+
expect(onChangeText).toHaveBeenCalledWith('1234');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('truncates decimal part with maxDecimalLength on user input', () => {
|
|
78
|
+
const onChangeText = jest.fn();
|
|
79
|
+
renderWithProvider(
|
|
80
|
+
<AmountInput
|
|
81
|
+
value=''
|
|
82
|
+
onChangeText={onChangeText}
|
|
83
|
+
maxDecimalLength={2}
|
|
84
|
+
thousandsSeparator={false}
|
|
85
|
+
testID='input'
|
|
86
|
+
/>,
|
|
87
|
+
);
|
|
88
|
+
fireEvent.changeText(screen.getByTestId('input'), '1.2345');
|
|
89
|
+
expect(onChangeText).toHaveBeenCalledWith('1.23');
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe('programmatic value formatting', () => {
|
|
94
|
+
it('formats a raw unformatted string on initial render', () => {
|
|
95
|
+
renderWithProvider(
|
|
96
|
+
<AmountInput value='2433.123456789' onChangeText={jest.fn()} />,
|
|
97
|
+
);
|
|
98
|
+
expect(screen.getByText('2 433.123456789', hidden)).toBeTruthy();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('formats a number value on initial render', () => {
|
|
102
|
+
renderWithProvider(
|
|
103
|
+
<AmountInput value={2433.123456789} onChangeText={jest.fn()} />,
|
|
104
|
+
);
|
|
105
|
+
expect(screen.getByText('2 433.123456789', hidden)).toBeTruthy();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('adds thousands grouping for a large number value', () => {
|
|
109
|
+
renderWithProvider(
|
|
110
|
+
<AmountInput value={1000000} onChangeText={jest.fn()} />,
|
|
111
|
+
);
|
|
112
|
+
expect(screen.getByText('1 000 000', hidden)).toBeTruthy();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('applies maxDecimalLength to a programmatically set value', () => {
|
|
116
|
+
renderWithProvider(
|
|
117
|
+
<AmountInput
|
|
118
|
+
value='2433.123456789'
|
|
119
|
+
maxDecimalLength={2}
|
|
120
|
+
onChangeText={jest.fn()}
|
|
121
|
+
/>,
|
|
122
|
+
);
|
|
123
|
+
expect(screen.getByText('2 433.12', hidden)).toBeTruthy();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('applies maxIntegerLength to a programmatically set value', () => {
|
|
127
|
+
renderWithProvider(
|
|
128
|
+
<AmountInput
|
|
129
|
+
value='1234567890'
|
|
130
|
+
maxIntegerLength={3}
|
|
131
|
+
allowDecimals={false}
|
|
132
|
+
thousandsSeparator={false}
|
|
133
|
+
onChangeText={jest.fn()}
|
|
134
|
+
/>,
|
|
135
|
+
);
|
|
136
|
+
expect(screen.getByText('123', hidden)).toBeTruthy();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('omits thousands separator when thousandsSeparator is false', () => {
|
|
140
|
+
renderWithProvider(
|
|
141
|
+
<AmountInput
|
|
142
|
+
value='2433.12'
|
|
143
|
+
thousandsSeparator={false}
|
|
144
|
+
onChangeText={jest.fn()}
|
|
145
|
+
/>,
|
|
146
|
+
);
|
|
147
|
+
expect(screen.getByText('2433.12', hidden)).toBeTruthy();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('formats consistently with what typing the same digits produces', () => {
|
|
151
|
+
const onChangeText = jest.fn();
|
|
152
|
+
renderWithProvider(
|
|
153
|
+
<AmountInput
|
|
154
|
+
value='2433.123456789'
|
|
155
|
+
onChangeText={onChangeText}
|
|
156
|
+
testID='input'
|
|
157
|
+
/>,
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
expect(screen.getByText('2 433.123456789', hidden)).toBeTruthy();
|
|
161
|
+
|
|
162
|
+
fireEvent.changeText(screen.getByTestId('input'), '2433.123456789');
|
|
163
|
+
expect(onChangeText).toHaveBeenCalledWith('2 433.123456789');
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
});
|
|
@@ -46,7 +46,12 @@ export const AmountInput = ({
|
|
|
46
46
|
...props
|
|
47
47
|
}: AmountInputProps) => {
|
|
48
48
|
const inputRef = useRef<TextInput>(null);
|
|
49
|
-
const inputValue = String(value)
|
|
49
|
+
const inputValue = textFormatter(String(value), {
|
|
50
|
+
allowDecimals,
|
|
51
|
+
thousandsSeparator,
|
|
52
|
+
maxIntegerLength,
|
|
53
|
+
maxDecimalLength,
|
|
54
|
+
});
|
|
50
55
|
const [isFocused, setIsFocused] = useState(false);
|
|
51
56
|
const disabled = useDisabledContext({
|
|
52
57
|
consumerName: 'AmountInput',
|
|
@@ -7,6 +7,10 @@ figma.connect(
|
|
|
7
7
|
{
|
|
8
8
|
imports: ["import { Avatar } from '@ledgerhq/lumen-ui-rnative'"],
|
|
9
9
|
props: {
|
|
10
|
+
appearance: figma.enum('appearance', {
|
|
11
|
+
gray: 'gray',
|
|
12
|
+
transparent: 'transparent',
|
|
13
|
+
}),
|
|
10
14
|
size: figma.enum('size', {
|
|
11
15
|
sm: 'sm',
|
|
12
16
|
md: 'md',
|
|
@@ -21,6 +25,7 @@ figma.connect(
|
|
|
21
25
|
example: (props) => (
|
|
22
26
|
<Avatar
|
|
23
27
|
src='https://example-image.com'
|
|
28
|
+
appearance={props.appearance}
|
|
24
29
|
size={props.size}
|
|
25
30
|
alt="John Doe's Avatar"
|
|
26
31
|
showNotification={props.showNotification}
|
|
@@ -40,6 +40,15 @@ Avatars come in four different sizes:
|
|
|
40
40
|
|
|
41
41
|
<Canvas of={AvatarStories.SizeShowcase} />
|
|
42
42
|
|
|
43
|
+
### Appearance
|
|
44
|
+
|
|
45
|
+
The `appearance` prop controls the background color of the avatar container.
|
|
46
|
+
|
|
47
|
+
- **`gray`**: Uses a muted gray background
|
|
48
|
+
- **`transparent`**: Uses a semi-transparent muted background — default
|
|
49
|
+
|
|
50
|
+
<Canvas of={AvatarStories.AppearanceShowcase} />
|
|
51
|
+
|
|
43
52
|
### States
|
|
44
53
|
|
|
45
54
|
The Avatar component handles two primary states automatically:
|
|
@@ -90,6 +90,47 @@ export const SizeShowcase: Story = {
|
|
|
90
90
|
),
|
|
91
91
|
};
|
|
92
92
|
|
|
93
|
+
const AppearanceShowcaseRender = () => {
|
|
94
|
+
return (
|
|
95
|
+
<Box lx={{ gap: 's8' }}>
|
|
96
|
+
<Box lx={{ flexDirection: 'row', gap: 's16', padding: 's8' }}>
|
|
97
|
+
<Avatar
|
|
98
|
+
alt='gray fallback'
|
|
99
|
+
size='md'
|
|
100
|
+
appearance='gray'
|
|
101
|
+
showNotification={false}
|
|
102
|
+
/>
|
|
103
|
+
<Avatar
|
|
104
|
+
alt='transparent fallback'
|
|
105
|
+
size='md'
|
|
106
|
+
appearance='transparent'
|
|
107
|
+
showNotification={false}
|
|
108
|
+
/>
|
|
109
|
+
</Box>
|
|
110
|
+
<Box lx={{ flexDirection: 'row', gap: 's16', padding: 's8' }}>
|
|
111
|
+
<Avatar
|
|
112
|
+
src={exampleSrc}
|
|
113
|
+
alt='gray with image'
|
|
114
|
+
size='md'
|
|
115
|
+
appearance='gray'
|
|
116
|
+
showNotification={false}
|
|
117
|
+
/>
|
|
118
|
+
<Avatar
|
|
119
|
+
src={exampleSrc}
|
|
120
|
+
alt='transparent with image'
|
|
121
|
+
size='md'
|
|
122
|
+
appearance='transparent'
|
|
123
|
+
showNotification={false}
|
|
124
|
+
/>
|
|
125
|
+
</Box>
|
|
126
|
+
</Box>
|
|
127
|
+
);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
export const AppearanceShowcase: Story = {
|
|
131
|
+
render: () => <AppearanceShowcaseRender />,
|
|
132
|
+
};
|
|
133
|
+
|
|
93
134
|
export const FallbackShowcase: Story = {
|
|
94
135
|
args: {
|
|
95
136
|
src: 'https://brokenLink.random',
|
|
@@ -4,7 +4,7 @@ import { render, waitFor } from '@testing-library/react-native';
|
|
|
4
4
|
import { ThemeProvider } from '../ThemeProvider/ThemeProvider';
|
|
5
5
|
import { Avatar } from './Avatar';
|
|
6
6
|
|
|
7
|
-
const { sizes } = ledgerLiveThemes.dark;
|
|
7
|
+
const { sizes, colors } = ledgerLiveThemes.dark;
|
|
8
8
|
|
|
9
9
|
const TestWrapper = ({ children }: { children: React.ReactNode }) => (
|
|
10
10
|
<ThemeProvider themes={ledgerLiveThemes} colorScheme='dark' locale='en'>
|
|
@@ -33,6 +33,36 @@ describe('Avatar Component', () => {
|
|
|
33
33
|
getByLabelText('user profile');
|
|
34
34
|
});
|
|
35
35
|
|
|
36
|
+
it('should render with transparent appearance by default', () => {
|
|
37
|
+
const { getByTestId } = render(
|
|
38
|
+
<TestWrapper>
|
|
39
|
+
<Avatar testID='avatar-id' />
|
|
40
|
+
</TestWrapper>,
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
expect(getByTestId('avatar-id').props.style.backgroundColor).toBe(
|
|
44
|
+
colors.bg.mutedTransparent,
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it.each<['transparent' | 'gray', string]>([
|
|
49
|
+
['transparent', colors.bg.mutedTransparent],
|
|
50
|
+
['gray', colors.bg.muted],
|
|
51
|
+
])(
|
|
52
|
+
'should render with %s appearance when specified',
|
|
53
|
+
(appearance, expectedColor) => {
|
|
54
|
+
const { getByTestId } = render(
|
|
55
|
+
<TestWrapper>
|
|
56
|
+
<Avatar testID='avatar-id' appearance={appearance} />
|
|
57
|
+
</TestWrapper>,
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
expect(getByTestId('avatar-id').props.style.backgroundColor).toBe(
|
|
61
|
+
expectedColor,
|
|
62
|
+
);
|
|
63
|
+
},
|
|
64
|
+
);
|
|
65
|
+
|
|
36
66
|
it('should render with different sizes', () => {
|
|
37
67
|
const { getByTestId, rerender } = render(
|
|
38
68
|
<TestWrapper>
|
|
@@ -7,6 +7,7 @@ import { DotIndicator } from '../DotIndicator';
|
|
|
7
7
|
import { Box } from '../Utility';
|
|
8
8
|
import type { AvatarProps } from './types';
|
|
9
9
|
|
|
10
|
+
type Appearance = NonNullable<AvatarProps['appearance']>;
|
|
10
11
|
type Size = NonNullable<AvatarProps['size']>;
|
|
11
12
|
|
|
12
13
|
const fallbackSizes = {
|
|
@@ -23,9 +24,20 @@ const dotSizeMap: Partial<
|
|
|
23
24
|
md: 'xl',
|
|
24
25
|
};
|
|
25
26
|
|
|
26
|
-
const useStyles = ({
|
|
27
|
+
const useStyles = ({
|
|
28
|
+
appearance,
|
|
29
|
+
size,
|
|
30
|
+
}: {
|
|
31
|
+
appearance: Appearance;
|
|
32
|
+
size: Size;
|
|
33
|
+
}) => {
|
|
27
34
|
return useStyleSheet(
|
|
28
35
|
(t) => {
|
|
36
|
+
const backgroundColors: Record<Appearance, string> = {
|
|
37
|
+
gray: t.colors.bg.muted,
|
|
38
|
+
transparent: t.colors.bg.mutedTransparent,
|
|
39
|
+
};
|
|
40
|
+
|
|
29
41
|
const sizeMap = {
|
|
30
42
|
sm: { size: t.sizes.s40, padding: t.spacings.s4 },
|
|
31
43
|
md: { size: t.sizes.s48, padding: t.spacings.s4 },
|
|
@@ -39,7 +51,7 @@ const useStyles = ({ size }: { size: Size }) => {
|
|
|
39
51
|
width: sizeMap[size].size,
|
|
40
52
|
height: sizeMap[size].size,
|
|
41
53
|
borderRadius: 9999,
|
|
42
|
-
backgroundColor:
|
|
54
|
+
backgroundColor: backgroundColors[appearance],
|
|
43
55
|
alignItems: 'center',
|
|
44
56
|
justifyContent: 'center',
|
|
45
57
|
padding: sizeMap[size].padding,
|
|
@@ -52,7 +64,7 @@ const useStyles = ({ size }: { size: Size }) => {
|
|
|
52
64
|
},
|
|
53
65
|
};
|
|
54
66
|
},
|
|
55
|
-
[size],
|
|
67
|
+
[appearance, size],
|
|
56
68
|
);
|
|
57
69
|
};
|
|
58
70
|
|
|
@@ -77,6 +89,7 @@ export const Avatar = ({
|
|
|
77
89
|
style,
|
|
78
90
|
src,
|
|
79
91
|
alt = 'avatar',
|
|
92
|
+
appearance = 'transparent',
|
|
80
93
|
size = 'md',
|
|
81
94
|
showNotification: showNotificationProp = false,
|
|
82
95
|
testID,
|
|
@@ -86,7 +99,7 @@ export const Avatar = ({
|
|
|
86
99
|
const { t } = useCommonTranslation();
|
|
87
100
|
const [error, setError] = useState<boolean>(false);
|
|
88
101
|
const shouldFallback = !src || error;
|
|
89
|
-
const styles = useStyles({ size });
|
|
102
|
+
const styles = useStyles({ appearance, size });
|
|
90
103
|
|
|
91
104
|
const resolvedAlt = alt || t('components.avatar.defaultAlt');
|
|
92
105
|
|
|
@@ -11,6 +11,12 @@ export type AvatarProps = {
|
|
|
11
11
|
* @optional
|
|
12
12
|
*/
|
|
13
13
|
alt?: string;
|
|
14
|
+
/**
|
|
15
|
+
* The visual appearance of the avatar background: `gray` or `transparent`.
|
|
16
|
+
* @optional
|
|
17
|
+
* @default transparent
|
|
18
|
+
*/
|
|
19
|
+
appearance?: 'gray' | 'transparent';
|
|
14
20
|
/**
|
|
15
21
|
* The size variant of the avatar.
|
|
16
22
|
* @optional
|
|
@@ -1,9 +1,107 @@
|
|
|
1
1
|
import { useDisabledContext } from '@ledgerhq/lumen-utils-shared';
|
|
2
|
+
import { useCallback } from 'react';
|
|
3
|
+
import { View, type GestureResponderEvent, StyleSheet } from 'react-native';
|
|
4
|
+
import Animated, {
|
|
5
|
+
useAnimatedStyle,
|
|
6
|
+
withTiming,
|
|
7
|
+
} from 'react-native-reanimated';
|
|
8
|
+
import { useStyleSheet, useTheme } from '../../../styles';
|
|
9
|
+
import { useTimingConfig } from '../../Animations/useTimingConfig';
|
|
2
10
|
import { useControllableState } from '../../utils';
|
|
3
11
|
import { Pressable } from '../Utility';
|
|
4
|
-
import { BaseSwitchThumb, BaseSwitchRoot } from './BaseSwitch';
|
|
5
12
|
|
|
6
|
-
import type { SwitchProps } from './types';
|
|
13
|
+
import type { SwitchProps, SwitchSize } from './types';
|
|
14
|
+
|
|
15
|
+
const useSwitchStyles = ({
|
|
16
|
+
checked,
|
|
17
|
+
disabled,
|
|
18
|
+
size,
|
|
19
|
+
}: {
|
|
20
|
+
checked: boolean;
|
|
21
|
+
disabled: boolean;
|
|
22
|
+
size: SwitchSize;
|
|
23
|
+
}) => {
|
|
24
|
+
const { theme } = useTheme();
|
|
25
|
+
|
|
26
|
+
const styles = useStyleSheet(
|
|
27
|
+
(t) => {
|
|
28
|
+
const sizes: Record<SwitchSize, { width: number; height: number }> = {
|
|
29
|
+
sm: { width: t.sizes.s24, height: t.sizes.s16 },
|
|
30
|
+
md: { width: t.sizes.s40, height: t.sizes.s24 },
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const thumbSizes: Record<SwitchSize, number> = {
|
|
34
|
+
sm: t.sizes.s12,
|
|
35
|
+
md: t.sizes.s20,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
root: StyleSheet.flatten([
|
|
40
|
+
{
|
|
41
|
+
borderRadius: t.borderRadius.full,
|
|
42
|
+
padding: t.spacings.s2,
|
|
43
|
+
overflow: 'hidden',
|
|
44
|
+
...sizes[size],
|
|
45
|
+
minWidth: sizes[size].width,
|
|
46
|
+
maxWidth: sizes[size].width,
|
|
47
|
+
minHeight: sizes[size].height,
|
|
48
|
+
maxHeight: sizes[size].height,
|
|
49
|
+
},
|
|
50
|
+
!checked &&
|
|
51
|
+
!disabled && {
|
|
52
|
+
backgroundColor: t.colors.bg.mutedStrong,
|
|
53
|
+
},
|
|
54
|
+
checked &&
|
|
55
|
+
!disabled && {
|
|
56
|
+
backgroundColor: t.colors.bg.active,
|
|
57
|
+
},
|
|
58
|
+
disabled && {
|
|
59
|
+
backgroundColor: t.colors.bg.disabled,
|
|
60
|
+
},
|
|
61
|
+
]),
|
|
62
|
+
thumbWrapper: {
|
|
63
|
+
position: 'absolute',
|
|
64
|
+
top: t.spacings.s2,
|
|
65
|
+
left: t.spacings.s2,
|
|
66
|
+
},
|
|
67
|
+
thumb: StyleSheet.flatten([
|
|
68
|
+
{
|
|
69
|
+
borderRadius: t.borderRadius.full,
|
|
70
|
+
backgroundColor: 'white',
|
|
71
|
+
width: thumbSizes[size],
|
|
72
|
+
height: thumbSizes[size],
|
|
73
|
+
},
|
|
74
|
+
disabled && {
|
|
75
|
+
backgroundColor: t.colors.bg.base,
|
|
76
|
+
},
|
|
77
|
+
]),
|
|
78
|
+
};
|
|
79
|
+
},
|
|
80
|
+
[checked, disabled, size],
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
const thumbTranslations: Record<SwitchSize, number> = {
|
|
84
|
+
sm: theme.spacings.s8,
|
|
85
|
+
md: theme.spacings.s16,
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
return { ...styles, thumbTranslate: thumbTranslations[size] };
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const useAnimatedThumb = (checked: boolean, translate: number) => {
|
|
92
|
+
const timing = useTimingConfig({ duration: 200, easing: 'easeInOut' });
|
|
93
|
+
|
|
94
|
+
// Reanimated best practice: derive the animation reactively by calling
|
|
95
|
+
// `withTiming` inside `useAnimatedStyle`. When `checked` (or `translate`)
|
|
96
|
+
// changes the worklet re-runs and animates from the current value to the
|
|
97
|
+
// new target -- no shared value or effect required.
|
|
98
|
+
return useAnimatedStyle(
|
|
99
|
+
() => ({
|
|
100
|
+
transform: [{ translateX: withTiming(checked ? translate : 0, timing) }],
|
|
101
|
+
}),
|
|
102
|
+
[checked, translate, timing],
|
|
103
|
+
);
|
|
104
|
+
};
|
|
7
105
|
|
|
8
106
|
/**
|
|
9
107
|
* The switch follows the design system tokens and supports checked, unchecked,
|
|
@@ -36,6 +134,7 @@ export const Switch = ({
|
|
|
36
134
|
defaultChecked = false,
|
|
37
135
|
disabled: disabledProp,
|
|
38
136
|
size = 'md',
|
|
137
|
+
onPress: onPressProp,
|
|
39
138
|
ref,
|
|
40
139
|
...props
|
|
41
140
|
}: SwitchProps) => {
|
|
@@ -49,16 +148,38 @@ export const Switch = ({
|
|
|
49
148
|
defaultProp: defaultChecked,
|
|
50
149
|
});
|
|
51
150
|
|
|
151
|
+
const styles = useSwitchStyles({
|
|
152
|
+
checked: !!checked,
|
|
153
|
+
disabled: !!disabled,
|
|
154
|
+
size,
|
|
155
|
+
});
|
|
156
|
+
const animatedStyle = useAnimatedThumb(!!checked, styles.thumbTranslate);
|
|
157
|
+
|
|
158
|
+
const onPress = useCallback(
|
|
159
|
+
(ev: GestureResponderEvent) => {
|
|
160
|
+
if (disabled) return;
|
|
161
|
+
onCheckedChange(!checked);
|
|
162
|
+
onPressProp?.(ev);
|
|
163
|
+
},
|
|
164
|
+
[disabled, checked, onCheckedChange, onPressProp],
|
|
165
|
+
);
|
|
166
|
+
|
|
52
167
|
return (
|
|
53
|
-
<Pressable
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
168
|
+
<Pressable
|
|
169
|
+
ref={ref}
|
|
170
|
+
lx={lx}
|
|
171
|
+
role='switch'
|
|
172
|
+
aria-checked={checked}
|
|
173
|
+
aria-disabled={disabled}
|
|
174
|
+
accessibilityState={{ checked, disabled }}
|
|
175
|
+
disabled={disabled}
|
|
176
|
+
onPress={onPress}
|
|
177
|
+
style={[styles.root, style]}
|
|
178
|
+
{...props}
|
|
179
|
+
>
|
|
180
|
+
<Animated.View style={[styles.thumbWrapper, animatedStyle]}>
|
|
181
|
+
<View style={styles.thumb} />
|
|
182
|
+
</Animated.View>
|
|
62
183
|
</Pressable>
|
|
63
184
|
);
|
|
64
185
|
};
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { StyledPressableProps } from '../../../styles';
|
|
2
2
|
|
|
3
|
+
export type SwitchSize = 'sm' | 'md';
|
|
4
|
+
|
|
3
5
|
export type SwitchProps = {
|
|
4
6
|
/**
|
|
5
7
|
* The disabled state of the switch.
|
|
@@ -15,7 +17,7 @@ export type SwitchProps = {
|
|
|
15
17
|
* The size of the switch.
|
|
16
18
|
* @default 'md'
|
|
17
19
|
*/
|
|
18
|
-
size?:
|
|
20
|
+
size?: SwitchSize;
|
|
19
21
|
/**
|
|
20
22
|
* The callback function called when the checked state changes.
|
|
21
23
|
*/
|