@ledgerhq/lumen-ui-rnative 0.1.31 → 0.1.32
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/AddressInput/AddressInput.mdx +10 -12
- package/dist/module/lib/Components/AddressInput/AddressInput.stories.js +2 -1
- package/dist/module/lib/Components/AddressInput/AddressInput.stories.js.map +1 -1
- package/dist/module/lib/Components/AmountInput/AmountInput.mdx +3 -3
- package/dist/module/lib/Components/BaseInput/BaseInput.js +60 -32
- package/dist/module/lib/Components/BaseInput/BaseInput.js.map +1 -1
- package/dist/module/lib/Components/DotCount/DotCount.stories.js +9 -18
- package/dist/module/lib/Components/DotCount/DotCount.stories.js.map +1 -1
- package/dist/module/lib/Components/DotIndicator/DotIndicator.stories.js +3 -5
- package/dist/module/lib/Components/DotIndicator/DotIndicator.stories.js.map +1 -1
- package/dist/module/lib/Components/MediaImage/MediaImage.test.js +7 -3
- package/dist/module/lib/Components/MediaImage/MediaImage.test.js.map +1 -1
- package/dist/module/lib/Components/SearchInput/SearchInput.mdx +4 -10
- package/dist/module/lib/Components/SearchInput/SearchInput.stories.js +2 -1
- package/dist/module/lib/Components/SearchInput/SearchInput.stories.js.map +1 -1
- package/dist/module/lib/Components/SegmentedControl/SegmentedControl.js +2 -1
- package/dist/module/lib/Components/SegmentedControl/SegmentedControl.js.map +1 -1
- package/dist/module/lib/Components/SegmentedControl/SegmentedControl.mdx +34 -4
- package/dist/module/lib/Components/SegmentedControl/SegmentedControl.stories.js +32 -7
- package/dist/module/lib/Components/SegmentedControl/SegmentedControl.stories.js.map +1 -1
- package/dist/module/lib/Components/SegmentedControl/SegmentedControl.test.js +26 -0
- package/dist/module/lib/Components/SegmentedControl/SegmentedControl.test.js.map +1 -1
- package/dist/module/lib/Components/TextInput/TextInput.js +4 -3
- package/dist/module/lib/Components/TextInput/TextInput.js.map +1 -1
- package/dist/module/lib/Components/TextInput/TextInput.mdx +8 -10
- package/dist/module/lib/Components/TextInput/TextInput.stories.js +40 -1
- package/dist/module/lib/Components/TextInput/TextInput.stories.js.map +1 -1
- package/dist/module/lib/Components/TextInput/TextInput.test.js +76 -0
- package/dist/module/lib/Components/TextInput/TextInput.test.js.map +1 -0
- package/dist/typescript/src/lib/Components/BaseInput/BaseInput.d.ts +1 -1
- package/dist/typescript/src/lib/Components/BaseInput/BaseInput.d.ts.map +1 -1
- package/dist/typescript/src/lib/Components/BaseInput/types.d.ts +9 -2
- package/dist/typescript/src/lib/Components/BaseInput/types.d.ts.map +1 -1
- package/dist/typescript/src/lib/Components/SegmentedControl/SegmentedControl.d.ts +1 -1
- package/dist/typescript/src/lib/Components/SegmentedControl/SegmentedControl.d.ts.map +1 -1
- package/dist/typescript/src/lib/Components/SegmentedControl/types.d.ts +4 -0
- package/dist/typescript/src/lib/Components/SegmentedControl/types.d.ts.map +1 -1
- package/dist/typescript/src/lib/Components/TextInput/TextInput.d.ts +4 -3
- package/dist/typescript/src/lib/Components/TextInput/TextInput.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/lib/Components/AddressInput/AddressInput.mdx +10 -12
- package/src/lib/Components/AddressInput/AddressInput.stories.tsx +2 -1
- package/src/lib/Components/AmountInput/AmountInput.mdx +3 -3
- package/src/lib/Components/BaseInput/BaseInput.tsx +61 -29
- package/src/lib/Components/BaseInput/types.ts +10 -2
- package/src/lib/Components/DotCount/DotCount.stories.tsx +12 -10
- package/src/lib/Components/DotIndicator/DotIndicator.stories.tsx +1 -3
- package/src/lib/Components/MediaImage/MediaImage.test.tsx +7 -3
- package/src/lib/Components/SearchInput/SearchInput.mdx +4 -10
- package/src/lib/Components/SearchInput/SearchInput.stories.tsx +2 -1
- package/src/lib/Components/SegmentedControl/SegmentedControl.mdx +34 -4
- package/src/lib/Components/SegmentedControl/SegmentedControl.stories.tsx +34 -6
- package/src/lib/Components/SegmentedControl/SegmentedControl.test.tsx +27 -0
- package/src/lib/Components/SegmentedControl/SegmentedControl.tsx +2 -0
- package/src/lib/Components/SegmentedControl/types.ts +4 -0
- package/src/lib/Components/TextInput/TextInput.mdx +8 -10
- package/src/lib/Components/TextInput/TextInput.stories.tsx +41 -1
- package/src/lib/Components/TextInput/TextInput.test.tsx +90 -0
- package/src/lib/Components/TextInput/TextInput.tsx +4 -3
|
@@ -53,6 +53,10 @@ export type SegmentedControlButtonProps = {
|
|
|
53
53
|
* Optional icon shown to the left of the label (from Symbols).
|
|
54
54
|
*/
|
|
55
55
|
icon?: IconComponent;
|
|
56
|
+
/**
|
|
57
|
+
* Optional content shown to the right of the label (e.g. DotCount badge).
|
|
58
|
+
*/
|
|
59
|
+
trailingContent?: ReactNode;
|
|
56
60
|
/**
|
|
57
61
|
* Optional callback when the button is pressed (in addition to onSelectedChange on the parent).
|
|
58
62
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../../../src/lib/Components/SegmentedControl/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACtD,OAAO,KAAK,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAC5E,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACxC,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE3C,MAAM,MAAM,qBAAqB,GAAG;IAClC;;OAEG;IACH,aAAa,EAAE,MAAM,CAAC;IACtB;;OAEG;IACH,gBAAgB,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1C;;OAEG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;OAGG;IACH,UAAU,CAAC,EAAE,YAAY,GAAG,eAAe,CAAC;IAC5C;;;;;OAKG;IACH,SAAS,CAAC,EAAE,KAAK,GAAG,OAAO,CAAC;IAC5B;;OAEG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B;;OAEG;IACH,QAAQ,EAAE,SAAS,CAAC;CACrB,GAAG,IAAI,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;AAE/B,KAAK,aAAa,GAAG,aAAa,CAAC;IACjC,IAAI,CAAC,EAAE,QAAQ,CAAC;IAChB,KAAK,CAAC,EAAE,cAAc,CAAC,OAAO,CAAC,CAAC;CACjC,CAAC,CAAC;AAEH,MAAM,MAAM,2BAA2B,GAAG;IACxC;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;OAEG;IACH,QAAQ,EAAE,SAAS,CAAC;IACpB;;OAEG;IACH,IAAI,CAAC,EAAE,aAAa,CAAC;IACrB;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;CACtB,GAAG,IAAI,CAAC,oBAAoB,EAAE,UAAU,CAAC,CAAC"}
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../../../src/lib/Components/SegmentedControl/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACtD,OAAO,KAAK,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAC5E,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACxC,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE3C,MAAM,MAAM,qBAAqB,GAAG;IAClC;;OAEG;IACH,aAAa,EAAE,MAAM,CAAC;IACtB;;OAEG;IACH,gBAAgB,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1C;;OAEG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;OAGG;IACH,UAAU,CAAC,EAAE,YAAY,GAAG,eAAe,CAAC;IAC5C;;;;;OAKG;IACH,SAAS,CAAC,EAAE,KAAK,GAAG,OAAO,CAAC;IAC5B;;OAEG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B;;OAEG;IACH,QAAQ,EAAE,SAAS,CAAC;CACrB,GAAG,IAAI,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;AAE/B,KAAK,aAAa,GAAG,aAAa,CAAC;IACjC,IAAI,CAAC,EAAE,QAAQ,CAAC;IAChB,KAAK,CAAC,EAAE,cAAc,CAAC,OAAO,CAAC,CAAC;CACjC,CAAC,CAAC;AAEH,MAAM,MAAM,2BAA2B,GAAG;IACxC;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;OAEG;IACH,QAAQ,EAAE,SAAS,CAAC;IACpB;;OAEG;IACH,IAAI,CAAC,EAAE,aAAa,CAAC;IACrB;;OAEG;IACH,eAAe,CAAC,EAAE,SAAS,CAAC;IAC5B;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;CACtB,GAAG,IAAI,CAAC,oBAAoB,EAAE,UAAU,CAAC,CAAC"}
|
|
@@ -6,7 +6,7 @@ import { type TextInputProps } from './types';
|
|
|
6
6
|
* - **Automatic clear button** appears when input has content
|
|
7
7
|
* - **Floating label** with smooth animations
|
|
8
8
|
* - **Suffix elements** for icons, buttons, or custom content
|
|
9
|
-
* - **
|
|
9
|
+
* - **Helper text** with optional `status` (`error` | `success`) for border and helper feedback styling
|
|
10
10
|
* - **Container-based spacing** with padding and gap for clean layout
|
|
11
11
|
* - **Flexible styling** via style props
|
|
12
12
|
* - **React Native TextInput** with proper mobile behavior
|
|
@@ -26,12 +26,13 @@ import { type TextInputProps } from './types';
|
|
|
26
26
|
* // Basic input with automatic clear button
|
|
27
27
|
* <TextInput label="Title" value={title} onChangeText={setTitle} />
|
|
28
28
|
*
|
|
29
|
-
* // Input with error
|
|
29
|
+
* // Input with error helper
|
|
30
30
|
* <TextInput
|
|
31
31
|
* label="Email"
|
|
32
32
|
* value={email}
|
|
33
33
|
* onChangeText={setEmail}
|
|
34
|
-
*
|
|
34
|
+
* helperText="Please enter a valid email address"
|
|
35
|
+
* status="error"
|
|
35
36
|
* />
|
|
36
37
|
*
|
|
37
38
|
* // Input with suffix element
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TextInput.d.ts","sourceRoot":"","sources":["../../../../../../src/lib/Components/TextInput/TextInput.tsx"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,SAAS,CAAC;AAE9C
|
|
1
|
+
{"version":3,"file":"TextInput.d.ts","sourceRoot":"","sources":["../../../../../../src/lib/Components/TextInput/TextInput.tsx"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,SAAS,CAAC;AAE9C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG;AACH,eAAO,MAAM,SAAS,GAAI,OAAO,cAAc,4CAE9C,CAAC"}
|
package/package.json
CHANGED
|
@@ -55,16 +55,10 @@ Use `hideClearButton` to prevent the clear button from appearing.
|
|
|
55
55
|
|
|
56
56
|
### Error State
|
|
57
57
|
|
|
58
|
-
The input supports error handling through `
|
|
58
|
+
The input supports error handling through `helperText` and `status` (`'error'` \| `'success'`), which show copy below the input with matching border and text styling.
|
|
59
59
|
|
|
60
60
|
<Canvas of={AddressInputStories.WithError} />
|
|
61
61
|
|
|
62
|
-
The error message will be automatically:
|
|
63
|
-
|
|
64
|
-
- Connected to the input
|
|
65
|
-
- Displayed with a warning icon
|
|
66
|
-
- Styled in the error color
|
|
67
|
-
- Announced by screen readers
|
|
68
62
|
|
|
69
63
|
### Disabled State
|
|
70
64
|
|
|
@@ -244,7 +238,8 @@ function MyComponent() {
|
|
|
244
238
|
placeholder='Enter address or ENS'
|
|
245
239
|
value={address}
|
|
246
240
|
onChangeText={setAddress}
|
|
247
|
-
|
|
241
|
+
helperText={error || undefined}
|
|
242
|
+
status={error ? 'error' : undefined}
|
|
248
243
|
onClear={() => {
|
|
249
244
|
// Default clearing happens automatically
|
|
250
245
|
// Add your custom logic here
|
|
@@ -289,14 +284,15 @@ function MyComponent() {
|
|
|
289
284
|
placeholder='Enter address or ENS'
|
|
290
285
|
value={address}
|
|
291
286
|
onChangeText={handleChange}
|
|
292
|
-
|
|
287
|
+
helperText={error || undefined}
|
|
288
|
+
status={error ? 'error' : undefined}
|
|
293
289
|
onQrCodeClick={() => openQrScanner()}
|
|
294
290
|
/>
|
|
295
291
|
);
|
|
296
292
|
}
|
|
297
293
|
```
|
|
298
294
|
|
|
299
|
-
> **Note:**
|
|
295
|
+
> **Note:** Helper text is optional. Use `helperText` with `status="error"` to show validation feedback below the input.
|
|
300
296
|
|
|
301
297
|
### With Custom Styling
|
|
302
298
|
|
|
@@ -387,7 +383,8 @@ function MyComponent() {
|
|
|
387
383
|
value={address}
|
|
388
384
|
onChangeText={setAddress}
|
|
389
385
|
onQrCodeClick={handleQrScan}
|
|
390
|
-
|
|
386
|
+
helperText={error || undefined}
|
|
387
|
+
status={error ? 'error' : undefined}
|
|
391
388
|
/>
|
|
392
389
|
);
|
|
393
390
|
}
|
|
@@ -412,7 +409,8 @@ function TransactionForm() {
|
|
|
412
409
|
placeholder='Enter address or ENS'
|
|
413
410
|
value={recipientAddress}
|
|
414
411
|
onChangeText={setRecipientAddress}
|
|
415
|
-
|
|
412
|
+
helperText={addressError || undefined}
|
|
413
|
+
status={addressError ? 'error' : undefined}
|
|
416
414
|
onQrCodeClick={() => openQrScanner()}
|
|
417
415
|
style={{ maxWidth: 320 }}
|
|
418
416
|
/>
|
|
@@ -119,7 +119,8 @@ export const WithError: Story = {
|
|
|
119
119
|
args: {
|
|
120
120
|
placeholder: 'Enter address or ENS',
|
|
121
121
|
value: 'invalid-address',
|
|
122
|
-
|
|
122
|
+
helperText: 'Invalid address format',
|
|
123
|
+
status: 'error',
|
|
123
124
|
prefix: 'To:',
|
|
124
125
|
editable: true,
|
|
125
126
|
hideClearButton: false,
|
|
@@ -385,7 +385,7 @@ const [hasError, setHasError] = useState(false);
|
|
|
385
385
|
</DoBlockItem>
|
|
386
386
|
<DontBlockItem
|
|
387
387
|
title="Don't use custom error props"
|
|
388
|
-
description='Use isInvalid prop
|
|
388
|
+
description='Use the isInvalid prop for error styling. AmountInput does not accept ad-hoc validation props.'
|
|
389
389
|
>
|
|
390
390
|
|
|
391
391
|
{/* prettier-ignore */}
|
|
@@ -400,8 +400,8 @@ const [hasError, setHasError] = useState(false);
|
|
|
400
400
|
value={value}
|
|
401
401
|
onChangeText={setValue}
|
|
402
402
|
currencyText='USD'
|
|
403
|
-
|
|
404
|
-
|
|
403
|
+
status={hasError ? 'error' : undefined}
|
|
404
|
+
helperText={hasError ? 'Invalid amount' : undefined}
|
|
405
405
|
/>
|
|
406
406
|
```
|
|
407
407
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
DisabledProvider,
|
|
3
|
+
resolveBaseInputPlaceholder,
|
|
3
4
|
useDisabledContext,
|
|
4
5
|
} from '@ledgerhq/lumen-utils-shared';
|
|
5
6
|
import {
|
|
@@ -21,6 +22,7 @@ import { useCommonTranslation } from '../../../i18n';
|
|
|
21
22
|
import type { LumenStyleSheetTheme } from '../../../styles';
|
|
22
23
|
import { useStyleSheet, useTheme } from '../../../styles';
|
|
23
24
|
import { useTimingConfig } from '../../Animations/useTimingConfig';
|
|
25
|
+
import { CheckmarkCircleFill } from '../../Symbols/Icons/CheckmarkCircleFill';
|
|
24
26
|
import { DeleteCircleFill } from '../../Symbols/Icons/DeleteCircleFill';
|
|
25
27
|
import { RuntimeConstants } from '../../utils';
|
|
26
28
|
import { InteractiveIcon } from '../InteractiveIcon';
|
|
@@ -34,7 +36,8 @@ export const BaseInput = ({
|
|
|
34
36
|
inputStyle,
|
|
35
37
|
labelStyle,
|
|
36
38
|
label,
|
|
37
|
-
|
|
39
|
+
helperText,
|
|
40
|
+
status,
|
|
38
41
|
hideClearButton,
|
|
39
42
|
onChangeText: onChangeTextProp,
|
|
40
43
|
editable,
|
|
@@ -42,6 +45,7 @@ export const BaseInput = ({
|
|
|
42
45
|
prefix,
|
|
43
46
|
suffix,
|
|
44
47
|
ref,
|
|
48
|
+
placeholder: placeholderProp,
|
|
45
49
|
...props
|
|
46
50
|
}: BaseInputProps) => {
|
|
47
51
|
const disabled = useDisabledContext({
|
|
@@ -65,6 +69,12 @@ export const BaseInput = ({
|
|
|
65
69
|
? !!props.value && props.value.length > 0
|
|
66
70
|
: uncontrolledValue.length > 0;
|
|
67
71
|
|
|
72
|
+
const { inputPlaceholder, labelStaysFloatedWithPlaceholder } =
|
|
73
|
+
resolveBaseInputPlaceholder({
|
|
74
|
+
label,
|
|
75
|
+
placeholder: placeholderProp,
|
|
76
|
+
});
|
|
77
|
+
|
|
68
78
|
const showClearButton = hasContent && !disabled && !hideClearButton;
|
|
69
79
|
|
|
70
80
|
const handleChangeText = useCallback(
|
|
@@ -87,7 +97,7 @@ export const BaseInput = ({
|
|
|
87
97
|
};
|
|
88
98
|
|
|
89
99
|
const styles = useStyles({
|
|
90
|
-
|
|
100
|
+
status,
|
|
91
101
|
isFocused,
|
|
92
102
|
isEditable: !disabled,
|
|
93
103
|
hasLabel: !!label,
|
|
@@ -97,8 +107,9 @@ export const BaseInput = ({
|
|
|
97
107
|
hasContent,
|
|
98
108
|
isFocused,
|
|
99
109
|
showClearButton,
|
|
100
|
-
|
|
110
|
+
status,
|
|
101
111
|
isEditable: !disabled,
|
|
112
|
+
labelStaysFloatedWithPlaceholder,
|
|
102
113
|
});
|
|
103
114
|
|
|
104
115
|
return (
|
|
@@ -114,6 +125,7 @@ export const BaseInput = ({
|
|
|
114
125
|
<TextInput
|
|
115
126
|
ref={inputRef}
|
|
116
127
|
value={value}
|
|
128
|
+
placeholder={inputPlaceholder}
|
|
117
129
|
style={StyleSheet.flatten([styles.input, inputStyle])}
|
|
118
130
|
onFocus={() => setIsFocused(true)}
|
|
119
131
|
onBlur={() => setIsFocused(false)}
|
|
@@ -158,10 +170,13 @@ export const BaseInput = ({
|
|
|
158
170
|
)}
|
|
159
171
|
</Pressable>
|
|
160
172
|
|
|
161
|
-
{
|
|
162
|
-
<View style={styles.
|
|
163
|
-
<DeleteCircleFill size={16} color='error' />
|
|
164
|
-
|
|
173
|
+
{!!helperText && (
|
|
174
|
+
<View style={styles.helperContainer}>
|
|
175
|
+
{status === 'error' && <DeleteCircleFill size={16} color='error' />}
|
|
176
|
+
{status === 'success' && (
|
|
177
|
+
<CheckmarkCircleFill size={16} color='success' />
|
|
178
|
+
)}
|
|
179
|
+
<Text style={styles.helperText}>{helperText}</Text>
|
|
165
180
|
</View>
|
|
166
181
|
)}
|
|
167
182
|
</Box>
|
|
@@ -170,18 +185,25 @@ export const BaseInput = ({
|
|
|
170
185
|
};
|
|
171
186
|
|
|
172
187
|
const useStyles = ({
|
|
173
|
-
|
|
188
|
+
status,
|
|
174
189
|
isFocused,
|
|
175
190
|
isEditable,
|
|
176
191
|
hasLabel,
|
|
177
192
|
}: {
|
|
178
|
-
|
|
193
|
+
status: 'error' | 'success' | undefined;
|
|
179
194
|
isFocused: boolean;
|
|
180
195
|
isEditable: boolean;
|
|
181
196
|
hasLabel: boolean;
|
|
182
197
|
}) => {
|
|
183
198
|
return useStyleSheet(
|
|
184
199
|
(t) => {
|
|
200
|
+
const hasStatusBorder = status === 'error' || status === 'success';
|
|
201
|
+
const statusBorderColors = {
|
|
202
|
+
error: t.colors.border.error,
|
|
203
|
+
success: t.colors.border.success,
|
|
204
|
+
} as const;
|
|
205
|
+
const statusBorderColor = status ? statusBorderColors[status] : undefined;
|
|
206
|
+
|
|
185
207
|
return {
|
|
186
208
|
container: StyleSheet.flatten([
|
|
187
209
|
{
|
|
@@ -198,15 +220,16 @@ const useStyles = ({
|
|
|
198
220
|
borderColor: 'transparent',
|
|
199
221
|
overflow: 'hidden',
|
|
200
222
|
},
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
223
|
+
hasStatusBorder &&
|
|
224
|
+
statusBorderColor && {
|
|
225
|
+
borderWidth: isFocused ? t.borderWidth.s2 : t.borderWidth.s1,
|
|
226
|
+
borderColor: statusBorderColor,
|
|
227
|
+
},
|
|
205
228
|
!isEditable && {
|
|
206
229
|
backgroundColor: t.colors.bg.disabled,
|
|
207
230
|
},
|
|
208
231
|
isFocused &&
|
|
209
|
-
!
|
|
232
|
+
!hasStatusBorder &&
|
|
210
233
|
isEditable && { borderColor: t.colors.border.active },
|
|
211
234
|
]),
|
|
212
235
|
input: StyleSheet.flatten([
|
|
@@ -217,31 +240,37 @@ const useStyles = ({
|
|
|
217
240
|
color: t.colors.text.base,
|
|
218
241
|
backgroundColor: t.colors.bg.muted,
|
|
219
242
|
outline: 'none',
|
|
220
|
-
...t.typographies.
|
|
221
|
-
paddingTop: t.spacings.
|
|
222
|
-
paddingBottom: t.spacings.
|
|
243
|
+
...t.typographies.body1,
|
|
244
|
+
paddingTop: t.spacings.s4,
|
|
245
|
+
paddingBottom: t.spacings.s2,
|
|
223
246
|
},
|
|
224
247
|
hasLabel && {
|
|
225
248
|
paddingTop: t.spacings.s20,
|
|
226
249
|
paddingBottom: t.spacings.s4,
|
|
227
250
|
paddingHorizontal: 0,
|
|
251
|
+
...t.typographies.body2,
|
|
228
252
|
},
|
|
229
|
-
RuntimeConstants.isIOS &&
|
|
253
|
+
RuntimeConstants.isIOS && { lineHeight: 0 },
|
|
230
254
|
RuntimeConstants.isAndroid && { includeFontPadding: false },
|
|
231
255
|
!isEditable && {
|
|
232
256
|
backgroundColor: t.colors.bg.disabled,
|
|
233
257
|
color: t.colors.text.disabled,
|
|
234
258
|
},
|
|
235
259
|
]),
|
|
236
|
-
|
|
260
|
+
helperContainer: {
|
|
237
261
|
marginTop: t.spacings.s8,
|
|
238
262
|
flexDirection: 'row',
|
|
239
263
|
alignItems: 'center',
|
|
240
264
|
gap: t.spacings.s2,
|
|
241
265
|
},
|
|
242
|
-
|
|
243
|
-
color: t.colors.text.error,
|
|
266
|
+
helperText: {
|
|
244
267
|
...t.typographies.body3,
|
|
268
|
+
flex: 1,
|
|
269
|
+
color: {
|
|
270
|
+
error: t.colors.text.error,
|
|
271
|
+
success: t.colors.text.success,
|
|
272
|
+
default: t.colors.text.muted,
|
|
273
|
+
}[status ?? 'default'],
|
|
245
274
|
},
|
|
246
275
|
suffixContainer: {
|
|
247
276
|
minWidth: t.sizes.s20,
|
|
@@ -250,7 +279,7 @@ const useStyles = ({
|
|
|
250
279
|
},
|
|
251
280
|
};
|
|
252
281
|
},
|
|
253
|
-
[
|
|
282
|
+
[status, isFocused, isEditable, hasLabel],
|
|
254
283
|
);
|
|
255
284
|
};
|
|
256
285
|
|
|
@@ -296,14 +325,16 @@ const useFloatingLabelStyles = ({
|
|
|
296
325
|
isFocused,
|
|
297
326
|
hasContent,
|
|
298
327
|
showClearButton,
|
|
299
|
-
|
|
328
|
+
status,
|
|
300
329
|
isEditable,
|
|
330
|
+
labelStaysFloatedWithPlaceholder,
|
|
301
331
|
}: {
|
|
302
332
|
isFocused: boolean;
|
|
303
333
|
hasContent: boolean;
|
|
304
334
|
showClearButton: boolean;
|
|
305
|
-
|
|
335
|
+
status: 'error' | 'success' | undefined;
|
|
306
336
|
isEditable: boolean;
|
|
337
|
+
labelStaysFloatedWithPlaceholder: boolean;
|
|
307
338
|
}) => {
|
|
308
339
|
const { theme } = useTheme();
|
|
309
340
|
|
|
@@ -320,20 +351,21 @@ const useFloatingLabelStyles = ({
|
|
|
320
351
|
showClearButton && {
|
|
321
352
|
width: '92%',
|
|
322
353
|
},
|
|
354
|
+
status === 'error' && {
|
|
355
|
+
color: t.colors.text.error,
|
|
356
|
+
},
|
|
323
357
|
!isEditable && {
|
|
324
358
|
color: t.colors.text.disabled,
|
|
325
359
|
},
|
|
326
|
-
hasError && {
|
|
327
|
-
color: t.colors.text.error,
|
|
328
|
-
},
|
|
329
360
|
]),
|
|
330
361
|
}),
|
|
331
|
-
[hasContent, showClearButton,
|
|
362
|
+
[hasContent, showClearButton, status, isEditable],
|
|
332
363
|
);
|
|
333
364
|
|
|
334
365
|
const { animatedStyle } = useAnimatedFloatingLabel({
|
|
335
366
|
theme,
|
|
336
|
-
isFloatingLabel:
|
|
367
|
+
isFloatingLabel:
|
|
368
|
+
isFocused || hasContent || labelStaysFloatedWithPlaceholder,
|
|
337
369
|
});
|
|
338
370
|
|
|
339
371
|
return { label, animatedStyle };
|
|
@@ -6,6 +6,8 @@ import type {
|
|
|
6
6
|
} from 'react-native';
|
|
7
7
|
import type { BoxProps } from '../Utility';
|
|
8
8
|
|
|
9
|
+
export type BaseInputStatus = 'error' | 'success';
|
|
10
|
+
|
|
9
11
|
export type BaseInputProps = {
|
|
10
12
|
/**
|
|
11
13
|
* The label text that floats above the input when focused or filled.
|
|
@@ -35,9 +37,15 @@ export type BaseInputProps = {
|
|
|
35
37
|
*/
|
|
36
38
|
labelStyle?: StyleProp<TextStyle>;
|
|
37
39
|
/**
|
|
38
|
-
*
|
|
40
|
+
* Optional text shown below the input (hint, error, or success copy).
|
|
41
|
+
* Pair with `status` for error/success styling and icons; omit `status` for a neutral hint.
|
|
42
|
+
*/
|
|
43
|
+
helperText?: string;
|
|
44
|
+
/**
|
|
45
|
+
* Visual state for border, helper text, helper icon, and the label in error state.
|
|
46
|
+
* Omit when `helperText` is a neutral hint.
|
|
39
47
|
*/
|
|
40
|
-
|
|
48
|
+
status?: BaseInputStatus;
|
|
41
49
|
/**
|
|
42
50
|
* Custom content to render after the input (right side in LTR).
|
|
43
51
|
* @example suffix={<Icon />}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from '@storybook/react-native-web-vite';
|
|
2
2
|
import { useState } from 'react';
|
|
3
|
-
import { Avatar } from '../Avatar/Avatar';
|
|
4
3
|
import { MediaImage } from '../MediaImage/MediaImage';
|
|
5
4
|
import {
|
|
6
5
|
SegmentedControl,
|
|
@@ -89,7 +88,14 @@ export const WithChildren: Story = {
|
|
|
89
88
|
|
|
90
89
|
return (
|
|
91
90
|
<Box lx={{ gap: 's24' }}>
|
|
92
|
-
<Box
|
|
91
|
+
<Box
|
|
92
|
+
lx={{
|
|
93
|
+
flexDirection: 'row',
|
|
94
|
+
alignItems: 'center',
|
|
95
|
+
justifyContent: 'center',
|
|
96
|
+
gap: 's12',
|
|
97
|
+
}}
|
|
98
|
+
>
|
|
93
99
|
<DotCount value={5} size='md'>
|
|
94
100
|
<MediaImage
|
|
95
101
|
src='https://crypto-icons.ledger.com/BTC.png'
|
|
@@ -98,12 +104,6 @@ export const WithChildren: Story = {
|
|
|
98
104
|
shape='circle'
|
|
99
105
|
/>
|
|
100
106
|
</DotCount>
|
|
101
|
-
<DotCount value={100} size='md'>
|
|
102
|
-
<Avatar
|
|
103
|
-
src='https://plus.unsplash.com/premium_photo-1689551670902-19b441a6afde?q=80&w=774&auto=format&fit=crop'
|
|
104
|
-
size='md'
|
|
105
|
-
/>
|
|
106
|
-
</DotCount>
|
|
107
107
|
</Box>
|
|
108
108
|
<SegmentedControl
|
|
109
109
|
selectedValue={fitState}
|
|
@@ -111,9 +111,11 @@ export const WithChildren: Story = {
|
|
|
111
111
|
tabLayout='fit'
|
|
112
112
|
accessibilityLabel='Fit layout'
|
|
113
113
|
>
|
|
114
|
-
<SegmentedControlButton
|
|
114
|
+
<SegmentedControlButton
|
|
115
|
+
value='preview'
|
|
116
|
+
trailingContent={<DotCount value={3} size='md' />}
|
|
117
|
+
>
|
|
115
118
|
Preview
|
|
116
|
-
<DotCount value={3} size='md' style={{ marginLeft: 6 }} />
|
|
117
119
|
</SegmentedControlButton>
|
|
118
120
|
<SegmentedControlButton value='raw'>Raw</SegmentedControlButton>
|
|
119
121
|
<SegmentedControlButton value='blame'>Blame</SegmentedControlButton>
|
|
@@ -66,9 +66,7 @@ export const WithChildren: Story = {
|
|
|
66
66
|
<DotIndicator appearance='red'>
|
|
67
67
|
<Button size='sm'>Submit</Button>
|
|
68
68
|
</DotIndicator>
|
|
69
|
-
<
|
|
70
|
-
<Avatar size='md' />
|
|
71
|
-
</DotIndicator>
|
|
69
|
+
<Avatar size='md' showNotification />
|
|
72
70
|
<DotIndicator appearance='red'>
|
|
73
71
|
<IconButton accessibilityLabel='Settings' icon={Settings} />
|
|
74
72
|
</DotIndicator>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect } from '@jest/globals';
|
|
2
2
|
import { ledgerLiveThemes } from '@ledgerhq/lumen-design-core';
|
|
3
|
-
import { render, waitFor } from '@testing-library/react-native';
|
|
3
|
+
import { act, render, waitFor } from '@testing-library/react-native';
|
|
4
4
|
import { ThemeProvider } from '../ThemeProvider/ThemeProvider';
|
|
5
5
|
import { MediaImage } from './MediaImage';
|
|
6
6
|
|
|
@@ -89,7 +89,9 @@ describe('MediaImage Component', () => {
|
|
|
89
89
|
);
|
|
90
90
|
|
|
91
91
|
const img = getByTestId('media-image-img');
|
|
92
|
-
|
|
92
|
+
act(() => {
|
|
93
|
+
img.props.onError();
|
|
94
|
+
});
|
|
93
95
|
|
|
94
96
|
rerender(
|
|
95
97
|
<TestWrapper>
|
|
@@ -140,7 +142,9 @@ describe('MediaImage Component', () => {
|
|
|
140
142
|
);
|
|
141
143
|
|
|
142
144
|
const img = getByTestId('media-image-img');
|
|
143
|
-
|
|
145
|
+
act(() => {
|
|
146
|
+
img.props.onError();
|
|
147
|
+
});
|
|
144
148
|
|
|
145
149
|
rerender(
|
|
146
150
|
<TestWrapper>
|
|
@@ -67,17 +67,10 @@ Alternatively, use `editable={false}` to prevent editing without applying the mu
|
|
|
67
67
|
|
|
68
68
|
### Error State
|
|
69
69
|
|
|
70
|
-
The search component supports error handling through `
|
|
70
|
+
The search component supports error handling through `helperText` and `status` (`'error'` \| `'success'`), which show copy below the input with matching border and text styling.
|
|
71
71
|
|
|
72
72
|
<Canvas of={SearchInputStories.WithError} />
|
|
73
73
|
|
|
74
|
-
The error message will be automatically:
|
|
75
|
-
|
|
76
|
-
- Connected to the input
|
|
77
|
-
- Displayed with a warning icon
|
|
78
|
-
- Styled in the error color
|
|
79
|
-
- Announced by screen readers
|
|
80
|
-
|
|
81
74
|
## Controlled vs Uncontrolled
|
|
82
75
|
|
|
83
76
|
The SearchInput component supports both controlled and uncontrolled usage.
|
|
@@ -232,13 +225,14 @@ function MyComponent() {
|
|
|
232
225
|
placeholder='Search products'
|
|
233
226
|
value={query}
|
|
234
227
|
onChangeText={handleSearch}
|
|
235
|
-
|
|
228
|
+
helperText={error || undefined}
|
|
229
|
+
status={error ? 'error' : undefined}
|
|
236
230
|
/>
|
|
237
231
|
);
|
|
238
232
|
}
|
|
239
233
|
```
|
|
240
234
|
|
|
241
|
-
> **Note:**
|
|
235
|
+
> **Note:** Helper text is optional. Use `helperText` with `status="error"` to show validation feedback below the search input.
|
|
242
236
|
|
|
243
237
|
### With Custom Styling
|
|
244
238
|
|
|
@@ -25,6 +25,7 @@ SegmentedControl is a tab bar–style component for switching between mutually e
|
|
|
25
25
|
- **Segments**: Individual options the user can select.
|
|
26
26
|
- **Selected state**: The active segment (sliding pill + semi-bold label).
|
|
27
27
|
- **Optional icon**: Icon to the left of the label (from Symbols).
|
|
28
|
+
- **Optional trailing content**: Any node (e.g. `DotCount`) rendered to the right of the label.
|
|
28
29
|
- **Appearance**: Use `appearance="background"` (default) for a surface background, or `appearance="no-background"` for a transparent container.
|
|
29
30
|
|
|
30
31
|
## Properties
|
|
@@ -40,6 +41,12 @@ You can use segments with text only (Base), or add an icon to each button (WithI
|
|
|
40
41
|
|
|
41
42
|
<Canvas of={SegmentedControlStories.WithIcons} />
|
|
42
43
|
|
|
44
|
+
### With trailing content
|
|
45
|
+
|
|
46
|
+
Use `trailingContent` to render content (e.g. `DotCount`) to the right of the label.
|
|
47
|
+
|
|
48
|
+
<Canvas of={SegmentedControlStories.WithTrailingContent} />
|
|
49
|
+
|
|
43
50
|
## Responsive Layout
|
|
44
51
|
|
|
45
52
|
SegmentedControl lays out segments in a horizontal row with equal width per segment. The sliding pill animates to the selected segment.
|
|
@@ -80,6 +87,33 @@ export default function Example() {
|
|
|
80
87
|
}
|
|
81
88
|
```
|
|
82
89
|
|
|
90
|
+
## With trailing content
|
|
91
|
+
|
|
92
|
+
Pass any node via `trailingContent` to render it to the right of the label. Use it for badges like `DotCount` to surface counts per segment.
|
|
93
|
+
|
|
94
|
+
```tsx
|
|
95
|
+
import { SegmentedControl, SegmentedControlButton, DotCount } from '@ledgerhq/lumen-ui-rnative';
|
|
96
|
+
|
|
97
|
+
function Example() {
|
|
98
|
+
const [state, setState] = React.useState('tokens');
|
|
99
|
+
return (
|
|
100
|
+
<SegmentedControl
|
|
101
|
+
selectedValue={state}
|
|
102
|
+
onSelectedChange={setState}
|
|
103
|
+
accessibilityLabel='Asset section'
|
|
104
|
+
>
|
|
105
|
+
<SegmentedControlButton value='tokens' trailingContent={<DotCount value={3} />}>
|
|
106
|
+
Tokens
|
|
107
|
+
</SegmentedControlButton>
|
|
108
|
+
<SegmentedControlButton value='nfts' trailingContent={<DotCount value={12} />}>
|
|
109
|
+
NFTs
|
|
110
|
+
</SegmentedControlButton>
|
|
111
|
+
<SegmentedControlButton value='activity'>Activity</SegmentedControlButton>
|
|
112
|
+
</SegmentedControl>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
83
117
|
## With icons
|
|
84
118
|
|
|
85
119
|
Pass an icon from symbols to each button for a left-positioned icon. Use icons on all segments or none for consistency.
|
|
@@ -113,9 +147,5 @@ function Example() {
|
|
|
113
147
|
}
|
|
114
148
|
```
|
|
115
149
|
|
|
116
|
-
<Box lx={{ flexDirection: 'column', gap: 's24' }}>
|
|
117
|
-
<CommonRulesDoAndDont />
|
|
118
|
-
</Box>
|
|
119
|
-
|
|
120
150
|
</Tab>
|
|
121
151
|
</CustomTabs>
|