@ledgerhq/lumen-ui-rnative 0.0.71 → 0.0.73
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/.storybook/preview-head.html +1 -0
- 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/BottomSheet/BottomSheet.stories.d.ts +4 -0
- package/dist/src/lib/Components/BottomSheet/BottomSheet.stories.d.ts.map +1 -1
- package/dist/src/lib/Components/BottomSheet/BottomSheet.stories.js +4 -0
- package/dist/src/lib/Components/Icon/Icon.d.ts.map +1 -1
- package/dist/src/lib/Components/Icon/Icon.js +1 -0
- package/dist/src/lib/Components/Icon/Icon.stories.js +1 -1
- package/dist/src/lib/Components/Icon/types.d.ts +1 -1
- package/dist/src/lib/Components/Icon/types.d.ts.map +1 -1
- package/dist/src/lib/Components/InteractiveIcon/InteractiveIcon.d.ts +1 -1
- package/dist/src/lib/Components/InteractiveIcon/InteractiveIcon.d.ts.map +1 -1
- package/dist/src/lib/Components/InteractiveIcon/InteractiveIcon.js +10 -2
- package/dist/src/lib/Components/InteractiveIcon/types.d.ts +17 -0
- package/dist/src/lib/Components/InteractiveIcon/types.d.ts.map +1 -1
- package/dist/src/lib/Components/InteractiveIcon/types.js +24 -1
- 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/Utility/Gradient/LinearGradient/LinearGradient.stories.d.ts.map +1 -1
- package/dist/src/lib/Components/Utility/Gradient/LinearGradient/LinearGradient.stories.js +0 -2
- 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 +3 -3
- 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/BottomSheet/BottomSheet.stories.tsx +4 -0
- package/src/lib/Components/Icon/Icon.stories.tsx +1 -1
- package/src/lib/Components/Icon/Icon.test.tsx +1 -1
- package/src/lib/Components/Icon/Icon.tsx +1 -0
- package/src/lib/Components/Icon/types.ts +1 -1
- package/src/lib/Components/InteractiveIcon/InteractiveIcon.tsx +15 -2
- package/src/lib/Components/InteractiveIcon/types.ts +47 -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/Utility/Gradient/LinearGradient/LinearGradient.stories.tsx +0 -2
- package/src/lib/Components/index.ts +3 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { Meta, StoryObj } from '@storybook/react-native-web-vite';
|
|
2
|
+
import { Box } from '../Utility/Box';
|
|
3
|
+
import { Skeleton } from './Skeleton';
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof Skeleton> = {
|
|
6
|
+
title: 'Communication/Skeleton',
|
|
7
|
+
component: Skeleton,
|
|
8
|
+
parameters: {
|
|
9
|
+
actions: { disable: true },
|
|
10
|
+
},
|
|
11
|
+
argTypes: {
|
|
12
|
+
component: {
|
|
13
|
+
control: 'select',
|
|
14
|
+
options: [undefined, 'list-item', 'tile'],
|
|
15
|
+
description: 'Pre-built skeleton component variant',
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export default meta;
|
|
21
|
+
type Story = StoryObj<typeof Skeleton>;
|
|
22
|
+
|
|
23
|
+
export const Base: Story = {
|
|
24
|
+
args: {
|
|
25
|
+
lx: {
|
|
26
|
+
height: 's16',
|
|
27
|
+
width: 's256',
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
render: (args) => (
|
|
31
|
+
<Box lx={{ padding: 's16', backgroundColor: 'canvas', borderRadius: 'md' }}>
|
|
32
|
+
<Skeleton
|
|
33
|
+
component={args.component}
|
|
34
|
+
lx={args.component ? undefined : args.lx}
|
|
35
|
+
/>
|
|
36
|
+
</Box>
|
|
37
|
+
),
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const WithListItem: Story = {
|
|
41
|
+
render: () => (
|
|
42
|
+
<Box lx={{ padding: 's16', backgroundColor: 'canvas', borderRadius: 'md' }}>
|
|
43
|
+
<Skeleton component='list-item' />
|
|
44
|
+
</Box>
|
|
45
|
+
),
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const WithTile: Story = {
|
|
49
|
+
render: () => (
|
|
50
|
+
<Box lx={{ padding: 's16', backgroundColor: 'canvas', borderRadius: 'md' }}>
|
|
51
|
+
<Skeleton component='tile' />
|
|
52
|
+
</Box>
|
|
53
|
+
),
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const SizeShowcase: Story = {
|
|
57
|
+
render: () => (
|
|
58
|
+
<Box
|
|
59
|
+
lx={{
|
|
60
|
+
padding: 's16',
|
|
61
|
+
backgroundColor: 'canvas',
|
|
62
|
+
borderRadius: 'md',
|
|
63
|
+
gap: 's4',
|
|
64
|
+
}}
|
|
65
|
+
>
|
|
66
|
+
<Skeleton lx={{ height: 's40', width: 's56' }} />
|
|
67
|
+
<Skeleton lx={{ height: 's12', width: 's112' }} />
|
|
68
|
+
<Skeleton lx={{ height: 's128', width: 's256' }} />
|
|
69
|
+
</Box>
|
|
70
|
+
),
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export const ShapeShowcase: Story = {
|
|
74
|
+
render: () => (
|
|
75
|
+
<Box
|
|
76
|
+
lx={{
|
|
77
|
+
padding: 's16',
|
|
78
|
+
backgroundColor: 'canvas',
|
|
79
|
+
borderRadius: 'md',
|
|
80
|
+
gap: 's4',
|
|
81
|
+
}}
|
|
82
|
+
>
|
|
83
|
+
<Skeleton lx={{ height: 's40', width: 's256', borderRadius: 'none' }} />
|
|
84
|
+
<Skeleton lx={{ height: 's40', width: 's256', borderRadius: 'lg' }} />
|
|
85
|
+
<Skeleton lx={{ width: 's48', height: 's48', borderRadius: 'full' }} />
|
|
86
|
+
<Skeleton lx={{ width: 's48', height: 's48', borderRadius: 'md' }} />
|
|
87
|
+
</Box>
|
|
88
|
+
),
|
|
89
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
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 { Skeleton } from './Skeleton';
|
|
7
|
+
|
|
8
|
+
const TestWrapper = ({ children }: { children: React.ReactNode }) => (
|
|
9
|
+
<ThemeProvider themes={ledgerLiveThemes} colorScheme='dark' locale='en'>
|
|
10
|
+
{children}
|
|
11
|
+
</ThemeProvider>
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
describe('Skeleton Component', () => {
|
|
15
|
+
it('should render correctly', () => {
|
|
16
|
+
const { getByTestId } = render(
|
|
17
|
+
<TestWrapper>
|
|
18
|
+
<Skeleton />
|
|
19
|
+
</TestWrapper>,
|
|
20
|
+
);
|
|
21
|
+
const skeletonElement = getByTestId('skeleton');
|
|
22
|
+
expect(skeletonElement).toBeTruthy();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should render list-item variant', () => {
|
|
26
|
+
const { getByTestId } = render(
|
|
27
|
+
<TestWrapper>
|
|
28
|
+
<Skeleton testID='list-item-skeleton' component='list-item' />
|
|
29
|
+
</TestWrapper>,
|
|
30
|
+
);
|
|
31
|
+
const skeletonElement = getByTestId('list-item-skeleton');
|
|
32
|
+
expect(skeletonElement).toBeTruthy();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should render tile variant', () => {
|
|
36
|
+
const { getByTestId } = render(
|
|
37
|
+
<TestWrapper>
|
|
38
|
+
<Skeleton testID='tile-skeleton' component='tile' />
|
|
39
|
+
</TestWrapper>,
|
|
40
|
+
);
|
|
41
|
+
const skeletonElement = getByTestId('tile-skeleton');
|
|
42
|
+
expect(skeletonElement).toBeTruthy();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should accept additional props', () => {
|
|
46
|
+
const { getByTestId } = render(
|
|
47
|
+
<TestWrapper>
|
|
48
|
+
<Skeleton testID='custom-skeleton' />
|
|
49
|
+
</TestWrapper>,
|
|
50
|
+
);
|
|
51
|
+
const skeletonElement = getByTestId('custom-skeleton');
|
|
52
|
+
expect(skeletonElement).toBeTruthy();
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { Pulse } from '../../Animations/Pulse';
|
|
2
|
+
import { Box } from '../Utility';
|
|
3
|
+
import type { SkeletonProps } from './types';
|
|
4
|
+
|
|
5
|
+
/** Internal base skeleton element */
|
|
6
|
+
const BaseSkeleton = ({ lx, ...props }: SkeletonProps) => {
|
|
7
|
+
return (
|
|
8
|
+
<Box
|
|
9
|
+
lx={{
|
|
10
|
+
borderRadius: 'md',
|
|
11
|
+
backgroundColor: 'mutedTransparent',
|
|
12
|
+
...lx,
|
|
13
|
+
}}
|
|
14
|
+
{...props}
|
|
15
|
+
/>
|
|
16
|
+
);
|
|
17
|
+
};
|
|
18
|
+
BaseSkeleton.displayName = 'BaseSkeleton';
|
|
19
|
+
|
|
20
|
+
const ListItemSkeleton = ({ lx, ...props }: SkeletonProps) => {
|
|
21
|
+
return (
|
|
22
|
+
<Box
|
|
23
|
+
lx={{
|
|
24
|
+
flexDirection: 'row',
|
|
25
|
+
width: 'full',
|
|
26
|
+
alignItems: 'center',
|
|
27
|
+
gap: 's16',
|
|
28
|
+
paddingVertical: 's8',
|
|
29
|
+
...lx,
|
|
30
|
+
}}
|
|
31
|
+
{...props}
|
|
32
|
+
>
|
|
33
|
+
<BaseSkeleton
|
|
34
|
+
lx={{
|
|
35
|
+
width: 's48',
|
|
36
|
+
height: 's48',
|
|
37
|
+
borderRadius: 'full',
|
|
38
|
+
flexShrink: 0,
|
|
39
|
+
}}
|
|
40
|
+
/>
|
|
41
|
+
<Box lx={{ flex: 1, flexDirection: 'column', gap: 's10' }}>
|
|
42
|
+
<BaseSkeleton
|
|
43
|
+
lx={{ height: 's12', width: 's176', borderRadius: 'full' }}
|
|
44
|
+
/>
|
|
45
|
+
<BaseSkeleton
|
|
46
|
+
lx={{ height: 's12', width: 's112', borderRadius: 'full' }}
|
|
47
|
+
/>
|
|
48
|
+
</Box>
|
|
49
|
+
</Box>
|
|
50
|
+
);
|
|
51
|
+
};
|
|
52
|
+
ListItemSkeleton.displayName = 'ListItemSkeleton';
|
|
53
|
+
|
|
54
|
+
const TileSkeleton = ({ lx, ...props }: SkeletonProps) => {
|
|
55
|
+
return (
|
|
56
|
+
<Box
|
|
57
|
+
lx={{
|
|
58
|
+
flexDirection: 'column',
|
|
59
|
+
width: 's112',
|
|
60
|
+
alignItems: 'center',
|
|
61
|
+
gap: 's12',
|
|
62
|
+
borderRadius: 'md',
|
|
63
|
+
paddingHorizontal: 's8',
|
|
64
|
+
paddingVertical: 's16',
|
|
65
|
+
...lx,
|
|
66
|
+
}}
|
|
67
|
+
{...props}
|
|
68
|
+
>
|
|
69
|
+
<BaseSkeleton
|
|
70
|
+
lx={{
|
|
71
|
+
width: 's48',
|
|
72
|
+
height: 's48',
|
|
73
|
+
borderRadius: 'full',
|
|
74
|
+
flexShrink: 0,
|
|
75
|
+
}}
|
|
76
|
+
/>
|
|
77
|
+
<Box
|
|
78
|
+
lx={{
|
|
79
|
+
width: 'full',
|
|
80
|
+
flexDirection: 'column',
|
|
81
|
+
alignItems: 'center',
|
|
82
|
+
gap: 's8',
|
|
83
|
+
}}
|
|
84
|
+
>
|
|
85
|
+
<BaseSkeleton
|
|
86
|
+
lx={{ height: 's12', width: 's48', borderRadius: 'full' }}
|
|
87
|
+
/>
|
|
88
|
+
<BaseSkeleton
|
|
89
|
+
lx={{ height: 's12', width: 's64', borderRadius: 'full' }}
|
|
90
|
+
/>
|
|
91
|
+
</Box>
|
|
92
|
+
</Box>
|
|
93
|
+
);
|
|
94
|
+
};
|
|
95
|
+
TileSkeleton.displayName = 'TileSkeleton';
|
|
96
|
+
|
|
97
|
+
const componentsMap = {
|
|
98
|
+
'list-item': ListItemSkeleton,
|
|
99
|
+
tile: TileSkeleton,
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* A skeleton component that displays a pulsing placeholder for loading content.
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* <Skeleton lx={{ height: 's16', width: 's256' }} />
|
|
107
|
+
*
|
|
108
|
+
* @example
|
|
109
|
+
* // List item variant
|
|
110
|
+
* <Skeleton component='list-item' lx={{ width: 's320' }} />
|
|
111
|
+
*
|
|
112
|
+
* @example
|
|
113
|
+
* // Tile variant
|
|
114
|
+
* <Skeleton component='tile' />
|
|
115
|
+
*/
|
|
116
|
+
const Skeleton = ({ lx, component, ...props }: SkeletonProps) => {
|
|
117
|
+
/**
|
|
118
|
+
* Check if the component is a valid pre-built variant and return the corresponding component.
|
|
119
|
+
*/
|
|
120
|
+
if (component && componentsMap[component]) {
|
|
121
|
+
const Component = componentsMap[component];
|
|
122
|
+
return (
|
|
123
|
+
<Pulse animate>
|
|
124
|
+
<Component {...props} lx={lx} />
|
|
125
|
+
</Pulse>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
<Pulse animate>
|
|
131
|
+
<BaseSkeleton testID='skeleton' lx={lx} {...props} />
|
|
132
|
+
</Pulse>
|
|
133
|
+
);
|
|
134
|
+
};
|
|
135
|
+
Skeleton.displayName = 'Skeleton';
|
|
136
|
+
|
|
137
|
+
export { Skeleton };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { StyledViewProps } from '../../../styles';
|
|
2
|
+
|
|
3
|
+
export type SkeletonProps = {
|
|
4
|
+
/**
|
|
5
|
+
* Pre-built skeleton component variant
|
|
6
|
+
* - `list-item`: Horizontal layout with circle and two text lines
|
|
7
|
+
* - `tile`: Vertical centered layout with circle and two text lines in a rounded container
|
|
8
|
+
*/
|
|
9
|
+
component?: 'list-item' | 'tile';
|
|
10
|
+
} & Omit<StyledViewProps, 'children'>;
|
|
@@ -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
|
+
});
|