# Progress

Reports how far a task has got, whether or not its length is known.

Progress is a readout of how far a task has got, drawn on screen and announced
to a screen reader. Useful whenever something takes long enough that the
interface has to say so; leave `value` unset and it reports a task whose length
nobody knows yet.

::component-preview{name="ProgressPreview"}
```vue
<template>
  <div class="flex w-64 flex-col gap-5">
    <Progress.Root
      :value="value"
      aria-label="Uploading"
      class="flex flex-col gap-2"
    >
      <div
        class="type-surface-code text-surface-muted flex items-baseline justify-between"
      >
        <span>Uploading</span>
        <Progress.Value class="tabular-nums" />
      </div>

      <Progress.Track :class="track">
        <Progress.Indicator :class="indicator" />
      </Progress.Track>
    </Progress.Root>

    <Progress.Root
      :value="null"
      aria-label="Preparing"
      class="flex flex-col gap-2"
    >
      <span class="type-surface-code text-surface-muted">Preparing</span>

      <Progress.Track :class="track">
        <div class="bg-primary-solid h-full w-1/3 animate-pulse" />
      </Progress.Track>
    </Progress.Root>

    <div class="flex gap-2">
      <Button :class="button" @click="step(-20)">−20</Button>
      <Button :class="button" @click="step(20)">+20</Button>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { Button, Progress } from '@maas/mirror/vue'

const value = ref(40)

function step(amount: number) {
  value.value = Math.min(100, Math.max(0, value.value + amount))
}

const track = 'rounded-component-round bg-surface-higher h-2 overflow-hidden'

const indicator = [
  'h-full w-full! origin-left scale-x-[calc(var(--mpr-percentage,0)*1%)]',
  'bg-primary-solid transition-transform duration-300',
  'data-[state=complete]:bg-success-solid',
].join(' ')

const button = [
  'rounded-component-md border-surface border px-2.5 py-1',
  'type-component-xs text-surface-muted hover:text-surface transition-colors',
].join(' ')
</script>
```
::

## Usage guidelines

- A progressbar has no implicit name, so render a `Progress.Label`, give the
  root an `aria-label`, or point `aria-labelledby` at the element that names it.
- The percentage, the formatted string and `aria-valuenow` all report the
  clamped value, so a source that overshoots `max` fills the track and says so.
  The raw `value` stays available on the slot and to `options.getAriaValueText`.
- `options.format` resolves against the runtime locale unless `options.locale`
  says otherwise. If the server and the client can differ, pin the locale, or
  pin the digits with `minimumFractionDigits` and `maximumFractionDigits`.

## Anatomy

Assemble the parts, in whatever order the layout needs.

::component-anatomy
---
parts:
  - name: Progress.Root
    required: true
    description: 'Renders a <div role="progressbar">. Owns the value and writes the percentage.'
    children:
      - name: Progress.Label
        description: Renders a <span> whose id becomes the root's aria-labelledby.
      - name: Progress.Track
        description: 'Renders a <div>. Structural: the full length of the bar.'
        children:
          - name: Progress.Indicator
            description: Renders a <div>, sized to the current percentage.
      - name: Progress.Value
        description: Renders a <span> with the formatted value.
---
::

```vue
<script setup lang="ts">
import { Progress } from '@maas/mirror/vue'
</script>

<template>
  <Progress.Root :value="uploaded" :options="{ max: total }">
    <Progress.Label>Uploading</Progress.Label>

    <Progress.Track>
      <Progress.Indicator />
    </Progress.Track>

    <Progress.Value />
  </Progress.Root>
</template>
```

## Examples

### Determinate progress

Pass a `value` and the percentage follows from where it sits between `min` and
`max`.

```vue
<template>
  <Progress.Root
    :value="uploaded"
    :options="{ max: total }"
    aria-label="Uploading"
  >
    <Progress.Track>
      <Progress.Indicator />
    </Progress.Track>
  </Progress.Root>
</template>
```

### Naming the progress

`Progress.Label` renders a `<span>` and registers its id as the root’s
`aria-labelledby`, so the name lives in the markup rather than in an attribute.

```vue
<template>
  <Progress.Root :value="uploaded" :options="{ max: total }">
    <Progress.Label>Uploading</Progress.Label>
    <Progress.Track>
      <Progress.Indicator />
    </Progress.Track>
  </Progress.Root>
</template>
```

