Skip to content
5 changes: 5 additions & 0 deletions .nx/version-plans/version-plan-1792684934000-ui-react.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@ledgerhq/lumen-ui-react': patch
---

feat(ProgressBar): add a determinate `ProgressBar` with semantic and decorative appearances and an optional label
5 changes: 5 additions & 0 deletions .nx/version-plans/version-plan-1792684934000-ui-rnative.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@ledgerhq/lumen-ui-rnative': patch
---

feat(ProgressBar): add a determinate `ProgressBar` with semantic and decorative appearances and an optional label
138 changes: 138 additions & 0 deletions apps/app-sandbox-rnative/src/app/(blocks)/ProgressBars.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { Box, IconButton, ProgressBar, Text } from '@ledgerhq/lumen-ui-rnative';
import { ArrowLeft, ArrowRight } from '@ledgerhq/lumen-ui-rnative/symbols';
import { useEffect, useState } from 'react';

const STEP = 10;
const AUTO_STEP = 20;
const AUTO_INTERVAL_MS = 30_000;

const appearances = [
'base',
'accent',
'success',
'error',
'warning',
'decorative-blue',
'decorative-pink',
'decorative-purple',
'decorative-green',
'decorative-turquoise',
'decorative-yellow',
'decorative-orange',
'decorative-red',
] as const;

const labelPositions = [
'start-top',
'start-bottom',
'end-top',
'end-bottom',
] as const;

