Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
fbbf909
initial draft
aquelemiguel Oct 29, 2025
3032f96
position clear button correctly
aquelemiguel Oct 29, 2025
4364a14
use pressable instead of touchableopacity
aquelemiguel Oct 31, 2025
aa64f0a
add animation for lable
aquelemiguel Oct 31, 2025
087312a
add containerclassname
aquelemiguel Oct 31, 2025
cea5e81
some more introp
aquelemiguel Oct 31, 2025
b52c160
fix ring not existing
aquelemiguel Oct 31, 2025
af72e49
input animation tweaks
aquelemiguel Oct 31, 2025
e8e5a28
patch spacing bc of border-2
aquelemiguel Oct 31, 2025
4e61068
add platform specific spacing for web
aquelemiguel Nov 3, 2025
ca19672
fix ref issue
aquelemiguel Nov 3, 2025
86c8685
use text-base for clear icon
aquelemiguel Nov 3, 2025
e660653
simplify translates
aquelemiguel Nov 3, 2025
51e5dc7
move disabled styles conditionally
aquelemiguel Nov 3, 2025
6ce285b
slight border fix
aquelemiguel Nov 3, 2025
5a07dd0
simplify styles further
aquelemiguel Nov 3, 2025
de1ff9a
just use react-native-reanimated
aquelemiguel Nov 3, 2025
540fc04
truncate
aquelemiguel Nov 3, 2025
c2b2e44
better sandbox
aquelemiguel Nov 3, 2025
ece473d
add suffix support
aquelemiguel Nov 3, 2025
f05b1d2
generate docs
aquelemiguel Nov 3, 2025
8968598
Merge branch 'main' into dls-348
aquelemiguel Nov 3, 2025
898fbc9
Merge branch 'main' into dls-348
aquelemiguel Nov 4, 2025
ee0e1f0
remove pressable style callback
aquelemiguel Nov 4, 2025
d86e071
move containerClassName to parent container
aquelemiguel Nov 4, 2025
4fd6303
use text-muted for clear circle
aquelemiguel Nov 4, 2025
64e0b67
rename containerClassName -> className
aquelemiguel Nov 5, 2025
45b41d7
omit non-classnames
aquelemiguel Nov 5, 2025
9f0be3a
use valid classname
aquelemiguel Nov 5, 2025
37590bb
no styling input in the case of textinput
aquelemiguel Nov 5, 2025
aaa0e08
only push docs
aquelemiguel Nov 6, 2025
1175641
add in construction wording
aquelemiguel Nov 6, 2025
126dbd9
more docs
aquelemiguel Nov 6, 2025
d86ea5c
add version plan
aquelemiguel Nov 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .nx/version-plans/version-plan-1762435410704.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@ledgerhq/ldls-ui-rnative': patch
'@ledgerhq/ldls-ui-react': patch
---