Without it the root points nowhere, so give it an `aria-label` instead.

### Indeterminate progress

Leave `value` at its default of `null` and `aria-valuenow` is dropped, which is
how a screen reader is told the progress is indeterminate. A `value` that is
not a finite number, such as the `NaN` or `Infinity` an unguarded division
returns, reads the same way rather than rendering as `NaN`.

```vue
<template>
  <Progress.Root aria-label="Preparing">
    <Progress.Track>
      <Progress.Indicator />
    </Progress.Track>
  </Progress.Root>
</template>
```

`aria-valuetext` says `indeterminate progress` in that state, and the
`Progress.Value` slot is handed the string `indeterminate` while the element
itself renders nothing.

There is no percentage to size the indicator with, so that state is yours to
style.

```css
.indicator[data-state='indeterminate'] {
  inline-size: 33%;
  animation: slide 1.2s linear infinite;
}
```

### Sizing the indicator

`Progress.Root` writes the percentage to `--mpr-percentage`, a unitless number
between `0` and `100`, and the CSS we ship turns it into the indicator’s
width.

```css
.mirror-progress-indicator {
  block-size: 100%;
  inline-size: calc(var(--mpr-percentage, 0) * 1%);
}
```

The property sits on the root, so anything inside the component can read it,
whether that is a tick mark, a moving label or a conic-gradient ring.

```css
.ring {
  background: conic-gradient(
    var(--app-color-primary-bg-solid) calc(var(--mpr-percentage, 0) * 1%),
    var(--app-color-surface-bg-higher) 0
  );
}
```

An indeterminate progress does not set the property at all, which is why every
`var(--mpr-percentage, …)` needs a fallback.

### Formatting the value

`options.format` drives both `Progress.Value` and `aria-valuetext`, and
defaults to `{ style: 'percent' }`, which formats the percentage rather than the
value.

```vue
<template>
  <Progress.Root :value="0.42" :options="{ max: 1 }" aria-label="Uploading">
    <Progress.Value />
  </Progress.Root>
</template>
```

Any other style formats the clamped value instead, which is how you report raw
units rather than a share of the whole.

```vue
<template>
  <Progress.Root
    :value="bytes"
    :options="{
      max: total,
      format: { style: 'unit', unit: 'megabyte', maximumFractionDigits: 1 },
    }"
    aria-label="Uploading"
  >
    <Progress.Value />
  </Progress.Root>
</template>
```

Take the slot to put the formatted string in markup of your own.

```vue
<template>
  <Progress.Value v-slot="{ formatted, percentage }">
    {{ formatted }} · {{ Math.round(percentage ?? 0) }} of 100
  </Progress.Value>
</template>
```

`options.locale` pins the locale `Intl.NumberFormat` resolves against, which is
what keeps a server render and a client render reading the same.

```vue
<template>
  <Progress.Root :value="uploaded" :options="{ max: total, locale: 'en-GB' }" />
</template>
```

### Overriding the announcement

`options.getAriaValueText` receives the formatted string and the raw value, and
what it returns replaces `aria-valuetext` entirely.

```vue
<template>
  <Progress.Root
    :value="uploaded"
    :options="{
      max: total,
      getAriaValueText: (formatted, value) => `${value} of ${total} files`,
    }"
    aria-label="Uploading"
  >
    <Progress.Track>
      <Progress.Indicator />
    </Progress.Track>
  </Progress.Root>
</template>
```

It is called in the indeterminate state too, where `value` is `null` and
`formatted` is an empty string, so guard for those. Leave it out and the
indeterminate state announces `indeterminate progress`.

## API reference

**Module.** A bundled `options` object, and `useMirrorProgress(id)`
as the programmatic API.

### `Progress.Root`

Renders a `<div role="progressbar">`.

#### Props

::docs-table
---
columns:
  - label: Prop
  - label: Type
  - label: Default
rows:
  - items:
      - label: id
        description: The instance ID.
      - label: string
      - label: generated
        plaintext: true
  - items:
      - label: value
        description: 'Current progress. `null` is [indeterminate](#indeterminate-progress).'
      - label: 'number | null'
      - label: 'null'
  - items:
      - label: options
        description: Everything else. Deep-merged over the defaults.
      - label: ProgressOptions
      - label: see below
        plaintext: true
---
::

#### Options

