@ledgerhq/lumen-ui-rnative 0.0.71 → 0.0.72
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/package.json +3 -3
- package/dist/src/i18n/locales/de.json +3 -0
- package/dist/src/i18n/locales/en.json +3 -0
- package/dist/src/i18n/locales/es.json +3 -0
- package/dist/src/i18n/locales/fr.json +3 -0
- package/dist/src/i18n/locales/ja.json +3 -0
- package/dist/src/i18n/locales/ko.json +3 -0
- package/dist/src/i18n/locales/pt.json +3 -0
- package/dist/src/i18n/locales/ru.json +3 -0
- package/dist/src/i18n/locales/th.json +3 -0
- package/dist/src/i18n/locales/tr.json +3 -0
- package/dist/src/i18n/locales/zh.json +3 -0
- package/dist/src/lib/Components/Skeleton/Skeleton.d.ts +21 -0
- package/dist/src/lib/Components/Skeleton/Skeleton.d.ts.map +1 -0
- package/dist/src/lib/Components/Skeleton/Skeleton.js +81 -0
- package/dist/src/lib/Components/Skeleton/Skeleton.stories.d.ts +11 -0
- package/dist/src/lib/Components/Skeleton/Skeleton.stories.d.ts.map +1 -0
- package/dist/src/lib/Components/Skeleton/Skeleton.stories.js +49 -0
- package/dist/src/lib/Components/Skeleton/index.d.ts +3 -0
- package/dist/src/lib/Components/Skeleton/index.d.ts.map +1 -0
- package/dist/src/lib/Components/Skeleton/index.js +2 -0
- package/dist/src/lib/Components/Skeleton/types.d.ts +10 -0
- package/dist/src/lib/Components/Skeleton/types.d.ts.map +1 -0
- package/dist/src/lib/Components/Skeleton/types.js +1 -0
- package/dist/src/lib/Components/Stepper/Stepper.d.ts +16 -0
- package/dist/src/lib/Components/Stepper/Stepper.d.ts.map +1 -0
- package/dist/src/lib/Components/Stepper/Stepper.js +74 -0
- package/dist/src/lib/Components/Stepper/Stepper.stories.d.ts +9 -0
- package/dist/src/lib/Components/Stepper/Stepper.stories.d.ts.map +1 -0
- package/dist/src/lib/Components/Stepper/Stepper.stories.js +35 -0
- package/dist/src/lib/Components/Stepper/index.d.ts +3 -0
- package/dist/src/lib/Components/Stepper/index.d.ts.map +1 -0
- package/dist/src/lib/Components/Stepper/index.js +2 -0
- package/dist/src/lib/Components/Stepper/types.d.ts +21 -0
- package/dist/src/lib/Components/Stepper/types.d.ts.map +1 -0
- package/dist/src/lib/Components/Stepper/types.js +1 -0
- package/dist/src/lib/Components/index.d.ts +3 -0
- package/dist/src/lib/Components/index.d.ts.map +1 -1
- package/dist/src/lib/Components/index.js +3 -0
- package/package.json +1 -1
- package/src/i18n/locales/de.json +3 -0
- package/src/i18n/locales/en.json +3 -0
- package/src/i18n/locales/es.json +3 -0
- package/src/i18n/locales/fr.json +3 -0
- package/src/i18n/locales/ja.json +3 -0
- package/src/i18n/locales/ko.json +3 -0
- package/src/i18n/locales/pt.json +3 -0
- package/src/i18n/locales/ru.json +3 -0
- package/src/i18n/locales/th.json +3 -0
- package/src/i18n/locales/tr.json +3 -0
- package/src/i18n/locales/zh.json +3 -0
- package/src/lib/Components/Skeleton/Skeleton.mdx +200 -0
- package/src/lib/Components/Skeleton/Skeleton.stories.tsx +89 -0
- package/src/lib/Components/Skeleton/Skeleton.test.tsx +54 -0
- package/src/lib/Components/Skeleton/Skeleton.tsx +137 -0
- package/src/lib/Components/Skeleton/index.ts +2 -0
- package/src/lib/Components/Skeleton/types.ts +10 -0
- package/src/lib/Components/Stepper/Stepper.mdx +217 -0
- package/src/lib/Components/Stepper/Stepper.stories.tsx +62 -0
- package/src/lib/Components/Stepper/Stepper.test.tsx +132 -0
- package/src/lib/Components/Stepper/Stepper.tsx +159 -0
- package/src/lib/Components/Stepper/index.ts +2 -0
- package/src/lib/Components/Stepper/types.ts +21 -0
- package/src/lib/Components/index.ts +3 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { Meta, Canvas, Controls } from '@storybook/addon-docs/blocks';
|
|
2
|
+
import { CustomTabs, Tab } from '../../../../.storybook/components';
|
|
3
|
+
import * as StepperStories from './Stepper.stories';
|
|
4
|
+
import { DoVsDontRow, DoBlockItem, DontBlockItem } from '../../../../.storybook/components/DoVsDont';
|
|
5
|
+
import CommonRulesDoAndDont from '../../../../.storybook/components/DoVsDont/CommonRulesDoAndDont.mdx';
|
|
6
|
+
import { Box } from '../Utility/Box';
|
|
7
|
+
import { Text } from '../Utility/Text';
|
|
8
|
+
|
|
9
|
+
<Meta title='Communication/Stepper/Docs' of={StepperStories} />
|
|
10
|
+
|
|
11
|
+
# 🔄 Stepper
|
|
12
|
+
|
|
13
|
+
<CustomTabs>
|
|
14
|
+
<Tab label="Overview">
|
|
15
|
+
|
|
16
|
+
The Stepper component provides visual feedback for multi-step processes and workflows. It displays progress as a circular indicator showing the current step out of total steps, helping users understand where they are in a sequence.
|
|
17
|
+
|
|
18
|
+
## Figma Library
|
|
19
|
+
|
|
20
|
+
All stepper designs are created and managed in our components library. This ensures consistency across all platforms and use cases.
|
|
21
|
+
|
|
22
|
+
- 🔗 **[Figma](https://www.figma.com/design/JxaLVMTWirCpU0rsbZ30k7/2.-Components-Library?node-id=11977-94&m=dev)**
|
|
23
|
+
|
|
24
|
+
## Interactive Example
|
|
25
|
+
|
|
26
|
+
<Canvas of={StepperStories.Base} />
|
|
27
|
+
<Controls of={StepperStories.Base} />
|
|
28
|
+
|
|
29
|
+
## 🎯 Anatomy
|
|
30
|
+
|
|
31
|
+
The Stepper consists of:
|
|
32
|
+
- **Track Circle**: A muted circular background showing the full progress path
|
|
33
|
+
- **Progress Arc**: A colored arc that fills clockwise from the top, representing completed progress
|
|
34
|
+
- **Label**: Centered text showing "current/total" steps (e.g., "2/4")
|
|
35
|
+
|
|
36
|
+
The progress arc starts at the top (12 o'clock position) and fills clockwise. When at 0% progress, a small dot appears at the top instead of an empty arc.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## 🏷️ Labels
|
|
41
|
+
|
|
42
|
+
**Default label**: The default label is "currentStep/totalSteps" (e.g., "2/4").
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
### Step Ranges
|
|
46
|
+
- **`totalSteps`** must be between **2** and **9**
|
|
47
|
+
- **`currentStep`** must be between **0** and **`totalSteps`**
|
|
48
|
+
- Use `currentStep={0}` to show the initial state (minimal dot before starting)
|
|
49
|
+
|
|
50
|
+
> ⚠️ Using more than **9** total steps or exceeding the valid range may cause the UI to break and text to be truncated inside the circular indicator.
|
|
51
|
+
|
|
52
|
+
> ⚠️ Values outside valid ranges are automatically clamped: **`currentStep`** is kept between **0** and **`totalSteps`**.
|
|
53
|
+
|
|
54
|
+
## 🎨 Disabled State
|
|
55
|
+
|
|
56
|
+
The Stepper supports a disabled state that changes the progress arc to a muted appearance:
|
|
57
|
+
|
|
58
|
+
<Canvas of={StepperStories.DisabledShowcase} />
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
## Custom Labels
|
|
62
|
+
|
|
63
|
+
By default, the stepper displays "currentStep/totalSteps" (e.g., "2/4"). You can provide a custom label for special cases:
|
|
64
|
+
|
|
65
|
+
<Canvas of={StepperStories.WithCustomLabel} />
|
|
66
|
+
|
|
67
|
+
> ⚠️ **Note**: The label and step counts are automatically truncated if they overflow the circular indicator. Keep custom labels short to ensure readability within the 48px circle.
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## Usage Guidelines
|
|
72
|
+
|
|
73
|
+
### Best Practices
|
|
74
|
+
|
|
75
|
+
- Keep step counts reasonable (typically 3-7 steps)
|
|
76
|
+
- Use descriptive labels when the numeric format isn't clear
|
|
77
|
+
- Combine with textual step indicators for better context
|
|
78
|
+
- Use disabled for paused or inactive steppers
|
|
79
|
+
|
|
80
|
+
</Tab>
|
|
81
|
+
<Tab label="Implementation">
|
|
82
|
+
|
|
83
|
+
## Installation
|
|
84
|
+
|
|
85
|
+
Install and set up the library with our [Setup Guide →](?path=/docs/getting-started-setup--docs).
|
|
86
|
+
|
|
87
|
+
### Basic Usage
|
|
88
|
+
|
|
89
|
+
Import and use the Stepper component:
|
|
90
|
+
|
|
91
|
+
```tsx
|
|
92
|
+
import React, { useState } from 'react';
|
|
93
|
+
import { View } from 'react-native';
|
|
94
|
+
import { Stepper } from '@ledgerhq/lumen-ui-rnative';
|
|
95
|
+
|
|
96
|
+
function OnboardingFlow() {
|
|
97
|
+
const [currentStep, setCurrentStep] = useState(0);
|
|
98
|
+
const totalSteps = 4;
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<View>
|
|
102
|
+
<Stepper currentStep={currentStep} totalSteps={totalSteps} />
|
|
103
|
+
{/* Your step content */}
|
|
104
|
+
</View>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### With Disabled State
|
|
110
|
+
|
|
111
|
+
Control the visual state based on step completion:
|
|
112
|
+
|
|
113
|
+
```tsx
|
|
114
|
+
import React from 'react';
|
|
115
|
+
import { Box, Stepper } from '@ledgerhq/lumen-ui-rnative';
|
|
116
|
+
|
|
117
|
+
function MultiStepForm() {
|
|
118
|
+
return (
|
|
119
|
+
<Box lx={{ flexDirection: 'row', gap: 's16' }}>
|
|
120
|
+
<Stepper currentStep={1} totalSteps={3} />
|
|
121
|
+
<Stepper currentStep={2} totalSteps={3} />
|
|
122
|
+
<Stepper currentStep={0} totalSteps={3} disabled />
|
|
123
|
+
</Box>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Custom Labels
|
|
129
|
+
|
|
130
|
+
Provide custom labels for localized or descriptive text:
|
|
131
|
+
|
|
132
|
+
```tsx
|
|
133
|
+
import React from 'react';
|
|
134
|
+
import { Stepper } from '@ledgerhq/lumen-ui-rnative';
|
|
135
|
+
|
|
136
|
+
function LocalizedStepper() {
|
|
137
|
+
const currentStep = 3;
|
|
138
|
+
const totalSteps = 5;
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
<Stepper
|
|
142
|
+
currentStep={currentStep}
|
|
143
|
+
totalSteps={totalSteps}
|
|
144
|
+
label={`Étape ${currentStep} sur ${totalSteps}`}
|
|
145
|
+
/>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Do's and Don'ts
|
|
151
|
+
|
|
152
|
+
<Box lx={{ gap: 's24' }}>
|
|
153
|
+
<DoVsDontRow>
|
|
154
|
+
<DoBlockItem
|
|
155
|
+
title='Provide meaningful step counts'
|
|
156
|
+
description='Use realistic total steps that match your flow'
|
|
157
|
+
>
|
|
158
|
+
|
|
159
|
+
{/* prettier-ignore */}
|
|
160
|
+
```tsx
|
|
161
|
+
<Stepper currentStep={3} totalSteps={5} />
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
</DoBlockItem>
|
|
165
|
+
<DontBlockItem
|
|
166
|
+
title="Don't use for single-step processes"
|
|
167
|
+
description='Steppers are for multi-step workflows only'
|
|
168
|
+
>
|
|
169
|
+
|
|
170
|
+
{/* prettier-ignore */}
|
|
171
|
+
```tsx
|
|
172
|
+
<Stepper currentStep={1} totalSteps={1} />
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
</DontBlockItem>
|
|
176
|
+
|
|
177
|
+
</DoVsDontRow>
|
|
178
|
+
|
|
179
|
+
<DoVsDontRow>
|
|
180
|
+
<DoBlockItem
|
|
181
|
+
title='Use disabled for paused steppers'
|
|
182
|
+
description='Reserve disabled for steppers that are not actively progressing'
|
|
183
|
+
>
|
|
184
|
+
|
|
185
|
+
{/* prettier-ignore */}
|
|
186
|
+
```tsx
|
|
187
|
+
<Stepper
|
|
188
|
+
currentStep={2}
|
|
189
|
+
totalSteps={4}
|
|
190
|
+
disabled
|
|
191
|
+
/>
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
</DoBlockItem>
|
|
195
|
+
<DontBlockItem
|
|
196
|
+
title="Don't disable active steps"
|
|
197
|
+
description='Active steps in progress should not be disabled'
|
|
198
|
+
>
|
|
199
|
+
|
|
200
|
+
{/* prettier-ignore */}
|
|
201
|
+
```tsx
|
|
202
|
+
<Stepper
|
|
203
|
+
currentStep={2}
|
|
204
|
+
totalSteps={4}
|
|
205
|
+
disabled // Wrong for active step
|
|
206
|
+
/>
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
</DontBlockItem>
|
|
210
|
+
|
|
211
|
+
</DoVsDontRow>
|
|
212
|
+
|
|
213
|
+
<CommonRulesDoAndDont />
|
|
214
|
+
</Box>
|
|
215
|
+
|
|
216
|
+
</Tab>
|
|
217
|
+
</CustomTabs>
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Meta, StoryObj } from '@storybook/react-native-web-vite';
|
|
2
|
+
import { Box } from '../Utility/Box';
|
|
3
|
+
import { Text } from '../Utility/Text';
|
|
4
|
+
import { Stepper } from './Stepper';
|
|
5
|
+
|
|
6
|
+
const meta: Meta<typeof Stepper> = {
|
|
7
|
+
title: 'Communication/Stepper',
|
|
8
|
+
component: Stepper,
|
|
9
|
+
parameters: {
|
|
10
|
+
actions: { disable: true },
|
|
11
|
+
},
|
|
12
|
+
argTypes: {
|
|
13
|
+
currentStep: { control: 'number' },
|
|
14
|
+
totalSteps: { control: 'number' },
|
|
15
|
+
disabled: { control: 'boolean' },
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export default meta;
|
|
20
|
+
type Story = StoryObj<typeof Stepper>;
|
|
21
|
+
|
|
22
|
+
export const Base: Story = {
|
|
23
|
+
args: {
|
|
24
|
+
currentStep: 2,
|
|
25
|
+
totalSteps: 4,
|
|
26
|
+
},
|
|
27
|
+
render: (args) => <Stepper {...args} />,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const DisabledShowcase: Story = {
|
|
31
|
+
render: () => (
|
|
32
|
+
<Box lx={{ gap: 's32', alignItems: 'center' }}>
|
|
33
|
+
<Box lx={{ alignItems: 'center', gap: 's8' }}>
|
|
34
|
+
<Text typography='body3' lx={{ color: 'muted' }}>
|
|
35
|
+
Default (2/4)
|
|
36
|
+
</Text>
|
|
37
|
+
<Stepper currentStep={2} totalSteps={4} />
|
|
38
|
+
</Box>
|
|
39
|
+
<Box lx={{ alignItems: 'center', gap: 's8' }}>
|
|
40
|
+
<Text typography='body3' lx={{ color: 'muted' }}>
|
|
41
|
+
Disabled (2/4)
|
|
42
|
+
</Text>
|
|
43
|
+
<Stepper currentStep={2} totalSteps={4} disabled />
|
|
44
|
+
</Box>
|
|
45
|
+
<Box lx={{ alignItems: 'center', gap: 's8' }}>
|
|
46
|
+
<Text typography='body3' lx={{ color: 'muted' }}>
|
|
47
|
+
Unstarted (0/9)
|
|
48
|
+
</Text>
|
|
49
|
+
<Stepper currentStep={0} totalSteps={9} />
|
|
50
|
+
</Box>
|
|
51
|
+
</Box>
|
|
52
|
+
),
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const WithCustomLabel: Story = {
|
|
56
|
+
args: {
|
|
57
|
+
currentStep: 5,
|
|
58
|
+
totalSteps: 5,
|
|
59
|
+
label: '🎉',
|
|
60
|
+
},
|
|
61
|
+
render: (args) => <Stepper {...args} />,
|
|
62
|
+
};
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { describe, it, expect } from '@jest/globals';
|
|
2
|
+
import { ledgerLiveThemes } from '@ledgerhq/lumen-design-core';
|
|
3
|
+
import { render } from '@testing-library/react-native';
|
|
4
|
+
import React from 'react';
|
|
5
|
+
import { ThemeProvider } from '../ThemeProvider/ThemeProvider';
|
|
6
|
+
import { Stepper } from './Stepper';
|
|
7
|
+
|
|
8
|
+
const TestWrapper = ({ children }: { children: React.ReactNode }) => (
|
|
9
|
+
<ThemeProvider themes={ledgerLiveThemes} colorScheme='dark' locale='en'>
|
|
10
|
+
{children}
|
|
11
|
+
</ThemeProvider>
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
describe('Stepper Component', () => {
|
|
15
|
+
it('should render with i18n accessibility label', async () => {
|
|
16
|
+
const { findByLabelText } = render(
|
|
17
|
+
<TestWrapper>
|
|
18
|
+
<Stepper currentStep={2} totalSteps={4} />
|
|
19
|
+
</TestWrapper>,
|
|
20
|
+
);
|
|
21
|
+
const stepper = await findByLabelText('Step 2 of 4');
|
|
22
|
+
expect(stepper).toBeTruthy();
|
|
23
|
+
expect(stepper.props.accessibilityRole).toBe('progressbar');
|
|
24
|
+
expect(stepper.props.accessibilityValue).toEqual(
|
|
25
|
+
expect.objectContaining({
|
|
26
|
+
now: 2,
|
|
27
|
+
min: 1,
|
|
28
|
+
max: 4,
|
|
29
|
+
text: '2/4',
|
|
30
|
+
}),
|
|
31
|
+
);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should render with custom label', () => {
|
|
35
|
+
const { getByLabelText } = render(
|
|
36
|
+
<TestWrapper>
|
|
37
|
+
<Stepper currentStep={3} totalSteps={5} label='Step 3 of 5' />
|
|
38
|
+
</TestWrapper>,
|
|
39
|
+
);
|
|
40
|
+
const stepper = getByLabelText('Step 3 of 5');
|
|
41
|
+
expect(stepper).toBeTruthy();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should render active border color by default', () => {
|
|
45
|
+
const { getByTestId } = render(
|
|
46
|
+
<TestWrapper>
|
|
47
|
+
<Stepper currentStep={1} totalSteps={4} testID='stepper' />
|
|
48
|
+
</TestWrapper>,
|
|
49
|
+
);
|
|
50
|
+
const stepper = getByTestId('stepper');
|
|
51
|
+
expect(stepper).toBeTruthy();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should render disabled style when disabled', () => {
|
|
55
|
+
const { getByTestId } = render(
|
|
56
|
+
<TestWrapper>
|
|
57
|
+
<Stepper currentStep={2} totalSteps={4} disabled testID='stepper' />
|
|
58
|
+
</TestWrapper>,
|
|
59
|
+
);
|
|
60
|
+
const stepper = getByTestId('stepper');
|
|
61
|
+
expect(stepper).toBeTruthy();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should clamp currentStep to totalSteps in display', () => {
|
|
65
|
+
const { getByText } = render(
|
|
66
|
+
<TestWrapper>
|
|
67
|
+
<Stepper currentStep={10} totalSteps={4} />
|
|
68
|
+
</TestWrapper>,
|
|
69
|
+
);
|
|
70
|
+
expect(getByText('4')).toBeTruthy();
|
|
71
|
+
expect(getByText('/4')).toBeTruthy();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should handle zero totalSteps gracefully', async () => {
|
|
75
|
+
const { findByLabelText } = render(
|
|
76
|
+
<TestWrapper>
|
|
77
|
+
<Stepper currentStep={1} totalSteps={0} />
|
|
78
|
+
</TestWrapper>,
|
|
79
|
+
);
|
|
80
|
+
const stepper = await findByLabelText('Step 0 of 0');
|
|
81
|
+
expect(stepper).toBeTruthy();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should handle currentStep <= 0 (minimal dot)', async () => {
|
|
85
|
+
const { findByLabelText } = render(
|
|
86
|
+
<TestWrapper>
|
|
87
|
+
<Stepper currentStep={0} totalSteps={4} />
|
|
88
|
+
</TestWrapper>,
|
|
89
|
+
);
|
|
90
|
+
const stepper = await findByLabelText('Step 0 of 4');
|
|
91
|
+
expect(stepper).toBeTruthy();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should clamp negative currentStep to 0', async () => {
|
|
95
|
+
const { findByLabelText, getByText } = render(
|
|
96
|
+
<TestWrapper>
|
|
97
|
+
<Stepper currentStep={-3} totalSteps={4} />
|
|
98
|
+
</TestWrapper>,
|
|
99
|
+
);
|
|
100
|
+
const stepper = await findByLabelText('Step 0 of 4');
|
|
101
|
+
expect(stepper).toBeTruthy();
|
|
102
|
+
expect(stepper.props.accessibilityValue).toEqual(
|
|
103
|
+
expect.objectContaining({
|
|
104
|
+
now: -3,
|
|
105
|
+
min: 1,
|
|
106
|
+
max: 4,
|
|
107
|
+
text: '0/4',
|
|
108
|
+
}),
|
|
109
|
+
);
|
|
110
|
+
expect(getByText('0')).toBeTruthy();
|
|
111
|
+
expect(getByText('/4')).toBeTruthy();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('should apply custom lx props', () => {
|
|
115
|
+
const { getByTestId } = render(
|
|
116
|
+
<TestWrapper>
|
|
117
|
+
<Stepper
|
|
118
|
+
currentStep={2}
|
|
119
|
+
totalSteps={4}
|
|
120
|
+
testID='stepper'
|
|
121
|
+
lx={{ marginTop: 's8' }}
|
|
122
|
+
/>
|
|
123
|
+
</TestWrapper>,
|
|
124
|
+
);
|
|
125
|
+
const stepper = getByTestId('stepper');
|
|
126
|
+
const style = stepper.props.style;
|
|
127
|
+
const flatStyle = Array.isArray(style)
|
|
128
|
+
? Object.assign({}, ...style)
|
|
129
|
+
: style;
|
|
130
|
+
expect(flatStyle.marginTop).toBe(8);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { getStepperCalculations } from '@ledgerhq/lumen-utils-shared';
|
|
2
|
+
import { useEffect, useRef } from 'react';
|
|
3
|
+
import { Animated, Easing } from 'react-native';
|
|
4
|
+
import Svg, { Circle } from 'react-native-svg';
|
|
5
|
+
import { useCommonTranslation } from '../../../i18n';
|
|
6
|
+
import { useTheme } from '../../../styles';
|
|
7
|
+
import { Box } from '../Utility/Box';
|
|
8
|
+
import { Text } from '../Utility/Text';
|
|
9
|
+
import { StepperProps } from './types';
|
|
10
|
+
|
|
11
|
+
const SIZE = 48;
|
|
12
|
+
const STROKE_WIDTH = 4;
|
|
13
|
+
|
|
14
|
+
const AnimatedCircle = Animated.createAnimatedComponent(Circle);
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A circular stepper component showing progress as current step out of total steps.
|
|
18
|
+
* Renders a track arc with a progress arc and a center label.
|
|
19
|
+
*
|
|
20
|
+
* @see [Figma – Stepper](https://www.figma.com/design/JxaLVMTWirCpU0rsbZ30k7/2.-Components-Library?node-id=11977-94&m=dev)
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* <Stepper currentStep={1} totalSteps={4} />
|
|
24
|
+
* <Stepper currentStep={0} totalSteps={9} disabled /> // Shows minimal dot, disabled style
|
|
25
|
+
*/
|
|
26
|
+
export const Stepper = ({
|
|
27
|
+
lx = {},
|
|
28
|
+
currentStep,
|
|
29
|
+
totalSteps,
|
|
30
|
+
disabled = false,
|
|
31
|
+
label,
|
|
32
|
+
ref,
|
|
33
|
+
...props
|
|
34
|
+
}: StepperProps) => {
|
|
35
|
+
const { t } = useCommonTranslation();
|
|
36
|
+
const { theme } = useTheme();
|
|
37
|
+
|
|
38
|
+
const {
|
|
39
|
+
displayLabel,
|
|
40
|
+
r,
|
|
41
|
+
cx,
|
|
42
|
+
cy,
|
|
43
|
+
trackDashArray,
|
|
44
|
+
progressDashArray,
|
|
45
|
+
progressDashOffset,
|
|
46
|
+
} = getStepperCalculations({
|
|
47
|
+
currentStep,
|
|
48
|
+
totalSteps,
|
|
49
|
+
size: SIZE,
|
|
50
|
+
label,
|
|
51
|
+
strokeWidth: STROKE_WIDTH,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const animatedOffset = useRef(new Animated.Value(progressDashOffset)).current;
|
|
55
|
+
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
const animation = Animated.timing(animatedOffset, {
|
|
58
|
+
toValue: progressDashOffset,
|
|
59
|
+
duration: 300,
|
|
60
|
+
easing: Easing.inOut(Easing.ease),
|
|
61
|
+
useNativeDriver: false,
|
|
62
|
+
});
|
|
63
|
+
animation.start();
|
|
64
|
+
return () => animation.stop();
|
|
65
|
+
}, [progressDashOffset, animatedOffset]);
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<Box
|
|
69
|
+
ref={ref}
|
|
70
|
+
accessibilityRole='progressbar'
|
|
71
|
+
accessibilityValue={{
|
|
72
|
+
now: currentStep,
|
|
73
|
+
min: 1,
|
|
74
|
+
max: totalSteps,
|
|
75
|
+
text: displayLabel,
|
|
76
|
+
}}
|
|
77
|
+
accessibilityLabel={
|
|
78
|
+
label ??
|
|
79
|
+
t('components.stepper.progressAriaLabel', {
|
|
80
|
+
currentStep: Math.min(Math.max(currentStep, 0), totalSteps),
|
|
81
|
+
totalSteps,
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
lx={{
|
|
85
|
+
width: 's48',
|
|
86
|
+
height: 's48',
|
|
87
|
+
flexShrink: 0,
|
|
88
|
+
alignItems: 'center',
|
|
89
|
+
justifyContent: 'center',
|
|
90
|
+
borderRadius: 'full',
|
|
91
|
+
...lx,
|
|
92
|
+
}}
|
|
93
|
+
{...props}
|
|
94
|
+
>
|
|
95
|
+
<Svg
|
|
96
|
+
width={SIZE}
|
|
97
|
+
height={SIZE}
|
|
98
|
+
viewBox={`0 0 ${SIZE} ${SIZE}`}
|
|
99
|
+
style={{ transform: [{ rotate: '135deg' }] }}
|
|
100
|
+
>
|
|
101
|
+
<Circle
|
|
102
|
+
cx={cx}
|
|
103
|
+
cy={cy}
|
|
104
|
+
r={r}
|
|
105
|
+
fill='none'
|
|
106
|
+
stroke={theme.colors.border.mutedSubtle}
|
|
107
|
+
strokeLinecap='round'
|
|
108
|
+
strokeWidth={STROKE_WIDTH}
|
|
109
|
+
strokeDasharray={trackDashArray}
|
|
110
|
+
strokeDashoffset={0}
|
|
111
|
+
/>
|
|
112
|
+
<AnimatedCircle
|
|
113
|
+
cx={cx}
|
|
114
|
+
cy={cy}
|
|
115
|
+
r={r}
|
|
116
|
+
fill='none'
|
|
117
|
+
stroke={
|
|
118
|
+
disabled
|
|
119
|
+
? theme.colors.border.mutedSubtleHover
|
|
120
|
+
: theme.colors.border.active
|
|
121
|
+
}
|
|
122
|
+
strokeLinecap='round'
|
|
123
|
+
strokeWidth={STROKE_WIDTH}
|
|
124
|
+
strokeDasharray={progressDashArray}
|
|
125
|
+
strokeDashoffset={animatedOffset}
|
|
126
|
+
/>
|
|
127
|
+
</Svg>
|
|
128
|
+
<Box
|
|
129
|
+
lx={{
|
|
130
|
+
position: 'absolute',
|
|
131
|
+
alignItems: 'center',
|
|
132
|
+
justifyContent: 'center',
|
|
133
|
+
flexDirection: 'row',
|
|
134
|
+
top: 's0',
|
|
135
|
+
left: 's0',
|
|
136
|
+
right: 's0',
|
|
137
|
+
bottom: 's0',
|
|
138
|
+
}}
|
|
139
|
+
>
|
|
140
|
+
{label ? (
|
|
141
|
+
<Text typography='body2SemiBold' lx={{ color: 'base' }}>
|
|
142
|
+
{label}
|
|
143
|
+
</Text>
|
|
144
|
+
) : (
|
|
145
|
+
<>
|
|
146
|
+
<Text typography='body1SemiBold' lx={{ color: 'base' }}>
|
|
147
|
+
{Math.min(Math.max(currentStep, 0), totalSteps)}
|
|
148
|
+
</Text>
|
|
149
|
+
<Text typography='body2SemiBold' lx={{ color: 'muted' }}>
|
|
150
|
+
/{totalSteps}
|
|
151
|
+
</Text>
|
|
152
|
+
</>
|
|
153
|
+
)}
|
|
154
|
+
</Box>
|
|
155
|
+
</Box>
|
|
156
|
+
);
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
Stepper.displayName = 'Stepper';
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { StyledViewProps } from '../../../styles';
|
|
2
|
+
|
|
3
|
+
export type StepperProps = {
|
|
4
|
+
/**
|
|
5
|
+
* Current step number (1-based). Use 0 or negative to show minimal dot (before starting).
|
|
6
|
+
*/
|
|
7
|
+
currentStep: number;
|
|
8
|
+
/**
|
|
9
|
+
* Total number of steps.
|
|
10
|
+
*/
|
|
11
|
+
totalSteps: number;
|
|
12
|
+
/**
|
|
13
|
+
* Whether the stepper is disabled. Changes the progress arc to a muted style.
|
|
14
|
+
* @default false
|
|
15
|
+
*/
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Optional custom label. Defaults to "{currentStep}/{totalSteps}".
|
|
19
|
+
*/
|
|
20
|
+
label?: string;
|
|
21
|
+
} & Omit<StyledViewProps, 'children'>;
|
|
@@ -17,7 +17,10 @@ export * from './NavBar';
|
|
|
17
17
|
export * from './PageIndicator';
|
|
18
18
|
export * from './SearchInput';
|
|
19
19
|
export * from './Select';
|
|
20
|
+
export * from './Skeleton';
|
|
21
|
+
export * from './Spinner';
|
|
20
22
|
export * from './Spot';
|
|
23
|
+
export * from './Stepper';
|
|
21
24
|
export * from './Subheader';
|
|
22
25
|
export * from './Switch';
|
|
23
26
|
export * from './TabBar';
|