feat(ui-rnative): add BaseInput
6 changes: 5 additions & 1 deletion apps/app-sandbox-rnative/src/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { SafeAreaView, ScrollView, StatusBar, View } from 'react-native';
import { Buttons } from './blocks/Buttons';
import { Checkboxes } from './blocks/Checkboxes';
import { IconButtons } from './blocks/IconButtons';
import { Inputs } from './blocks/Inputs';
import { Switches } from './blocks/Switches';
import { ToggleThemeSwitch } from './blocks/ToggleThemeSwitch';
import { SandboxBlock } from './SandboxBlock';
Expand All @@ -14,9 +15,12 @@ export const App = () => {
<ThemeProvider defaultMode='dark' className='flex flex-1'>
<ScrollView
contentInsetAdjustmentBehavior='automatic'
className='bg-canvas-sheet h-screen px-16 '
className='h-screen bg-canvas-sheet px-16'
>
<View className='flex flex-col gap-32 py-40'>
<SandboxBlock title='Inputs'>
<Inputs />
</SandboxBlock>
<SandboxBlock title='Checkboxes'>
<Checkboxes />
</SandboxBlock>
Expand Down
68 changes: 68 additions & 0 deletions apps/app-sandbox-rnative/src/app/blocks/Inputs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { TextInput } from '@ledgerhq/ldls-ui-rnative';
import { Eye, EyeCross } from '@ledgerhq/ldls-ui-rnative/symbols';
import { useEffect, useState } from 'react';
import { Alert, Pressable, View } from 'react-native';

export function Inputs() {
const [team, setTeam] = useState<string>();
const [isTeamValid, setIsTeamValid] = useState(true);

useEffect(() => {
if (team) {
setIsTeamValid(team.toLowerCase() === 'ldls');
}
}, [team]);

const [showPassword, setShowPassword] = useState(false);

return (
<View className='gap-8'>
<TextInput
label='Username'
className='min-w-full'
onClear={() =>
Alert.alert('Custom handler', 'You found an easter egg!', [
{ text: 'Okay', style: 'default' },
])
}
/>
<TextInput
label='Password'
className='min-w-full'
secureTextEntry={!showPassword}
hideClearButton
suffix={
<Pressable onPress={() => setShowPassword(!showPassword)}>
{showPassword ? (
<EyeCross size={20} className='text-base' />
) : (
<Eye size={20} className='text-base' />
)}
</Pressable>
}
/>
<TextInput
label='Company'
className='min-w-full'
defaultValue='Ledger'
editable={false}
/>
<TextInput
label='Team'
className='min-w-full'
value={team}
onChangeText={setTeam}
errorMessage={
!isTeamValid && team !== undefined
? 'Team must match "ldls"!'
: undefined
}
/>
<TextInput
label='A very long label that should really be truncated at different breakpoints'
className='min-w-full'
defaultValue='This is a default value!'
/>
</View>
);
}
41 changes: 21 additions & 20 deletions libs/ui-react/src/lib/Components/AddressInput/AddressInput.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { CustomTabs, Tab } from '../../../../.storybook/CustomTabs';

The AddressInput component provides a specialized input field for entering cryptocurrency addresses or ENS names. Built on top of BaseInput, it features a customizable prefix label (defaults to "To:") and context-aware suffix icons.

> **Layout:** The AddressInput component takes the full width of its container by default. You can control the width directly using `containerClassName` or by wrapping in a container.
> **Layout:** The AddressInput component takes the full width of its container by default. You can control the width directly using `className` or by wrapping in a container.

## Introduction

Expand Down Expand Up @@ -89,10 +89,10 @@ import { AddressInput } from '@ledgerhq/ldls-ui-react';

// Basic usage
<AddressInput
placeholder="Enter address or ENS"
placeholder='Enter address or ENS'
value={address}
onChange={(e) => setAddress(e.target.value)}
/>
/>;
```

## Key Features
Expand Down Expand Up @@ -156,7 +156,8 @@ const [error, setError] = React.useState('');

React.useEffect(() => {
if (address.length > 0) {
const isValidEthereumAddress = address.startsWith('0x') && address.length === 42;
const isValidEthereumAddress =
address.startsWith('0x') && address.length === 42;
const isValidENS = address.endsWith('.eth');

if (!isValidEthereumAddress && !isValidENS) {
Expand All @@ -172,7 +173,7 @@ React.useEffect(() => {
onChange={(e) => setAddress(e.target.value)}
errorMessage={error}
aria-invalid={!!error}
/>
/>;
```

### Custom Clear Behavior
Expand All @@ -195,14 +196,14 @@ Handle clearing logic with additional state management:

### Layout & Styling

The AddressInput component takes the **full width** of its parent container by default. You can control the width using `containerClassName` (preferred) or wrapper containers:
The AddressInput component takes the **full width** of its parent container by default. You can control the width using `className` (preferred) or wrapper containers:

```tsx
// ✅ Preferred: Direct width control
<AddressInput placeholder="Enter address or ENS" containerClassName="max-w-md" />
<AddressInput placeholder="Enter address or ENS" className="max-w-md" />

// ✅ Responsive width control
<AddressInput placeholder="Enter address or ENS" containerClassName="w-full md:max-w-md" />
<AddressInput placeholder="Enter address or ENS" className="w-full md:max-w-md" />

// Alternative: Container wrapper
<div className="max-w-md">
Expand Down Expand Up @@ -231,11 +232,11 @@ const handleQrScan = () => {
};

<AddressInput
placeholder="Enter address or ENS"
placeholder='Enter address or ENS'
value={address}
onChange={(e) => setAddress(e.target.value)}
onQrCodeClick={handleQrScan}
/>
/>;
```

## Common Patterns
Expand All @@ -245,12 +246,12 @@ const handleQrScan = () => {
```tsx
<form>
<AddressInput
placeholder="Enter address or ENS"
placeholder='Enter address or ENS'
value={recipientAddress}
onChange={(e) => setRecipientAddress(e.target.value)}
errorMessage={addressError}
aria-invalid={!!addressError}
containerClassName="max-w-md"
className='max-w-md'
/>
{/* Other transaction fields */}
</form>
Expand All @@ -260,16 +261,16 @@ const handleQrScan = () => {

```tsx
<AddressInput
placeholder="Enter address or ENS"
placeholder='Enter address or ENS'
value={selectedAddress}
onChange={(e) => setSelectedAddress(e.target.value)}
onQrCodeClick={() => openQrScanner()}
suffix={
selectedAddress ? (
<InteractiveIcon
iconType="stroked"
iconType='stroked'
onClick={openAddressBook}
aria-label="Open address book"
aria-label='Open address book'
>
<ContactsIcon size={20} />
</InteractiveIcon>
Expand Down Expand Up @@ -299,11 +300,11 @@ The component includes comprehensive test coverage for:
✅ **Do**

```tsx
// Use containerClassName for layout and width control (preferred)
<AddressInput placeholder="Enter address or ENS" containerClassName="max-w-md" />
// Use className for layout and width control (preferred)
<AddressInput placeholder="Enter address or ENS" className="max-w-md" />

// Use containerClassName for responsive design
<AddressInput placeholder="Enter address or ENS" containerClassName="w-full md:max-w-lg" />
// Use className for responsive design
<AddressInput placeholder="Enter address or ENS" className="w-full md:max-w-lg" />

// Use custom prefix for different contexts
<AddressInput
Expand All @@ -322,7 +323,7 @@ const validateAddress = (address) => {
❌ **Don't**

```tsx
// Don't use generic wrapper divs when containerClassName is available
// Don't use generic wrapper divs when className is available
<div className="max-w-md">
<AddressInput placeholder="Enter address or ENS" />
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,12 @@ export const Empty: Story = {
args: {
placeholder: 'Enter address or ENS',
onQrCodeClick: () => console.log('QR code clicked!'),
containerClassName: 'max-w-md',
className: 'max-w-md',
},
parameters: {
docs: {
source: {
code: '<AddressInput placeholder="Enter address or ENS" onQrCodeClick={() => openQrScanner()} containerClassName="max-w-md" />',
code: '<AddressInput placeholder="Enter address or ENS" onQrCodeClick={() => openQrScanner()} className="max-w-md" />',
},
},
},
Expand All @@ -75,13 +75,13 @@ export const WithContent: Story = {
args: {
placeholder: 'Enter address or ENS',
defaultValue: '0x95f980s5ag77xe7csuz',
containerClassName: 'max-w-md',
className: 'max-w-md',
onQrCodeClick: () => console.log('QR code clicked!'),
},
parameters: {
docs: {
source: {
code: '<AddressInput placeholder="Enter address or ENS" defaultValue="0x95f980s5ag77xe7csuz" containerClassName="max-w-md" />',
code: '<AddressInput placeholder="Enter address or ENS" defaultValue="0x95f980s5ag77xe7csuz" className="max-w-md" />',
},
},
},
Expand All @@ -95,13 +95,13 @@ export const Disabled: Story = {
placeholder: 'Enter address or ENS',
disabled: true,
defaultValue: '0x95f980s5ag77xe7csuz',
containerClassName: 'max-w-md',
className: 'max-w-md',
onQrCodeClick: () => console.log('QR code clicked!'),
},
parameters: {
docs: {
source: {
code: '<AddressInput placeholder="Enter address or ENS" disabled defaultValue="0x95f980s5ag77xe7csuz" containerClassName="max-w-md" />',
code: '<AddressInput placeholder="Enter address or ENS" disabled defaultValue="0x95f980s5ag77xe7csuz" className="max-w-md" />',
},
},
},
Expand All @@ -116,7 +116,7 @@ export const Error: Story = {
defaultValue: 'invalid-address-format',
errorMessage: 'Invalid address format',
'aria-invalid': true,
containerClassName: 'max-w-md',
className: 'max-w-md',
onQrCodeClick: () => console.log('QR code clicked!'),
},
parameters: {
Expand All @@ -127,7 +127,7 @@ export const Error: Story = {
defaultValue="invalid-address-format"
errorMessage="Invalid address format"
aria-invalid={true}
containerClassName="max-w-md"
className="max-w-md"
/>`,
},
},
Expand Down Expand Up @@ -180,7 +180,7 @@ export const Controlled: Story = {
}}
errorMessage={error}
aria-invalid={!!error}
containerClassName='max-w-md'
className='max-w-md'
/>

<div className='text-muted body-3'>
Expand All @@ -190,11 +190,11 @@ export const Controlled: Story = {
</div>

{address && !error && (
<div className='border-muted bg-success rounded-md border p-16'>
<h4 className='text-success body-2-semi-bold mb-4'>
<div className='rounded-md border border-muted bg-success p-16'>
<h4 className='mb-4 text-success body-2-semi-bold'>
Valid Address
</h4>
<p className='text-success body-3 break-all'>{address}</p>
<p className='break-all text-success body-3'>{address}</p>
</div>
)}
</div>
Expand All @@ -208,13 +208,13 @@ export const Controlled: Story = {
export const WithoutQrCode: Story = {
args: {
placeholder: 'Enter address or ENS',
containerClassName: 'max-w-md',
className: 'max-w-md',
onQrCodeClick: undefined,
},
parameters: {
docs: {
source: {
code: '<AddressInput containerClassName="max-w-md" />',
code: '<AddressInput className="max-w-md" />',
},
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export type AddressInputProps = {
* - **Conditional QR code scanner** appears only when onQrCodeClick handler is provided
* - **ENS and address support** optimized for cryptocurrency address entry
* - **Error state styling** with aria-invalid and errorMessage support
* - **Flexible styling** via className, containerClassName props
* - **Flexible styling** via className, inputClassName, labelClassName props
*
* ## Clear Button Behavior
* - Shows automatically when input has content and is not disabled
Expand Down Expand Up @@ -92,7 +92,7 @@ export const AddressInput = React.forwardRef<
// Use custom prefix if provided, otherwise default "To:" prefix
const effectivePrefix = (
<span
className='body-1 group-has-[:disabled]:text-disabled text-nowrap text-base'
className='text-nowrap text-base body-1 group-has-[:disabled]:text-disabled'
aria-hidden='true'
>
{prefix}
Expand Down
Loading
Loading