::docs-table
---
columns:
  - label: Option
  - label: Type
  - label: Default
rows:
  - items:
      - label: min
        description: Lower bound of the range.
      - label: number
      - label: '0'
  - items:
      - label: max
        description: Upper bound of the range.
      - label: number
      - label: '100'
  - items:
      - label: locale
        description: Locale for `Intl.NumberFormat`.
      - label: Intl.LocalesArgument
      - label: the runtime locale
        plaintext: true
  - items:
      - label: format
        description: '[Formatting](#formatting-the-value) for `Progress.Value` and `aria-valuetext`.'
      - label: Intl.NumberFormatOptions
      - label: '{ style: ''percent'' }'
  - items:
      - label: getAriaValueText
        description: 'Replaces [`aria-valuetext`](#overriding-the-announcement).'
      - label: '(formatted: string, value: number | null) => string'
        escape: true
      - label: undefined
---
::

#### Emits

None. `Progress` is a readout.

#### Slot props

::docs-table
---
columns:
  - label: Prop
  - label: Type
rows:
  - items:
      - label: value
        description: The raw value, unclamped.
      - label: 'number | null'
  - items:
      - label: percentage
        description: The clamped percentage, `0` to `100`. `null` while indeterminate.
      - label: 'number | null'
  - items:
      - label: status
        description: Mirrors `data-state`.
      - label: '''indeterminate'' | ''progressing'' | ''complete'''
  - items:
      - label: formatted
        description: The formatted value. Empty while indeterminate.
      - label: string
---
::

### `Progress.Label`

Renders a `<span>` and registers its id as the root’s `aria-labelledby`. No
props of its own.

#### Slot props

`status: 'indeterminate' | 'progressing' | 'complete'`.

### `Progress.Track`

Renders a `<div>`, the full length of the bar and the box the indicator is
measured against. No props of its own.

### `Progress.Indicator`

Renders a `<div>`, sized to `calc(var(--mpr-percentage, 0) * 1%)` on the inline
axis and `100%` on the block axis. No props of its own.

### `Progress.Value`

Renders a `<span>` with the formatted value, `aria-hidden` because the root
already announces it. No props of its own.

#### Slot props

::docs-table
---
columns:
  - label: Prop
  - label: Type
rows:
  - items:
      - label: value
        description: The raw value, unclamped.
      - label: 'number | null'
  - items:
      - label: percentage
        description: The clamped percentage, `0` to `100`.
      - label: 'number | null'
  - items:
      - label: formatted
        description: 'The formatted value, or [`indeterminate`](#indeterminate-progress).'
      - label: string
---
::

### Composable

`useMirrorProgress(id)` reaches a progress from anywhere in the app.

::docs-table
---
columns:
  - label: Key
  - label: Type
rows:
  - items:
      - label: value
        description: The raw value.
      - label: 'ComputedRef<number | null>'
        escape: true
  - items:
      - label: percentage
        description: The clamped percentage.
      - label: 'ComputedRef<number | null>'
        escape: true
  - items:
      - label: status
        description: Mirrors `data-state`.
      - label: ComputedRef<ProgressStatus>
        escape: true
  - items:
      - label: formatted
        description: The formatted value.
      - label: ComputedRef<string>
        escape: true
  - items:
      - label: setValue
        description: Sets the value.
      - label: '(next: number | null) => void'
        escape: true
---
::

### Data attributes

Every part writes the same values, so any of them can be styled from the state
without a wrapper class.

::data-attributes
::

### CSS variables

::css-variables
::

### Errors

::docs-table
---
columns:
  - label: Code
rows:
  - items:
      - label: missing_progress_context
        description: 'A `Progress` part other than the root rendered outside `Progress.Root` with no `id` of its own.'
  - items:
      - label: invalid_progress_range
        description: '`min` is greater than or equal to `max`. Checked on every change, not only on mount.'
---
::

## Accessibility

`Progress.Root` renders a `role="progressbar"` with `aria-valuemin`,
`aria-valuemax`, `aria-valuenow` and `aria-valuetext`, and takes no focus and no
keyboard interaction. A `Progress.Label` inside it names it through
`aria-labelledby`.

An indeterminate progress omits `aria-valuenow` and announces
`indeterminate progress`, unless `options.getAriaValueText` supplies something
else.

The root also renders one visually hidden character of text, because NVDA only
reads the name of a progressbar that has content.