export default function ProgressBars() {
const [value, setValue] = useState(40);
const [autoValue, setAutoValue] = useState(0);

useEffect(() => {
const interval = setInterval(() => {
setAutoValue((v) => (v >= 100 ? 0 : v + AUTO_STEP));
}, AUTO_INTERVAL_MS);
return () => clearInterval(interval);
}, []);

return (
<Box lx={{ gap: 's32', width: 'full' }}>
{/* Interactive */}
<Box lx={{ gap: 's8', width: 'full' }}>
<Text typography='body3' lx={{ color: 'muted' }}>
Interactive
</Text>
<Box
lx={{
flexDirection: 'row',
gap: 's12',
alignItems: 'center',
}}
>
<IconButton
icon={ArrowLeft}
size='xs'
accessibilityLabel='Decrease'
appearance='transparent'
onPress={() => setValue((v) => Math.max(0, v - STEP))}
/>
<Box lx={{ flex: 1 }}>
<ProgressBar
value={value}
accessibilityLabel='Interactive progress'
label={`${value}%`}
/>
</Box>
<IconButton
icon={ArrowRight}
size='xs'
accessibilityLabel='Increase'
appearance='transparent'
onPress={() => setValue((v) => Math.min(100, v + STEP))}
/>
</Box>
</Box>

{/* Animated */}
<Box lx={{ gap: 's8', width: 'full' }}>
<Text typography='body3' lx={{ color: 'muted' }}>
Animated fill (+20 every 30s)
</Text>
<ProgressBar
value={autoValue}
appearance='accent'
accessibilityLabel='Auto progress'
label='Syncing accounts'
labelPosition='start-top'
/>
</Box>

{/* Appearances */}
<Box lx={{ gap: 's12', width: 'full' }}>
<Text typography='body3' lx={{ color: 'muted' }}>
Appearances
</Text>
{appearances.map((appearance) => (
<ProgressBar
key={appearance}
value={60}
appearance={appearance}
accessibilityLabel={appearance}
label={appearance}
/>
))}
</Box>

{/* Label positions */}
<Box lx={{ gap: 's24', width: 'full' }}>
<Text typography='body3' lx={{ color: 'muted' }}>
Label positions
</Text>
{labelPositions.map((position) => (
<ProgressBar
key={position}
value={40}
accessibilityLabel={position}
label={position}
labelPosition={position}
/>
))}
</Box>

{/* Values */}
<Box lx={{ gap: 's12', width: 'full' }}>
<Text typography='body3' lx={{ color: 'muted' }}>
Values
</Text>
{[0, 25, 50, 75, 100].map((v) => (
<ProgressBar key={v} value={v} accessibilityLabel={`${v}%`} />
))}
</Box>
</Box>
);
}
1 change: 1 addition & 0 deletions apps/app-sandbox-rnative/src/blocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export const blocks: BlockMeta[] = [
{ slug: 'NavBars', title: 'Navbar' },
{ slug: 'PageIndicators', title: 'PageIndicator' },
{ slug: 'Paginations', title: 'Pagination' },
{ slug: 'ProgressBars', title: 'ProgressBar' },
{ slug: 'SectionHeaders', title: 'SectionHeader' },
{ slug: 'SegmentedControls', title: 'SegmentedControl' },
{ slug: 'SelectLists', title: 'SelectList' },
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import figma from '@figma/code-connect';
import { ProgressBar } from './ProgressBar';

figma.connect(
ProgressBar,
'https://www.figma.com/design/JxaLVMTWirCpU0rsbZ30k7/2.-Components-Library?node-id=21807-57957&m=dev',
{
imports: ["import { ProgressBar } from '@ledgerhq/lumen-ui-react'"],
example: () => (
<ProgressBar value={25} label='Label' labelPosition='start-bottom' />
),
Comment thread
aammami-ledger marked this conversation as resolved.
Resolved
},
);
122 changes: 122 additions & 0 deletions libs/ui-react/src/lib/Components/core/ProgressBar/ProgressBar.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { Meta, Canvas, Controls, Source } from '@storybook/addon-docs/blocks';
import * as ProgressBarStories from './ProgressBar.stories';
import {
CustomTabs,
Tab,
SetupNote,
} from '../../../../../.storybook/components';

<Meta title='Core/ProgressBar' of={ProgressBarStories} />

# ProgressBar

<CustomTabs>
<Tab label="Overview">

The ProgressBar shows how far along a task is, such as an upload, a sync or an onboarding checklist. It fills a track from 0 to 100 and can show an optional label above or below the bar.

- 🔗 **[Figma](https://www.figma.com/design/JxaLVMTWirCpU0rsbZ30k7/2.-Components-Library?node-id=21807-57957&m=dev)**

<Canvas of={ProgressBarStories.Base} />

## Properties

<Controls of={ProgressBarStories.Base} />

### Value

`value` is a number from 0 to 100. Values outside the range are clamped.

The fill animates to its new width in 200ms with an ease-out curve.

<Canvas of={ProgressBarStories.ValueShowcase} />

### Appearance

Five appearances carry meaning and are driven by the state of the task: `base` (default), `accent`, `success`, `error` and `warning`.

Eight `decorative-*` appearances (`blue`, `pink`, `purple`, `green`, `turquoise`, `yellow`, `orange`, `red`) are available for identity or categorization.

> ⚠️ Decorative appearances are not for status. Use `success`, `error` or `warning` to communicate the state of a task.

<Canvas of={ProgressBarStories.AppearanceShowcase} />

### Label

Pass `label` to show a label. It also names the bar for assistive technology.

<Canvas of={ProgressBarStories.WithLabel} />

`labelPosition` places the label above or below the bar, aligned to the start or end edge. The default is `start-bottom`. `start` and `end` follow the text direction.

With a label, `className` applies to the wrapper around the label and the bar. Without one, it applies to the bar.

<Canvas of={ProgressBarStories.LabelPositionShowcase} />

The label can contain more than text:

<Canvas of={ProgressBarStories.WithRichLabel} />

## Responsive behavior

The track spans the full width of its container, so size the bar by sizing its container. Its height is fixed at 6px.

## Accessibility

- The bar has `role="progressbar"` with `aria-valuemin` 0, `aria-valuemax` 100 and `aria-valuenow` set to the clamped value.
- Every bar needs an accessible name, and TypeScript requires either `label` or `aria-label`.
- Pair the bar with visible text so color is never the only signal.

### Accessible name

- **With a `label`**: the label names the bar, and you don't need `aria-label`.
- **Without a visible label**: pass `aria-label`, for example `aria-label='Upload progress'`.
- **When the spoken name should differ from the visible text**: pass both. `aria-label` takes precedence over `label` for assistive technology, while the label stays visible.

</Tab>
<Tab label="Implementation">

## Installation

Install the packages and their peer dependencies:

```bash
# Install the UI Kit
npm install @ledgerhq/lumen-ui-react @ledgerhq/lumen-design-core

```

<SetupNote />

### Basic Usage

A bar without a visible label needs an `aria-label`:

<Source of={ProgressBarStories.Base} />

### With a label

`label` renders the visible label and wires it as the accessible name, so no `aria-label` is needed.

<Source of={ProgressBarStories.WithLabel} />

### Appearance

<Source of={ProgressBarStories.AppearanceShowcase} />

### Label position

<Source of={ProgressBarStories.LabelPositionShowcase} />

### Custom accessible name

```tsx
<ProgressBar
value={33}
label='4 of 12'
aria-label='Account sync progress'
/>
```

</Tab>
</CustomTabs>
Loading
Loading