@ledgerhq/lumen-ui-rnative 0.1.1 → 0.1.3
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/Animations/constants.d.ts +28 -0
- package/dist/src/lib/Animations/constants.d.ts.map +1 -0
- package/dist/src/lib/Animations/constants.js +27 -0
- package/dist/src/lib/Animations/index.d.ts +1 -0
- package/dist/src/lib/Animations/index.d.ts.map +1 -1
- package/dist/src/lib/Animations/index.js +1 -0
- package/dist/src/lib/Components/AmountDisplay/AmountDisplay.d.ts +1 -1
- package/dist/src/lib/Components/AmountDisplay/AmountDisplay.d.ts.map +1 -1
- package/dist/src/lib/Components/AmountDisplay/AmountDisplay.js +76 -5
- package/dist/src/lib/Components/AmountDisplay/AmountDisplay.stories.d.ts +1 -0
- package/dist/src/lib/Components/AmountDisplay/AmountDisplay.stories.d.ts.map +1 -1
- package/dist/src/lib/Components/AmountDisplay/AmountDisplay.stories.js +25 -2
- package/dist/src/lib/Components/AmountDisplay/types.d.ts +20 -25
- package/dist/src/lib/Components/AmountDisplay/types.d.ts.map +1 -1
- package/dist/src/lib/Components/AmountDisplay/types.js +1 -1
- package/dist/src/lib/Components/SegmentedControl/SegmentedControl.d.ts +10 -0
- package/dist/src/lib/Components/SegmentedControl/SegmentedControl.d.ts.map +1 -0
- package/dist/src/lib/Components/SegmentedControl/SegmentedControl.js +114 -0
- package/dist/src/lib/Components/SegmentedControl/SegmentedControl.stories.d.ts +58 -0
- package/dist/src/lib/Components/SegmentedControl/SegmentedControl.stories.d.ts.map +1 -0
- package/dist/src/lib/Components/SegmentedControl/SegmentedControl.stories.js +61 -0
- package/dist/src/lib/Components/SegmentedControl/SegmentedControlContext.d.ts +11 -0
- package/dist/src/lib/Components/SegmentedControl/SegmentedControlContext.d.ts.map +1 -0
- package/dist/src/lib/Components/SegmentedControl/SegmentedControlContext.js +7 -0
- package/dist/src/lib/Components/SegmentedControl/index.d.ts +3 -0
- package/dist/src/lib/Components/SegmentedControl/index.d.ts.map +1 -0
- package/dist/src/lib/Components/SegmentedControl/index.js +1 -0
- package/dist/src/lib/Components/SegmentedControl/types.d.ts +45 -0
- package/dist/src/lib/Components/SegmentedControl/types.d.ts.map +1 -0
- package/dist/src/lib/Components/SegmentedControl/types.js +1 -0
- package/dist/src/lib/Components/TabBar/TabBar.js +1 -0
- package/dist/src/lib/Components/TabBar/types.d.ts +0 -1
- package/dist/src/lib/Components/TabBar/types.d.ts.map +1 -1
- package/dist/src/lib/Components/index.d.ts +1 -0
- package/dist/src/lib/Components/index.d.ts.map +1 -1
- package/dist/src/lib/Components/index.js +1 -0
- package/dist/src/styles/theme/resolvers/resolveFontWeights.js +1 -1
- 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/Animations/constants.ts +31 -0
- package/src/lib/Animations/index.ts +1 -0
- package/src/lib/Components/AmountDisplay/AmountDisplay.mdx +7 -1
- package/src/lib/Components/AmountDisplay/AmountDisplay.stories.tsx +29 -2
- package/src/lib/Components/AmountDisplay/AmountDisplay.test.tsx +101 -51
- package/src/lib/Components/AmountDisplay/AmountDisplay.tsx +175 -24
- package/src/lib/Components/AmountDisplay/types.ts +22 -25
- package/src/lib/Components/SegmentedControl/SegmentedControl.mdx +159 -0
- package/src/lib/Components/SegmentedControl/SegmentedControl.stories.tsx +102 -0
- package/src/lib/Components/SegmentedControl/SegmentedControl.test.tsx +57 -0
- package/src/lib/Components/SegmentedControl/SegmentedControl.tsx +202 -0
- package/src/lib/Components/SegmentedControl/SegmentedControlContext.tsx +17 -0
- package/src/lib/Components/SegmentedControl/index.ts +2 -0
- package/src/lib/Components/SegmentedControl/types.ts +46 -0
- package/src/lib/Components/TabBar/TabBar.tsx +1 -0
- package/src/lib/Components/TabBar/types.ts +0 -1
- package/src/lib/Components/index.ts +1 -0
- package/src/styles/theme/createStylesheetTheme.test.ts +1 -1
- package/src/styles/theme/resolvers/resolveFontWeights.test.ts +9 -6
- package/src/styles/theme/resolvers/resolveFontWeights.ts +1 -1
- package/dist/src/lib/Components/Banner/Banner.figma.d.ts +0 -2
- package/dist/src/lib/Components/Banner/Banner.figma.d.ts.map +0 -1
- package/dist/src/lib/Components/Banner/Banner.figma.js +0 -45
- package/dist/src/lib/Components/Checkbox/Checkbox.figma.d.ts +0 -2
- package/dist/src/lib/Components/Checkbox/Checkbox.figma.d.ts.map +0 -1
- package/dist/src/lib/Components/Checkbox/Checkbox.figma.js +0 -32
- package/dist/src/lib/Components/InteractiveIcon/InteractiveIcon.figma.d.ts +0 -2
- package/dist/src/lib/Components/InteractiveIcon/InteractiveIcon.figma.d.ts.map +0 -1
- package/dist/src/lib/Components/InteractiveIcon/InteractiveIcon.figma.js +0 -26
- package/dist/src/lib/Components/Switch/Switch.figma.d.ts +0 -2
- package/dist/src/lib/Components/Switch/Switch.figma.d.ts.map +0 -1
- package/dist/src/lib/Components/Switch/Switch.figma.js +0 -32
- package/dist/src/lib/Components/Tile/Tile.figma.d.ts +0 -2
- package/dist/src/lib/Components/Tile/Tile.figma.d.ts.map +0 -1
- package/dist/src/lib/Components/Tile/Tile.figma.js +0 -28
- package/src/lib/Components/Banner/Banner.figma.tsx +0 -59
- package/src/lib/Components/Checkbox/Checkbox.figma.tsx +0 -49
- package/src/lib/Components/InteractiveIcon/InteractiveIcon.figma.tsx +0 -42
- package/src/lib/Components/Switch/Switch.figma.tsx +0 -47
- package/src/lib/Components/Tile/Tile.figma.tsx +0 -53
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { Meta, Canvas, Controls } from '@storybook/addon-docs/blocks';
|
|
2
|
+
import * as SegmentedControlStories from './SegmentedControl.stories';
|
|
3
|
+
import { SegmentedControl } from './SegmentedControl';
|
|
4
|
+
import { CustomTabs, Tab } from '../../../../.storybook/components';
|
|
5
|
+
import CommonRulesDoAndDont from '../../../../.storybook/components/DoVsDont/CommonRulesDoAndDont.mdx';
|
|
6
|
+
import { Box } from '../Utility';
|
|
7
|
+
|
|
8
|
+
<Meta title='Navigation/SegmentedControl' of={SegmentedControlStories} />
|
|
9
|
+
|
|
10
|
+
# 📑 SegmentedControl
|
|
11
|
+
|
|
12
|
+
<CustomTabs>
|
|
13
|
+
<Tab label="Overview">
|
|
14
|
+
|
|
15
|
+
## Introduction
|
|
16
|
+
|
|
17
|
+
SegmentedControl is a tab bar–style component for switching between mutually exclusive options. Use it when the user needs to choose one of a small set of choices (e.g. transaction type, asset section).
|
|
18
|
+
|
|
19
|
+
> View in [Figma](https://www.figma.com/design/JxaLVMTWirCpU0rsbZ30k7/2.-Components-Library?node-id=1170-2345&m=dev).
|
|
20
|
+
|
|
21
|
+
## Anatomy
|
|
22
|
+
|
|
23
|
+
<Canvas of={SegmentedControlStories.Base} />
|
|
24
|
+
|
|
25
|
+
- **Segments**: Individual options the user can select.
|
|
26
|
+
- **Selected state**: The active segment (sliding pill + semi-bold label).
|
|
27
|
+
- **Optional icon**: Icon to the left of the label (from Symbols).
|
|
28
|
+
|
|
29
|
+
## Properties
|
|
30
|
+
|
|
31
|
+
<Canvas of={SegmentedControlStories.Base} />
|
|
32
|
+
<Controls of={SegmentedControlStories.Base} />
|
|
33
|
+
|
|
34
|
+
### Labels only vs with icons
|
|
35
|
+
|
|
36
|
+
You can use segments with text only (Base), or add an icon to each button (WithIcons). Use one approach consistently for all segments in a single control.
|
|
37
|
+
|
|
38
|
+
<Canvas of={SegmentedControlStories.Base} />
|
|
39
|
+
|
|
40
|
+
<Canvas of={SegmentedControlStories.WithIcons} />
|
|
41
|
+
|
|
42
|
+
## Responsive Layout
|
|
43
|
+
|
|
44
|
+
SegmentedControl lays out segments in a horizontal row with equal width per segment. The sliding pill animates to the selected segment.
|
|
45
|
+
|
|
46
|
+
## Accessibility
|
|
47
|
+
|
|
48
|
+
- Use accessibilityLabel on SegmentedControl to describe the control (e.g. "Transaction type", "Asset section").
|
|
49
|
+
- Each segment exposes accessibilityState with a selected value for screen readers.
|
|
50
|
+
|
|
51
|
+
</Tab>
|
|
52
|
+
<Tab label="Implementation">
|
|
53
|
+
|
|
54
|
+
## Installation
|
|
55
|
+
|
|
56
|
+
Use the library as part of `@ledgerhq/lumen-ui-rnative`. See the [Setup Guide →](?path=/docs/getting-started-setup--docs).
|
|
57
|
+
|
|
58
|
+
## SegmentedControlButton
|
|
59
|
+
|
|
60
|
+
Use as a direct child of SegmentedControl (or inside wrappers such as Tooltip). Required: value (string) and children (label). Optional: icon (component from symbols), onPress (runs in addition to parent onSelectedChange).
|
|
61
|
+
|
|
62
|
+
<div className='my-24 overflow-hidden rounded-lg'>
|
|
63
|
+
<table className='w-full'>
|
|
64
|
+
<thead>
|
|
65
|
+
<tr className='border-b border-muted bg-muted'>
|
|
66
|
+
<th className='p-12 text-left text-on-accent body-2'>Prop</th>
|
|
67
|
+
<th className='p-12 text-left text-on-accent body-2'>Type</th>
|
|
68
|
+
<th className='p-12 text-left text-on-accent body-2'>Description</th>
|
|
69
|
+
</tr>
|
|
70
|
+
</thead>
|
|
71
|
+
<tbody className='bg-canvas'>
|
|
72
|
+
<tr className='border-b border-muted'>
|
|
73
|
+
<td className='text-accent p-12'>value</td>
|
|
74
|
+
<td className='p-12 text-muted'>string</td>
|
|
75
|
+
<td className='p-12 text-muted'>Unique value for this segment (e.g. "send", "receive")</td>
|
|
76
|
+
</tr>
|
|
77
|
+
<tr className='border-b border-muted'>
|
|
78
|
+
<td className='text-accent p-12'>children</td>
|
|
79
|
+
<td className='p-12 text-muted'>ReactNode</td>
|
|
80
|
+
<td className='p-12 text-muted'>Button label (e.g. "Send", "Tokens")</td>
|
|
81
|
+
</tr>
|
|
82
|
+
<tr className='border-b border-muted'>
|
|
83
|
+
<td className='text-accent p-12'>icon</td>
|
|
84
|
+
<td className='p-12 text-muted'>ComponentType</td>
|
|
85
|
+
<td className='p-12 text-muted'>Optional icon to the left of the label (from Symbols)</td>
|
|
86
|
+
</tr>
|
|
87
|
+
<tr>
|
|
88
|
+
<td className='text-accent p-12'>onPress</td>
|
|
89
|
+
<td className='p-12 text-muted'>() => void</td>
|
|
90
|
+
<td className='p-12 text-muted'>Optional callback when the button is pressed (in addition to parent onSelectedChange)</td>
|
|
91
|
+
</tr>
|
|
92
|
+
</tbody>
|
|
93
|
+
</table>
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
## Basic Usage
|
|
97
|
+
|
|
98
|
+
Control selected value in state at the top level and pass it to SegmentedControl. Buttons use value to identify themselves; selected state is derived from context.
|
|
99
|
+
|
|
100
|
+
```tsx
|
|
101
|
+
import React from 'react';
|
|
102
|
+
import { SegmentedControl, SegmentedControlButton } from '@ledgerhq/lumen-ui-rnative';
|
|
103
|
+
|
|
104
|
+
export default function Example() {
|
|
105
|
+
const [state, setState] = React.useState('send');
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<SegmentedControl
|
|
109
|
+
selectedValue={state}
|
|
110
|
+
onSelectedChange={setState}
|
|
111
|
+
accessibilityLabel='Transaction type'
|
|
112
|
+
>
|
|
113
|
+
<SegmentedControlButton value='send'>Send</SegmentedControlButton>
|
|
114
|
+
<SegmentedControlButton value='receive'>Receive</SegmentedControlButton>
|
|
115
|
+
<SegmentedControlButton value='buy'>Buy</SegmentedControlButton>
|
|
116
|
+
</SegmentedControl>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## With icons
|
|
122
|
+
|
|
123
|
+
Pass an icon from symbols to each button for a left-positioned icon. Use icons on all segments or none for consistency.
|
|
124
|
+
|
|
125
|
+
```tsx
|
|
126
|
+
import { SegmentedControl, SegmentedControlButton } from '@ledgerhq/lumen-ui-rnative';
|
|
127
|
+
import { Coins, Nft, TransferHorizontal, Settings } from '@ledgerhq/lumen-ui-rnative/symbols';
|
|
128
|
+
|
|
129
|
+
function Example() {
|
|
130
|
+
const [state, setState] = React.useState('tokens');
|
|
131
|
+
return (
|
|
132
|
+
<SegmentedControl
|
|
133
|
+
selectedValue={state}
|
|
134
|
+
onSelectedChange={setState}
|
|
135
|
+
accessibilityLabel='Asset section'
|
|
136
|
+
>
|
|
137
|
+
<SegmentedControlButton value='tokens' icon={Coins}>
|
|
138
|
+
Tokens
|
|
139
|
+
</SegmentedControlButton>
|
|
140
|
+
<SegmentedControlButton value='nfts' icon={Nft}>
|
|
141
|
+
NFTs
|
|
142
|
+
</SegmentedControlButton>
|
|
143
|
+
<SegmentedControlButton value='activity' icon={TransferHorizontal}>
|
|
144
|
+
Activity
|
|
145
|
+
</SegmentedControlButton>
|
|
146
|
+
<SegmentedControlButton value='settings' icon={Settings}>
|
|
147
|
+
Settings
|
|
148
|
+
</SegmentedControlButton>
|
|
149
|
+
</SegmentedControl>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
<Box lx={{ flexDirection: 'column', gap: 's24' }}>
|
|
155
|
+
<CommonRulesDoAndDont />
|
|
156
|
+
</Box>
|
|
157
|
+
|
|
158
|
+
</Tab>
|
|
159
|
+
</CustomTabs>
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-native-web-vite';
|
|
2
|
+
import React, { useState } from 'react';
|
|
3
|
+
import { Coins, Nft, TransferHorizontal, Settings } from '../../Symbols';
|
|
4
|
+
import { Box } from '../Utility';
|
|
5
|
+
import { SegmentedControl, SegmentedControlButton } from './SegmentedControl';
|
|
6
|
+
|
|
7
|
+
const meta = {
|
|
8
|
+
title: 'Navigation/SegmentedControl',
|
|
9
|
+
component: SegmentedControl,
|
|
10
|
+
subcomponents: {
|
|
11
|
+
SegmentedControlButton,
|
|
12
|
+
},
|
|
13
|
+
parameters: {
|
|
14
|
+
layout: 'centered',
|
|
15
|
+
backgrounds: { default: 'light' },
|
|
16
|
+
},
|
|
17
|
+
argTypes: {
|
|
18
|
+
onSelectedChange: {
|
|
19
|
+
action: 'change',
|
|
20
|
+
description: 'Callback when the selected value changes',
|
|
21
|
+
table: {
|
|
22
|
+
type: { summary: '(value: string) => void' },
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
accessibilityLabel: {
|
|
26
|
+
control: 'text',
|
|
27
|
+
description: 'Accessible label for the control',
|
|
28
|
+
table: {
|
|
29
|
+
type: { summary: 'string' },
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
selectedValue: {
|
|
33
|
+
control: 'text',
|
|
34
|
+
description:
|
|
35
|
+
'The value of the currently selected segment (drives the sliding pill)',
|
|
36
|
+
table: {
|
|
37
|
+
type: { summary: 'string' },
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
children: {
|
|
41
|
+
control: false,
|
|
42
|
+
description: 'SegmentedControlButton elements',
|
|
43
|
+
table: {
|
|
44
|
+
type: { summary: 'ReactNode' },
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
} satisfies Meta<typeof SegmentedControl>;
|
|
49
|
+
|
|
50
|
+
export default meta;
|
|
51
|
+
type Story = StoryObj<typeof meta>;
|
|
52
|
+
|
|
53
|
+
export const Base: Story = {
|
|
54
|
+
args: {} as React.ComponentProps<typeof SegmentedControl>,
|
|
55
|
+
render: () => {
|
|
56
|
+
const [state, setState] = useState('send');
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<Box lx={{ width: 's256' }}>
|
|
60
|
+
<SegmentedControl
|
|
61
|
+
selectedValue={state}
|
|
62
|
+
onSelectedChange={setState}
|
|
63
|
+
accessibilityLabel='Transaction type'
|
|
64
|
+
>
|
|
65
|
+
<SegmentedControlButton value='send'>Send</SegmentedControlButton>
|
|
66
|
+
<SegmentedControlButton value='receive'>
|
|
67
|
+
Receive
|
|
68
|
+
</SegmentedControlButton>
|
|
69
|
+
<SegmentedControlButton value='buy'>Buy</SegmentedControlButton>
|
|
70
|
+
</SegmentedControl>
|
|
71
|
+
</Box>
|
|
72
|
+
);
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export const WithIcons: Story = {
|
|
77
|
+
args: {} as React.ComponentProps<typeof SegmentedControl>,
|
|
78
|
+
render: () => {
|
|
79
|
+
const [state, setState] = useState('tokens');
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<SegmentedControl
|
|
83
|
+
selectedValue={state}
|
|
84
|
+
onSelectedChange={setState}
|
|
85
|
+
accessibilityLabel='Asset section'
|
|
86
|
+
>
|
|
87
|
+
<SegmentedControlButton value='tokens' icon={Coins}>
|
|
88
|
+
Tokens
|
|
89
|
+
</SegmentedControlButton>
|
|
90
|
+
<SegmentedControlButton value='nfts' icon={Nft}>
|
|
91
|
+
NFTs
|
|
92
|
+
</SegmentedControlButton>
|
|
93
|
+
<SegmentedControlButton value='activity' icon={TransferHorizontal}>
|
|
94
|
+
Activity
|
|
95
|
+
</SegmentedControlButton>
|
|
96
|
+
<SegmentedControlButton value='settings' icon={Settings}>
|
|
97
|
+
Settings
|
|
98
|
+
</SegmentedControlButton>
|
|
99
|
+
</SegmentedControl>
|
|
100
|
+
);
|
|
101
|
+
},
|
|
102
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { describe, it, expect, jest } from '@jest/globals';
|
|
2
|
+
import { ledgerLiveThemes } from '@ledgerhq/lumen-design-core';
|
|
3
|
+
import { render, fireEvent } from '@testing-library/react-native';
|
|
4
|
+
import React from 'react';
|
|
5
|
+
import { ThemeProvider } from '../ThemeProvider/ThemeProvider';
|
|
6
|
+
import { SegmentedControl, SegmentedControlButton } from './SegmentedControl';
|
|
7
|
+
|
|
8
|
+
const TestWrapper = ({ children }: { children: React.ReactNode }) => (
|
|
9
|
+
<ThemeProvider themes={ledgerLiveThemes} colorScheme='dark' locale='en'>
|
|
10
|
+
{children}
|
|
11
|
+
</ThemeProvider>
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
describe('SegmentedControl', () => {
|
|
15
|
+
it('renders segments with labels', () => {
|
|
16
|
+
const { getByText } = render(
|
|
17
|
+
<TestWrapper>
|
|
18
|
+
<SegmentedControl
|
|
19
|
+
selectedValue='send'
|
|
20
|
+
onSelectedChange={() => {
|
|
21
|
+
/* empty */
|
|
22
|
+
}}
|
|
23
|
+
accessibilityLabel='Transaction type'
|
|
24
|
+
>
|
|
25
|
+
<SegmentedControlButton value='send'>Send</SegmentedControlButton>
|
|
26
|
+
<SegmentedControlButton value='receive'>
|
|
27
|
+
Receive
|
|
28
|
+
</SegmentedControlButton>
|
|
29
|
+
</SegmentedControl>
|
|
30
|
+
</TestWrapper>,
|
|
31
|
+
);
|
|
32
|
+
expect(getByText('Send')).toBeTruthy();
|
|
33
|
+
expect(getByText('Receive')).toBeTruthy();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('calls onSelectedChange with segment value when a segment is pressed', () => {
|
|
37
|
+
const onSelectedChange = jest.fn();
|
|
38
|
+
const { getByText } = render(
|
|
39
|
+
<TestWrapper>
|
|
40
|
+
<SegmentedControl
|
|
41
|
+
selectedValue='send'
|
|
42
|
+
onSelectedChange={onSelectedChange}
|
|
43
|
+
accessibilityLabel='Transaction type'
|
|
44
|
+
>
|
|
45
|
+
<SegmentedControlButton value='send'>Send</SegmentedControlButton>
|
|
46
|
+
<SegmentedControlButton value='receive'>
|
|
47
|
+
Receive
|
|
48
|
+
</SegmentedControlButton>
|
|
49
|
+
</SegmentedControl>
|
|
50
|
+
</TestWrapper>,
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
fireEvent.press(getByText('Receive'));
|
|
54
|
+
|
|
55
|
+
expect(onSelectedChange).toHaveBeenCalledWith('receive');
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useRef } from 'react';
|
|
2
|
+
import { LayoutChangeEvent } from 'react-native';
|
|
3
|
+
import Animated, {
|
|
4
|
+
useAnimatedStyle,
|
|
5
|
+
useSharedValue,
|
|
6
|
+
withTiming,
|
|
7
|
+
} from 'react-native-reanimated';
|
|
8
|
+
import { useStyleSheet } from '../../../styles';
|
|
9
|
+
import { durations, easingCurves } from '../../Animations/constants';
|
|
10
|
+
import { Box, Pressable, Text } from '../Utility';
|
|
11
|
+
import {
|
|
12
|
+
SegmentedControlContextProvider,
|
|
13
|
+
useSegmentedControlContext,
|
|
14
|
+
} from './SegmentedControlContext';
|
|
15
|
+
import type {
|
|
16
|
+
SegmentedControlButtonProps,
|
|
17
|
+
SegmentedControlProps,
|
|
18
|
+
} from './types';
|
|
19
|
+
|
|
20
|
+
const ICON_SIZE = 16;
|
|
21
|
+
|
|
22
|
+
export function SegmentedControlButton({
|
|
23
|
+
value,
|
|
24
|
+
children,
|
|
25
|
+
icon: Icon,
|
|
26
|
+
onPress,
|
|
27
|
+
...props
|
|
28
|
+
}: SegmentedControlButtonProps) {
|
|
29
|
+
const styles = useButtonStyles();
|
|
30
|
+
const { selectedValue, onSelectedChange } = useSegmentedControlContext();
|
|
31
|
+
|
|
32
|
+
const selected = selectedValue === value;
|
|
33
|
+
|
|
34
|
+
function handlePress() {
|
|
35
|
+
onSelectedChange(value);
|
|
36
|
+
onPress?.();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<Pressable
|
|
41
|
+
onPress={handlePress}
|
|
42
|
+
accessibilityState={{ selected }}
|
|
43
|
+
style={styles.button}
|
|
44
|
+
{...props}
|
|
45
|
+
>
|
|
46
|
+
<Box style={styles.content}>
|
|
47
|
+
{Icon && (
|
|
48
|
+
<Box style={styles.iconWrap}>
|
|
49
|
+
<Icon size={ICON_SIZE} />
|
|
50
|
+
</Box>
|
|
51
|
+
)}
|
|
52
|
+
<Text
|
|
53
|
+
typography={selected ? 'body2SemiBold' : 'body2'}
|
|
54
|
+
lx={{ color: 'base' }}
|
|
55
|
+
style={styles.label}
|
|
56
|
+
>
|
|
57
|
+
{children}
|
|
58
|
+
</Text>
|
|
59
|
+
</Box>
|
|
60
|
+
</Pressable>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
SegmentedControlButton.displayName = 'SegmentedControlButton';
|
|
65
|
+
|
|
66
|
+
function useButtonStyles() {
|
|
67
|
+
return useStyleSheet(
|
|
68
|
+
(t) => ({
|
|
69
|
+
button: {
|
|
70
|
+
flex: 1,
|
|
71
|
+
flexDirection: 'row',
|
|
72
|
+
alignItems: 'center',
|
|
73
|
+
justifyContent: 'center',
|
|
74
|
+
paddingHorizontal: t.spacings.s16,
|
|
75
|
+
paddingVertical: t.spacings.s8,
|
|
76
|
+
borderRadius: t.borderRadius.full,
|
|
77
|
+
zIndex: 1,
|
|
78
|
+
},
|
|
79
|
+
content: {
|
|
80
|
+
flexDirection: 'row',
|
|
81
|
+
alignItems: 'center',
|
|
82
|
+
justifyContent: 'center',
|
|
83
|
+
gap: t.spacings.s8,
|
|
84
|
+
},
|
|
85
|
+
label: {
|
|
86
|
+
textAlign: 'center',
|
|
87
|
+
includeFontPadding: false,
|
|
88
|
+
},
|
|
89
|
+
iconWrap: {
|
|
90
|
+
flexDirection: 'row',
|
|
91
|
+
alignItems: 'center',
|
|
92
|
+
},
|
|
93
|
+
}),
|
|
94
|
+
[],
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function SegmentedControl({
|
|
99
|
+
selectedValue,
|
|
100
|
+
onSelectedChange,
|
|
101
|
+
accessibilityLabel,
|
|
102
|
+
children,
|
|
103
|
+
...props
|
|
104
|
+
}: SegmentedControlProps) {
|
|
105
|
+
const styles = useRootStyles();
|
|
106
|
+
const pillTranslateX = useSharedValue(0);
|
|
107
|
+
const pillWidth = useSharedValue(0);
|
|
108
|
+
const pillHeight = useSharedValue(0);
|
|
109
|
+
const hasLayoutRef = useRef(false);
|
|
110
|
+
|
|
111
|
+
const getSelectedIndex = useCallback((): number => {
|
|
112
|
+
return React.Children.toArray(children).findIndex((child) => {
|
|
113
|
+
if (React.isValidElement(child) && child.props != null) {
|
|
114
|
+
return (child.props as { value?: string }).value === selectedValue;
|
|
115
|
+
}
|
|
116
|
+
return false;
|
|
117
|
+
});
|
|
118
|
+
}, [selectedValue, children]);
|
|
119
|
+
|
|
120
|
+
function onLayout(e: LayoutChangeEvent) {
|
|
121
|
+
const { width, height } = e.nativeEvent.layout;
|
|
122
|
+
const count = React.Children.count(children);
|
|
123
|
+
const slotWidth = count > 0 ? width / count : 0;
|
|
124
|
+
|
|
125
|
+
pillWidth.value = slotWidth;
|
|
126
|
+
pillHeight.value = height;
|
|
127
|
+
|
|
128
|
+
if (!hasLayoutRef.current) {
|
|
129
|
+
hasLayoutRef.current = true;
|
|
130
|
+
const index = getSelectedIndex();
|
|
131
|
+
if (index >= 0) {
|
|
132
|
+
pillTranslateX.value = index * slotWidth;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
useEffect(() => {
|
|
138
|
+
if (!hasLayoutRef.current) return;
|
|
139
|
+
const index = getSelectedIndex();
|
|
140
|
+
if (index >= 0 && pillWidth.value > 0) {
|
|
141
|
+
pillTranslateX.value = withTiming(index * pillWidth.value, {
|
|
142
|
+
duration: durations['250'],
|
|
143
|
+
easing: easingCurves.bezier.default,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}, [pillWidth, pillTranslateX, getSelectedIndex]);
|
|
147
|
+
|
|
148
|
+
const animatedPillStyle = useAnimatedStyle(
|
|
149
|
+
() => ({
|
|
150
|
+
transform: [{ translateX: pillTranslateX.value }],
|
|
151
|
+
width: pillWidth.value,
|
|
152
|
+
height: pillHeight.value,
|
|
153
|
+
}),
|
|
154
|
+
[pillTranslateX, pillWidth, pillHeight],
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
return (
|
|
158
|
+
<SegmentedControlContextProvider
|
|
159
|
+
value={{ selectedValue, onSelectedChange }}
|
|
160
|
+
>
|
|
161
|
+
<Box
|
|
162
|
+
accessibilityRole='radiogroup'
|
|
163
|
+
accessibilityLabel={accessibilityLabel}
|
|
164
|
+
onLayout={onLayout}
|
|
165
|
+
style={styles.container}
|
|
166
|
+
{...props}
|
|
167
|
+
>
|
|
168
|
+
{children}
|
|
169
|
+
<Animated.View
|
|
170
|
+
style={[styles.pill, animatedPillStyle]}
|
|
171
|
+
pointerEvents='none'
|
|
172
|
+
/>
|
|
173
|
+
</Box>
|
|
174
|
+
</SegmentedControlContextProvider>
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
SegmentedControl.displayName = 'SegmentedControl';
|
|
179
|
+
|
|
180
|
+
function useRootStyles() {
|
|
181
|
+
return useStyleSheet(
|
|
182
|
+
(t) => ({
|
|
183
|
+
container: {
|
|
184
|
+
flexDirection: 'row',
|
|
185
|
+
alignItems: 'center',
|
|
186
|
+
position: 'relative',
|
|
187
|
+
width: '100%',
|
|
188
|
+
borderRadius: t.borderRadius.full,
|
|
189
|
+
backgroundColor: t.colors.bg.baseTransparent,
|
|
190
|
+
},
|
|
191
|
+
pill: {
|
|
192
|
+
position: 'absolute',
|
|
193
|
+
top: 0,
|
|
194
|
+
left: 0,
|
|
195
|
+
borderRadius: t.borderRadius.sm,
|
|
196
|
+
backgroundColor: t.colors.bg.muted,
|
|
197
|
+
zIndex: 0,
|
|
198
|
+
},
|
|
199
|
+
}),
|
|
200
|
+
[],
|
|
201
|
+
);
|
|
202
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { createSafeContext } from '@ledgerhq/lumen-utils-shared';
|
|
2
|
+
|
|
3
|
+
export type SegmentedControlContextValue = {
|
|
4
|
+
selectedValue: string;
|
|
5
|
+
onSelectedChange: (value: string) => void;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const [SegmentedControlContextProvider, _useSegmentedControlSafeContext] =
|
|
9
|
+
createSafeContext<SegmentedControlContextValue>('SegmentedControl');
|
|
10
|
+
|
|
11
|
+
export const useSegmentedControlContext = () =>
|
|
12
|
+
_useSegmentedControlSafeContext({
|
|
13
|
+
consumerName: 'SegmentedControlButton',
|
|
14
|
+
contextRequired: true,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export { SegmentedControlContextProvider };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { ComponentType, ReactNode } from 'react';
|
|
2
|
+
import { StyledPressableProps } from '../../../styles';
|
|
3
|
+
import { IconSize } from '../Icon';
|
|
4
|
+
import { BoxProps } from '../Utility';
|
|
5
|
+
|
|
6
|
+
export type SegmentedControlProps = {
|
|
7
|
+
/**
|
|
8
|
+
* The value of the currently selected segment (drives the sliding pill).
|
|
9
|
+
*/
|
|
10
|
+
selectedValue: string;
|
|
11
|
+
/**
|
|
12
|
+
* Callback when the selected segment value changes.
|
|
13
|
+
*/
|
|
14
|
+
onSelectedChange: (value: string) => void;
|
|
15
|
+
/**
|
|
16
|
+
* Accessible label for the control (e.g. "File view").
|
|
17
|
+
*/
|
|
18
|
+
accessibilityLabel?: string;
|
|
19
|
+
/**
|
|
20
|
+
* Segment buttons (SegmentedControlButton). Can be wrapped (e.g. in Tooltip).
|
|
21
|
+
*/
|
|
22
|
+
children: ReactNode;
|
|
23
|
+
} & Omit<BoxProps, 'children'>;
|
|
24
|
+
|
|
25
|
+
type IconComponent = ComponentType<{
|
|
26
|
+
size?: IconSize;
|
|
27
|
+
}>;
|
|
28
|
+
|
|
29
|
+
export type SegmentedControlButtonProps = {
|
|
30
|
+
/**
|
|
31
|
+
* Value for this segment (must be unique among siblings).
|
|
32
|
+
*/
|
|
33
|
+
value: string;
|
|
34
|
+
/**
|
|
35
|
+
* Button label (e.g. "Preview", "Raw").
|
|
36
|
+
*/
|
|
37
|
+
children: ReactNode;
|
|
38
|
+
/**
|
|
39
|
+
* Optional icon shown to the left of the label (from Symbols).
|
|
40
|
+
*/
|
|
41
|
+
icon?: IconComponent;
|
|
42
|
+
/**
|
|
43
|
+
* Optional callback when the button is pressed (in addition to onSelectedChange on the parent).
|
|
44
|
+
*/
|
|
45
|
+
onPress?: () => void;
|
|
46
|
+
} & Omit<StyledPressableProps, 'children'>;
|
|
@@ -15,7 +15,7 @@ describe('createStylesheetTheme', () => {
|
|
|
15
15
|
});
|
|
16
16
|
|
|
17
17
|
it('should flatten heading/body typography tokens', () => {
|
|
18
|
-
jest.spyOn(RuntimeConstants, '
|
|
18
|
+
jest.spyOn(RuntimeConstants, 'isIOS', 'get').mockReturnValue(true);
|
|
19
19
|
const theme = ledgerLiveThemes.dark;
|
|
20
20
|
|
|
21
21
|
const result = createStylesheetTheme(theme);
|
|
@@ -9,8 +9,9 @@ describe('resolveTypographies', () => {
|
|
|
9
9
|
jest.restoreAllMocks();
|
|
10
10
|
});
|
|
11
11
|
|
|
12
|
-
it('should not modify fontFamily on
|
|
13
|
-
jest.spyOn(RuntimeConstants, '
|
|
12
|
+
it('should not modify fontFamily on iOS', () => {
|
|
13
|
+
jest.spyOn(RuntimeConstants, 'isIOS', 'get').mockReturnValue(true);
|
|
14
|
+
jest.spyOn(RuntimeConstants, 'isBrowser', 'get').mockReturnValue(false);
|
|
14
15
|
const theme = ledgerLiveThemes.dark;
|
|
15
16
|
|
|
16
17
|
const result = createStylesheetTheme(theme);
|
|
@@ -24,8 +25,9 @@ describe('resolveTypographies', () => {
|
|
|
24
25
|
);
|
|
25
26
|
});
|
|
26
27
|
|
|
27
|
-
it('should append weight suffix to fontFamily on
|
|
28
|
-
jest.spyOn(RuntimeConstants, '
|
|
28
|
+
it('should append weight suffix to fontFamily on Android', () => {
|
|
29
|
+
jest.spyOn(RuntimeConstants, 'isIOS', 'get').mockReturnValue(false);
|
|
30
|
+
jest.spyOn(RuntimeConstants, 'isBrowser', 'get').mockReturnValue(false);
|
|
29
31
|
const theme = ledgerLiveThemes.dark;
|
|
30
32
|
|
|
31
33
|
const result = createStylesheetTheme(theme);
|
|
@@ -44,8 +46,9 @@ describe('resolveTypographies', () => {
|
|
|
44
46
|
});
|
|
45
47
|
});
|
|
46
48
|
|
|
47
|
-
it('should not modify other typography properties on
|
|
48
|
-
jest.spyOn(RuntimeConstants, '
|
|
49
|
+
it('should not modify other typography properties on Android', () => {
|
|
50
|
+
jest.spyOn(RuntimeConstants, 'isIOS', 'get').mockReturnValue(false);
|
|
51
|
+
jest.spyOn(RuntimeConstants, 'isBrowser', 'get').mockReturnValue(false);
|
|
49
52
|
const theme = ledgerLiveThemes.dark;
|
|
50
53
|
|
|
51
54
|
const result = createStylesheetTheme(theme);
|
|
@@ -14,7 +14,7 @@ export const FONT_WEIGHT_SUFFIX_MAP = {
|
|
|
14
14
|
};
|
|
15
15
|
|
|
16
16
|
export const resolveFontWeights = (typographies: LumenTypographyTokens) => {
|
|
17
|
-
if (RuntimeConstants.
|
|
17
|
+
if (RuntimeConstants.isIOS || RuntimeConstants.isBrowser) {
|
|
18
18
|
return typographies;
|
|
19
19
|
}
|
|
20
20
|
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"Banner.figma.d.ts","sourceRoot":"","sources":["../../../../../src/lib/Components/Banner/Banner.figma.tsx"],"names":[],"mappings":""}
|