@ledgerhq/lumen-ui-rnative 0.1.38 → 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/BottomSheet/BottomSheet.js +3 -1
- package/dist/module/lib/Components/BottomSheet/BottomSheet.js.map +1 -1
- package/dist/module/lib/Components/BottomSheet/BottomSheet.stories.js +1 -0
- package/dist/module/lib/Components/BottomSheet/BottomSheet.stories.js.map +1 -1
- package/dist/module/lib/Components/BottomSheet/BottomSheet.test.js +33 -1
- package/dist/module/lib/Components/BottomSheet/BottomSheet.test.js.map +1 -1
- package/dist/module/lib/Components/BottomSheet/BottomSheetHeader.js +7 -3
- package/dist/module/lib/Components/BottomSheet/BottomSheetHeader.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/BottomSheet/BottomSheet.d.ts +2 -2
- package/dist/typescript/src/lib/Components/BottomSheet/BottomSheet.d.ts.map +1 -1
- package/dist/typescript/src/lib/Components/BottomSheet/BottomSheetHeader.d.ts.map +1 -1
- package/dist/typescript/src/lib/Components/BottomSheet/types.d.ts +9 -0
- package/dist/typescript/src/lib/Components/BottomSheet/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/BottomSheet/BottomSheet.stories.tsx +1 -0
- package/src/lib/Components/BottomSheet/BottomSheet.test.tsx +32 -1
- package/src/lib/Components/BottomSheet/BottomSheet.tsx +10 -4
- package/src/lib/Components/BottomSheet/BottomSheetHeader.tsx +10 -6
- package/src/lib/Components/BottomSheet/types.ts +9 -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
|
@@ -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,10 +1,11 @@
|
|
|
1
1
|
import { describe, it, expect, jest } from '@jest/globals';
|
|
2
2
|
import { ledgerLiveThemes } from '@ledgerhq/lumen-design-core';
|
|
3
3
|
import type { RenderOptions } from '@testing-library/react-native';
|
|
4
|
-
import { render } from '@testing-library/react-native';
|
|
4
|
+
import { fireEvent, render } from '@testing-library/react-native';
|
|
5
5
|
import { Text, View } from 'react-native';
|
|
6
6
|
import { ThemeProvider } from '../ThemeProvider/ThemeProvider';
|
|
7
7
|
import { BottomSheet as BottomSheetComponent } from './BottomSheet';
|
|
8
|
+
import { BottomSheetHeader } from './BottomSheetHeader';
|
|
8
9
|
// Mock react-native-gesture-handler which is used by @gorhom/bottom-sheet
|
|
9
10
|
jest.mock('react-native-gesture-handler', () => ({}));
|
|
10
11
|
|
|
@@ -339,6 +340,36 @@ describe('BottomSheet', () => {
|
|
|
339
340
|
expect(onDismiss).not.toHaveBeenCalled();
|
|
340
341
|
});
|
|
341
342
|
|
|
343
|
+
it('calls onHeaderClosePressed when the header close button is pressed', () => {
|
|
344
|
+
const { BottomSheet } = require('./BottomSheet');
|
|
345
|
+
const onHeaderClosePressed = jest.fn();
|
|
346
|
+
const { getByTestId } = renderWithTheme(
|
|
347
|
+
<BottomSheet
|
|
348
|
+
onHeaderClosePressed={onHeaderClosePressed}
|
|
349
|
+
testID='bottom-sheet'
|
|
350
|
+
>
|
|
351
|
+
<BottomSheetHeader title='Title' />
|
|
352
|
+
</BottomSheet>,
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
fireEvent.press(getByTestId('bottom-sheet-header-close-button'));
|
|
356
|
+
|
|
357
|
+
expect(onHeaderClosePressed).toHaveBeenCalledTimes(1);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it('does not require onHeaderClosePressed to close from the header', () => {
|
|
361
|
+
const { BottomSheet } = require('./BottomSheet');
|
|
362
|
+
const { getByTestId } = renderWithTheme(
|
|
363
|
+
<BottomSheet testID='bottom-sheet'>
|
|
364
|
+
<BottomSheetHeader title='Title' />
|
|
365
|
+
</BottomSheet>,
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
expect(() =>
|
|
369
|
+
fireEvent.press(getByTestId('bottom-sheet-header-close-button')),
|
|
370
|
+
).not.toThrow();
|
|
371
|
+
});
|
|
372
|
+
|
|
342
373
|
it('accepts multiple callbacks simultaneously', () => {
|
|
343
374
|
const { BottomSheet } = require('./BottomSheet');
|
|
344
375
|
const onOpen = jest.fn();
|
|
@@ -57,14 +57,18 @@ const useStyles = ({
|
|
|
57
57
|
};
|
|
58
58
|
|
|
59
59
|
const [BottomSheetProvider, useBottomSheetContext] =
|
|
60
|
-
createSafeContext<
|
|
61
|
-
|
|
62
|
-
|
|
60
|
+
createSafeContext<
|
|
61
|
+
Pick<
|
|
62
|
+
BottomSheetProps,
|
|
63
|
+
'onBack' | 'hideCloseButton' | 'onHeaderClosePressed'
|
|
64
|
+
>
|
|
65
|
+
>('BottomSheet');
|
|
63
66
|
|
|
64
67
|
export const BottomSheet = ({
|
|
65
68
|
onOpen,
|
|
66
69
|
onClose,
|
|
67
70
|
onDismiss,
|
|
71
|
+
onHeaderClosePressed,
|
|
68
72
|
onBack,
|
|
69
73
|
onAnimate,
|
|
70
74
|
children,
|
|
@@ -203,7 +207,9 @@ export const BottomSheet = ({
|
|
|
203
207
|
handleComponent={hideHandle ? HiddenHandle : CustomHandle}
|
|
204
208
|
backdropComponent={hideBackdrop ? undefined : renderBackdrop}
|
|
205
209
|
>
|
|
206
|
-
<BottomSheetProvider
|
|
210
|
+
<BottomSheetProvider
|
|
211
|
+
value={{ onBack, hideCloseButton, onHeaderClosePressed }}
|
|
212
|
+
>
|
|
207
213
|
{children}
|
|
208
214
|
</BottomSheetProvider>
|
|
209
215
|
</GorhomBottomSheetModal>
|
|
@@ -97,14 +97,18 @@ export const BottomSheetHeader = ({
|
|
|
97
97
|
}: BottomSheetHeaderProps) => {
|
|
98
98
|
const { t } = useCommonTranslation();
|
|
99
99
|
const { close } = useBottomSheet();
|
|
100
|
-
const { onBack, hideCloseButton } =
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
100
|
+
const { onBack, hideCloseButton, onHeaderClosePressed } =
|
|
101
|
+
useBottomSheetContext({
|
|
102
|
+
consumerName: 'BottomSheetHeader',
|
|
103
|
+
contextRequired: true,
|
|
104
|
+
});
|
|
104
105
|
|
|
105
106
|
const handleClose = useCallback(() => {
|
|
107
|
+
if (onHeaderClosePressed) {
|
|
108
|
+
onHeaderClosePressed();
|
|
109
|
+
}
|
|
106
110
|
close();
|
|
107
|
-
}, [close]);
|
|
111
|
+
}, [close, onHeaderClosePressed]);
|
|
108
112
|
|
|
109
113
|
const hasTitleSection = Boolean(title || description);
|
|
110
114
|
const hasIcons = Boolean(onBack || !hideCloseButton);
|
|
@@ -115,7 +119,7 @@ export const BottomSheetHeader = ({
|
|
|
115
119
|
hidden: !hasIcons && density !== 'compact',
|
|
116
120
|
});
|
|
117
121
|
|
|
118
|
-
if (!
|
|
122
|
+
if (!hasTitleSection && !onBack && hideCloseButton) {
|
|
119
123
|
return null;
|
|
120
124
|
}
|
|
121
125
|
|
|
@@ -94,6 +94,15 @@ export type BottomSheetProps = PropsWithChildren & {
|
|
|
94
94
|
* @default undefined
|
|
95
95
|
*/
|
|
96
96
|
onClose?: () => void;
|
|
97
|
+
/**
|
|
98
|
+
* Callback function to handle when the close button in the header is pressed.
|
|
99
|
+
* This is distinct from {@link onClose} and {@link onDismiss}—those will always
|
|
100
|
+
* also be called after this event if the close button results in a full sheet dismissal.
|
|
101
|
+
* Use this to react specifically to header close intent (e.g., tracking, custom UI),
|
|
102
|
+
* but do cleanup/unmount logic in {@link onClose} or {@link onDismiss}.
|
|
103
|
+
* @default undefined
|
|
104
|
+
*/
|
|
105
|
+
onHeaderClosePressed?: () => void;
|
|
97
106
|
/**
|
|
98
107
|
* Callback function to handle the open event.
|
|
99
108
|
* @default undefined
|