@jobber/components-native 0.109.1 → 0.110.0
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/docs/ProgressBar/ProgressBar.md +1 -1
- package/dist/docs/ProgressIndicator/ProgressIndicator.md +117 -0
- package/dist/docs/index.md +1 -0
- package/dist/package.json +2 -2
- package/dist/src/ProgressBar/ProgressBar.js +6 -0
- package/dist/src/ProgressBar/ProgressBar.test.js +2 -0
- package/dist/src/ProgressIndicator/ProgressIndicator.js +106 -0
- package/dist/src/ProgressIndicator/ProgressIndicator.size.style.js +15 -0
- package/dist/src/ProgressIndicator/ProgressIndicator.style.js +51 -0
- package/dist/src/ProgressIndicator/ProgressIndicator.test.js +138 -0
- package/dist/src/ProgressIndicator/index.js +1 -0
- package/dist/src/ProgressIndicator/types.js +1 -0
- package/dist/src/hooks/useAtlantisI18n/locales/en.json +3 -0
- package/dist/src/hooks/useAtlantisI18n/locales/es.json +3 -0
- package/dist/src/index.js +1 -0
- package/dist/src/utils/meta/meta.json +1 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/dist/types/src/ProgressBar/ProgressBar.d.ts +3 -0
- package/dist/types/src/ProgressBar/types.d.ts +3 -0
- package/dist/types/src/ProgressIndicator/ProgressIndicator.d.ts +3 -0
- package/dist/types/src/ProgressIndicator/ProgressIndicator.size.style.d.ts +19 -0
- package/dist/types/src/ProgressIndicator/ProgressIndicator.style.d.ts +48 -0
- package/dist/types/src/ProgressIndicator/ProgressIndicator.test.d.ts +1 -0
- package/dist/types/src/ProgressIndicator/index.d.ts +2 -0
- package/dist/types/src/ProgressIndicator/types.d.ts +44 -0
- package/dist/types/src/index.d.ts +1 -0
- package/package.json +2 -2
- package/src/ProgressBar/ProgressBar.stories.tsx +2 -0
- package/src/ProgressBar/ProgressBar.test.tsx +2 -0
- package/src/ProgressBar/ProgressBar.tsx +6 -0
- package/src/ProgressBar/docs/ProgressBarBasic.tsx +2 -0
- package/src/ProgressBar/docs/ProgressBarWithHeader.tsx +2 -0
- package/src/ProgressBar/types.ts +3 -0
- package/src/ProgressIndicator/ProgressIndicator.size.style.ts +20 -0
- package/src/ProgressIndicator/ProgressIndicator.stories.tsx +154 -0
- package/src/ProgressIndicator/ProgressIndicator.style.ts +53 -0
- package/src/ProgressIndicator/ProgressIndicator.test.tsx +214 -0
- package/src/ProgressIndicator/ProgressIndicator.tsx +198 -0
- package/src/ProgressIndicator/docs/ProgressIndicatorBasic.tsx +22 -0
- package/src/ProgressIndicator/docs/ProgressIndicatorPending.tsx +22 -0
- package/src/ProgressIndicator/docs/index.ts +2 -0
- package/src/ProgressIndicator/guide.md +104 -0
- package/src/ProgressIndicator/index.ts +2 -0
- package/src/ProgressIndicator/types.ts +51 -0
- package/src/hooks/useAtlantisI18n/locales/en.json +3 -0
- package/src/hooks/useAtlantisI18n/locales/es.json +3 -0
- package/src/index.ts +1 -0
- package/src/utils/meta/meta.json +1 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Progress Bar
|
|
2
2
|
|
|
3
|
-
> **Deprecated.** Use [ProgressIndicator](/
|
|
3
|
+
> **Deprecated.** Use [ProgressIndicator](../ProgressIndicator/ProgressIndicator.md) for new
|
|
4
4
|
> work. `ProgressIndicator` is the supported determinate progress indicator
|
|
5
5
|
> going forward — it offers the same semantic with a cleaner prop surface
|
|
6
6
|
> (`value` / `max` instead of `currentStep` / `totalSteps`,
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# Progress Indicator
|
|
2
|
+
|
|
3
|
+
`ProgressIndicator` communicates **determinate** progress — a known fraction of
|
|
4
|
+
a task that has been completed. Use it when you can express progress as
|
|
5
|
+
`value / max`, such as a multi-step setup wizard or a file upload with a known
|
|
6
|
+
total size.
|
|
7
|
+
|
|
8
|
+
For indeterminate activity (loading time or fraction unknown), use
|
|
9
|
+
[ActivityIndicator](../ActivityIndicator/ActivityIndicator.md) instead.
|
|
10
|
+
|
|
11
|
+
Some great use cases for a `ProgressIndicator` include:
|
|
12
|
+
|
|
13
|
+
* Setup wizards, where we know how many steps the user has completed and how
|
|
14
|
+
many steps remain (use `variation="stepped"`).
|
|
15
|
+
* File uploads, where we know the total file size and how much data has already
|
|
16
|
+
been sent (use the default `variation="continuous"`).
|
|
17
|
+
|
|
18
|
+
## Design & usage guidelines
|
|
19
|
+
|
|
20
|
+
The default `ProgressIndicator` size is `base` (16px tall) and can be used in
|
|
21
|
+
most cases. The `small` (8px) and `smaller` (4px) sizes should be used when the
|
|
22
|
+
indicator sits inline with text or in tight surface layouts.
|
|
23
|
+
|
|
24
|
+
`ProgressIndicator` always fills the width of its parent. Layout — including
|
|
25
|
+
inline-vs-block placement — is the consumer's responsibility. Wrap the indicator
|
|
26
|
+
with your own flex / `<Stack>` / `<Box>` container, or pass a `className` /
|
|
27
|
+
`style`, to control the surrounding layout.
|
|
28
|
+
|
|
29
|
+
### Continuous vs. stepped
|
|
30
|
+
|
|
31
|
+
Use `variation="continuous"` (the default) when progress is a smooth value
|
|
32
|
+
between `0` and `max` — for example, a file upload where any percentage is
|
|
33
|
+
meaningful.
|
|
34
|
+
|
|
35
|
+
Use `variation="stepped"` when progress is naturally segmented — for example, a
|
|
36
|
+
multi-step wizard where the user is on step 2 of 5. Stepped renders `max`
|
|
37
|
+
discrete segments and fills the first `value` of them.
|
|
38
|
+
|
|
39
|
+
## Accessibility
|
|
40
|
+
|
|
41
|
+
`ProgressIndicator` announces itself politely to assistive technology:
|
|
42
|
+
|
|
43
|
+
* `role="progressbar"` marks the root element as a progress bar.
|
|
44
|
+
* `aria-valuenow`, `aria-valuemin`, and `aria-valuemax` reflect the current
|
|
45
|
+
progress so screen readers can announce the value as a percentage or ratio
|
|
46
|
+
appropriately.
|
|
47
|
+
* `aria-label` defaults to a localized-as-English template: `"{value}%"` when
|
|
48
|
+
`max === 100` (or `max` is omitted), and `"{value} / {max}"` otherwise. Pass a
|
|
49
|
+
custom `ariaLabel` to localize or to describe the specific activity (e.g.
|
|
50
|
+
`ariaLabel="Uploading file"`).
|
|
51
|
+
|
|
52
|
+
In the stepped variation, the individual segments are presentational `<div>`s
|
|
53
|
+
with no `role` or `aria-*` attributes. The whole indicator announces once as a
|
|
54
|
+
single progressbar — the outer `aria-valuenow` / `aria-valuemax` already carry
|
|
55
|
+
the progress information.
|
|
56
|
+
|
|
57
|
+
The indicator does not participate in the keyboard tab order.
|
|
58
|
+
|
|
59
|
+
### Reduced motion
|
|
60
|
+
|
|
61
|
+
The width transition between `value` updates is intentionally **preserved**
|
|
62
|
+
under
|
|
63
|
+
[`prefers-reduced-motion: reduce`](https://developer.mozilla.org/docs/Web/CSS/@media/prefers-reduced-motion).
|
|
64
|
+
The slide between progress values conveys functional information about progress,
|
|
65
|
+
direction, and magnitude — it is exempt under
|
|
66
|
+
[WCAG 2.3.3 ("Animation from Interactions")](https://www.w3.org/WAI/WCAG21/Understanding/animation-from-interactions.html).
|
|
67
|
+
Removing the transition would make progress harder to perceive, not easier.
|
|
68
|
+
|
|
69
|
+
## Relationship to ProgressBar
|
|
70
|
+
|
|
71
|
+
The legacy [ProgressBar](../ProgressBar/ProgressBar.md) component remains available
|
|
72
|
+
and unchanged. New code should prefer `ProgressIndicator`. A follow-up change is
|
|
73
|
+
expected to consolidate `ProgressBar` onto `ProgressIndicator`.
|
|
74
|
+
|
|
75
|
+
`ProgressIndicator` renames `ProgressBar`'s props to align with HTML5
|
|
76
|
+
`<progress>` semantics and mobile naming:
|
|
77
|
+
|
|
78
|
+
| `ProgressBar` (legacy) | `ProgressIndicator` (new) |
|
|
79
|
+
| ---------------------- | ------------------------- |
|
|
80
|
+
| `currentStep` | `value` |
|
|
81
|
+
| `totalSteps` | `max` |
|
|
82
|
+
| `variation="progress"` | `variation="continuous"` |
|
|
83
|
+
| `UNSAFE_className` | `className` |
|
|
84
|
+
| `UNSAFE_style` | `style` |
|
|
85
|
+
|
|
86
|
+
## Mobile
|
|
87
|
+
|
|
88
|
+
Mobile (`@jobber/components-native`) ships its own `ProgressIndicator` with the
|
|
89
|
+
same `value` / `max` / `variation` / `size` surface. Two platform differences:
|
|
90
|
+
|
|
91
|
+
* The accessible label prop is `accessibilityLabel` (React Native's convention),
|
|
92
|
+
and the derived default follows the same percent / ratio templates. Styling
|
|
93
|
+
uses the `style` prop (there is no `className`).
|
|
94
|
+
* Mobile adds a **`pendingValue`** prop (mobile-only). It renders an additional
|
|
95
|
+
in-flight band beyond `value`, on the same scale as `value` and `max`, and is
|
|
96
|
+
**additive**: with `value={2}`, `pendingValue={3}`, `max={10}` the bar shows
|
|
97
|
+
2/10 completed plus another 3/10 in flight (5/10 filled in total). It applies
|
|
98
|
+
to both the continuous and stepped variations, and adds a third derived label
|
|
99
|
+
template — `"{value} of {max}, {pendingValue} pending"`.
|
|
100
|
+
|
|
101
|
+
The mobile `ProgressBar` remains available and unchanged; new mobile code should
|
|
102
|
+
prefer `ProgressIndicator`.
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
## Props
|
|
106
|
+
|
|
107
|
+
### Mobile
|
|
108
|
+
|
|
109
|
+
| Prop | Type | Required | Default | Description |
|
|
110
|
+
|------|------|----------|---------|-------------|
|
|
111
|
+
| `value` | `number` | Yes | — | Current progress. Expected to be in the range `0..max` inclusive. |
|
|
112
|
+
| `accessibilityLabel` | `string` | No | — | Accessible label override. When omitted, the label is derived via `useAtlantisI18n`: `"{value}%"` when `max` is `100`... |
|
|
113
|
+
| `max` | `number` | No | `100` | Maximum value. |
|
|
114
|
+
| `pendingValue` | `number` | No | `0` | Additional in-flight progress beyond `value`, measured on the same scale as `value` and `max`. The bar visually fills... |
|
|
115
|
+
| `size` | `"base" | "small" | "smaller"` | No | `base` | Set the size of the progress indicator. Corresponds to bar heights of 4px / 8px / 16px. |
|
|
116
|
+
| `style` | `StyleProp<ViewStyle>` | No | — | Custom style applied to the root element. |
|
|
117
|
+
| `variation` | `"continuous" | "stepped"` | No | `continuous` | Set the variation of the progress indicator. `continuous` renders a single fill; `stepped` renders one segment per in... |
|
package/dist/docs/index.md
CHANGED
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
[page-layouts](./page-layouts/page-layouts.md)
|
|
50
50
|
[product-vocabulary](./product-vocabulary/product-vocabulary.md)
|
|
51
51
|
[ProgressBar](./ProgressBar/ProgressBar.md)
|
|
52
|
+
[ProgressIndicator](./ProgressIndicator/ProgressIndicator.md)
|
|
52
53
|
[Radii](./Radii/Radii.md)
|
|
53
54
|
[ResponsiveBreakpoint](./ResponsiveBreakpoint/ResponsiveBreakpoint.md)
|
|
54
55
|
[scaffolding](./scaffolding/scaffolding.md)
|
package/dist/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jobber/components-native",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.110.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "React Native implementation of Atlantis",
|
|
6
6
|
"repository": {
|
|
@@ -124,5 +124,5 @@
|
|
|
124
124
|
"react-native-screens": ">=4.18.0",
|
|
125
125
|
"react-native-svg": ">=12.0.0"
|
|
126
126
|
},
|
|
127
|
-
"gitHead": "
|
|
127
|
+
"gitHead": "c81a2b04748119a1bd84cc39f4f5e7a97c410afb"
|
|
128
128
|
}
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
/* eslint-disable import/no-deprecated -- This file implements the deprecated
|
|
2
|
+
* ProgressBar component; self-references predate the deprecation. Migrate to
|
|
3
|
+
* ProgressIndicator. */
|
|
1
4
|
import React from "react";
|
|
2
5
|
import { View } from "react-native";
|
|
3
6
|
import { useStyles } from "./ProgressBar.style";
|
|
@@ -6,6 +9,9 @@ import { ProgressBarStepped } from "./ProgressBarStepped";
|
|
|
6
9
|
import { sizeStyles } from "./ProgressBar.size.style";
|
|
7
10
|
import { useAtlantisI18n } from "../hooks/useAtlantisI18n";
|
|
8
11
|
import { useAtlantisTheme } from "../AtlantisThemeContext";
|
|
12
|
+
/**
|
|
13
|
+
* @deprecated Use `ProgressIndicator` from `@jobber/components-native` instead.
|
|
14
|
+
*/
|
|
9
15
|
export function ProgressBar({ loading, total, current, inProgress = 0, reverseTheme = false, header, variation = "progress", size = "base", UNSAFE_style, }) {
|
|
10
16
|
const { t } = useAtlantisI18n();
|
|
11
17
|
const styles = useStyles();
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import React, { useEffect } from "react";
|
|
2
|
+
import { View } from "react-native";
|
|
3
|
+
import Animated, { Easing, useAnimatedStyle, useSharedValue, withTiming, } from "react-native-reanimated";
|
|
4
|
+
import { useStyles } from "./ProgressIndicator.style";
|
|
5
|
+
import { sizeHeights, sizeStyles } from "./ProgressIndicator.size.style";
|
|
6
|
+
import { useAtlantisI18n } from "../hooks/useAtlantisI18n";
|
|
7
|
+
import { tokens } from "../utils/design";
|
|
8
|
+
export function ProgressIndicator({ value, max = 100, pendingValue = 0, variation = "continuous", size = "base", accessibilityLabel, style, }) {
|
|
9
|
+
const styles = useStyles();
|
|
10
|
+
const { t } = useAtlantisI18n();
|
|
11
|
+
const label = accessibilityLabel !== null && accessibilityLabel !== void 0 ? accessibilityLabel : getA11yLabel();
|
|
12
|
+
const radius = sizeHeights[size] / 2;
|
|
13
|
+
return (React.createElement(View, { accessible: true, accessibilityRole: "progressbar", accessibilityLabel: label, accessibilityValue: { min: 0, max, now: value, text: label }, style: [
|
|
14
|
+
variation === "stepped" ? styles.stepped : styles.continuous,
|
|
15
|
+
sizeStyles[size],
|
|
16
|
+
// Round the continuous track so its trailing (unfilled) end is pill-
|
|
17
|
+
// shaped like the leading end; `overflow: "hidden"` clips the fill to
|
|
18
|
+
// match. The stepped variation rounds its individual segments instead.
|
|
19
|
+
variation === "continuous" && { borderRadius: radius },
|
|
20
|
+
style,
|
|
21
|
+
] }, variation === "stepped" ? (React.createElement(ProgressIndicatorStepped, { value: value, max: max, pendingValue: pendingValue, size: size })) : (React.createElement(ProgressIndicatorContinuous, { value: value, max: max, pendingValue: pendingValue, size: size }))));
|
|
22
|
+
function getA11yLabel() {
|
|
23
|
+
if (pendingValue > 0) {
|
|
24
|
+
return t("ProgressIndicator.ariaLabelPending", {
|
|
25
|
+
value: String(value),
|
|
26
|
+
max: String(max),
|
|
27
|
+
pendingValue: String(pendingValue),
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
if (max === 100) {
|
|
31
|
+
return t("ProgressIndicator.ariaLabelPercent", { value: String(value) });
|
|
32
|
+
}
|
|
33
|
+
return t("ProgressIndicator.ariaLabelRatio", {
|
|
34
|
+
value: String(value),
|
|
35
|
+
max: String(max),
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Clamp a raw progress amount to a percentage of `max` in the range 0..100.
|
|
41
|
+
*/
|
|
42
|
+
function toPercent(amount, max) {
|
|
43
|
+
if (max <= 0)
|
|
44
|
+
return 0;
|
|
45
|
+
return Math.min(100, Math.max(0, (amount / max) * 100));
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* File-private renderer for `variation="continuous"`. Renders the fill and,
|
|
49
|
+
* when `pendingValue > 0`, a pending band. The outer `ProgressIndicator` shell
|
|
50
|
+
* owns the accessibility contract; this renderer emits presentational children
|
|
51
|
+
* only.
|
|
52
|
+
*/
|
|
53
|
+
function ProgressIndicatorContinuous({ value, max, pendingValue, size, }) {
|
|
54
|
+
const styles = useStyles();
|
|
55
|
+
const radius = sizeHeights[size] / 2;
|
|
56
|
+
const fillPercent = toPercent(value, max);
|
|
57
|
+
const pendingPercent = toPercent(value + pendingValue, max);
|
|
58
|
+
// The width transition is intentionally preserved regardless of the user's
|
|
59
|
+
// reduce-motion preference: the slide between progress values conveys
|
|
60
|
+
// functional progress and is exempt under WCAG 2.3.3 ("Animation from
|
|
61
|
+
// Interactions"). See design.md.
|
|
62
|
+
const fill = useSharedValue(fillPercent);
|
|
63
|
+
const pending = useSharedValue(pendingPercent);
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
fill.value = withTiming(fillPercent, {
|
|
66
|
+
duration: tokens["timing-base"],
|
|
67
|
+
easing: Easing.out(Easing.ease),
|
|
68
|
+
});
|
|
69
|
+
}, [fillPercent]);
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
pending.value = withTiming(pendingPercent, {
|
|
72
|
+
duration: tokens["timing-base"],
|
|
73
|
+
easing: Easing.out(Easing.ease),
|
|
74
|
+
});
|
|
75
|
+
}, [pendingPercent]);
|
|
76
|
+
const fillStyle = useAnimatedStyle(() => ({ width: `${fill.value}%` }));
|
|
77
|
+
const pendingStyle = useAnimatedStyle(() => ({ width: `${pending.value}%` }));
|
|
78
|
+
return (React.createElement(React.Fragment, null,
|
|
79
|
+
pendingValue > 0 && (React.createElement(Animated.View, { testID: "progressindicator-pending", style: [styles.pending, { borderRadius: radius }, pendingStyle] })),
|
|
80
|
+
React.createElement(Animated.View, { testID: "progressindicator-fill", style: [styles.fill, { borderRadius: radius }, fillStyle] })));
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* File-private renderer for `variation="stepped"`. Renders `max` presentational
|
|
84
|
+
* segments, each classified filled / pending / empty. No accessibility props on
|
|
85
|
+
* the segments — the outer shell announces once.
|
|
86
|
+
*/
|
|
87
|
+
function ProgressIndicatorStepped({ value, max, pendingValue, size, }) {
|
|
88
|
+
const styles = useStyles();
|
|
89
|
+
const radius = sizeHeights[size] / 2;
|
|
90
|
+
const segmentCount = Math.max(0, Math.floor(max));
|
|
91
|
+
return (React.createElement(React.Fragment, null, Array.from({ length: segmentCount }).map((_, index) => {
|
|
92
|
+
const step = index + 1;
|
|
93
|
+
const isFilled = step <= value;
|
|
94
|
+
const isPending = !isFilled && step <= value + pendingValue;
|
|
95
|
+
return (React.createElement(View, { key: step, testID: isFilled
|
|
96
|
+
? "progressindicator-step-filled"
|
|
97
|
+
: isPending
|
|
98
|
+
? "progressindicator-step-pending"
|
|
99
|
+
: "progressindicator-step-empty", style: [
|
|
100
|
+
styles.segment,
|
|
101
|
+
{ borderRadius: radius },
|
|
102
|
+
isFilled && styles.segmentFilled,
|
|
103
|
+
isPending && styles.segmentPending,
|
|
104
|
+
] }));
|
|
105
|
+
})));
|
|
106
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { StyleSheet } from "react-native";
|
|
2
|
+
/**
|
|
3
|
+
* Bar heights per size. Matches the web `ProgressIndicator` ladder and today's
|
|
4
|
+
* mobile `ProgressBar` sizing.
|
|
5
|
+
*/
|
|
6
|
+
export const sizeHeights = {
|
|
7
|
+
smaller: 4,
|
|
8
|
+
small: 8,
|
|
9
|
+
base: 16,
|
|
10
|
+
};
|
|
11
|
+
export const sizeStyles = StyleSheet.create({
|
|
12
|
+
smaller: { height: sizeHeights.smaller },
|
|
13
|
+
small: { height: sizeHeights.small },
|
|
14
|
+
base: { height: sizeHeights.base },
|
|
15
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { buildThemedStyles } from "../AtlantisThemeContext";
|
|
2
|
+
/**
|
|
3
|
+
* ProgressIndicator
|
|
4
|
+
*
|
|
5
|
+
* Colour tokens mirror the web `ProgressIndicator`: the fill / filled segments
|
|
6
|
+
* use `color-interactive--subtle`; the track / empty segments use
|
|
7
|
+
* `color-surface--active`. The mobile-only pending band / pending segments use
|
|
8
|
+
* `color-informative`, carried forward from today's `ProgressBar` `inProgress`
|
|
9
|
+
* overlay.
|
|
10
|
+
*
|
|
11
|
+
* The bar height is set by the size style; the border radius is derived from
|
|
12
|
+
* that height at render time so the fill and segments stay pill-shaped at every
|
|
13
|
+
* size.
|
|
14
|
+
*/
|
|
15
|
+
export const useStyles = buildThemedStyles(tokens => ({
|
|
16
|
+
/* Continuous: the root IS the track; the fill / pending sit on top. */
|
|
17
|
+
continuous: {
|
|
18
|
+
width: "100%",
|
|
19
|
+
flexDirection: "row",
|
|
20
|
+
overflow: "hidden",
|
|
21
|
+
backgroundColor: tokens["color-surface--active"],
|
|
22
|
+
},
|
|
23
|
+
fill: {
|
|
24
|
+
height: "100%",
|
|
25
|
+
backgroundColor: tokens["color-interactive--subtle"],
|
|
26
|
+
},
|
|
27
|
+
pending: {
|
|
28
|
+
height: "100%",
|
|
29
|
+
position: "absolute",
|
|
30
|
+
left: 0,
|
|
31
|
+
top: 0,
|
|
32
|
+
backgroundColor: tokens["color-informative"],
|
|
33
|
+
},
|
|
34
|
+
/* Stepped: the root IS the flex row; segments are direct children. */
|
|
35
|
+
stepped: {
|
|
36
|
+
width: "100%",
|
|
37
|
+
flexDirection: "row",
|
|
38
|
+
gap: tokens["space-small"],
|
|
39
|
+
},
|
|
40
|
+
segment: {
|
|
41
|
+
flex: 1,
|
|
42
|
+
height: "100%",
|
|
43
|
+
backgroundColor: tokens["color-surface--active"],
|
|
44
|
+
},
|
|
45
|
+
segmentFilled: {
|
|
46
|
+
backgroundColor: tokens["color-interactive--subtle"],
|
|
47
|
+
},
|
|
48
|
+
segmentPending: {
|
|
49
|
+
backgroundColor: tokens["color-informative"],
|
|
50
|
+
},
|
|
51
|
+
}));
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { render, screen } from "@testing-library/react-native";
|
|
3
|
+
import { ProgressIndicator } from ".";
|
|
4
|
+
describe("ProgressIndicator", () => {
|
|
5
|
+
describe("public API", () => {
|
|
6
|
+
it("renders with role='progressbar'", () => {
|
|
7
|
+
render(React.createElement(ProgressIndicator, { value: 50 }));
|
|
8
|
+
expect(screen.getByRole("progressbar")).toBeTruthy();
|
|
9
|
+
});
|
|
10
|
+
it("sets accessibilityValue now/min/max", () => {
|
|
11
|
+
render(React.createElement(ProgressIndicator, { value: 50 }));
|
|
12
|
+
expect(screen.getByRole("progressbar").props.accessibilityValue).toEqual(expect.objectContaining({ now: 50, min: 0, max: 100 }));
|
|
13
|
+
});
|
|
14
|
+
it("uses max in accessibilityValue when provided", () => {
|
|
15
|
+
render(React.createElement(ProgressIndicator, { value: 3, max: 4 }));
|
|
16
|
+
expect(screen.getByRole("progressbar").props.accessibilityValue).toEqual(expect.objectContaining({ now: 3, max: 4 }));
|
|
17
|
+
});
|
|
18
|
+
it("renders a continuous fill by default", () => {
|
|
19
|
+
render(React.createElement(ProgressIndicator, { value: 50 }));
|
|
20
|
+
expect(screen.getByTestId("progressindicator-fill")).toBeTruthy();
|
|
21
|
+
expect(screen.queryByTestId("progressindicator-step-filled")).toBeNull();
|
|
22
|
+
});
|
|
23
|
+
it("applies style to the root", () => {
|
|
24
|
+
render(React.createElement(ProgressIndicator, { value: 50, style: { margin: 8 } }));
|
|
25
|
+
expect(screen.getByRole("progressbar").props.style).toEqual(expect.arrayContaining([expect.objectContaining({ margin: 8 })]));
|
|
26
|
+
});
|
|
27
|
+
it("rounds the continuous track so both ends are pill-shaped", () => {
|
|
28
|
+
render(React.createElement(ProgressIndicator, { value: 50, size: "base" }));
|
|
29
|
+
// base height is 16px, so the track radius is 8. Without this the
|
|
30
|
+
// unfilled (right) end of the track renders with square corners.
|
|
31
|
+
expect(screen.getByRole("progressbar").props.style).toEqual(expect.arrayContaining([expect.objectContaining({ borderRadius: 8 })]));
|
|
32
|
+
});
|
|
33
|
+
it("does not round the stepped root (segments carry their own radius)", () => {
|
|
34
|
+
render(React.createElement(ProgressIndicator, { value: 2, max: 5, variation: "stepped" }));
|
|
35
|
+
const rootStyles = screen
|
|
36
|
+
.getByRole("progressbar")
|
|
37
|
+
.props.style.filter(Boolean);
|
|
38
|
+
expect(rootStyles).not.toContainEqual(expect.objectContaining({ borderRadius: expect.anything() }));
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
describe("accessibility label", () => {
|
|
42
|
+
it("defaults to '{value}%' when max is omitted", () => {
|
|
43
|
+
render(React.createElement(ProgressIndicator, { value: 42 }));
|
|
44
|
+
expect(screen.getByLabelText("42%")).toBeTruthy();
|
|
45
|
+
});
|
|
46
|
+
it("defaults to '{value}%' when max === 100", () => {
|
|
47
|
+
render(React.createElement(ProgressIndicator, { value: 42, max: 100 }));
|
|
48
|
+
expect(screen.getByLabelText("42%")).toBeTruthy();
|
|
49
|
+
});
|
|
50
|
+
it("defaults to '{value} / {max}' when max !== 100", () => {
|
|
51
|
+
render(React.createElement(ProgressIndicator, { value: 3, max: 4 }));
|
|
52
|
+
expect(screen.getByLabelText("3 / 4")).toBeTruthy();
|
|
53
|
+
});
|
|
54
|
+
it("defaults to the pending template when pendingValue > 0", () => {
|
|
55
|
+
render(React.createElement(ProgressIndicator, { value: 2, pendingValue: 3, max: 10 }));
|
|
56
|
+
expect(screen.getByLabelText("2 of 10, 3 pending")).toBeTruthy();
|
|
57
|
+
});
|
|
58
|
+
it("uses a consumer-supplied accessibilityLabel when provided", () => {
|
|
59
|
+
render(React.createElement(ProgressIndicator, { value: 50, accessibilityLabel: "Uploading file" }));
|
|
60
|
+
expect(screen.getByLabelText("Uploading file")).toBeTruthy();
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
/**
|
|
64
|
+
* Native screen readers (VoiceOver / TalkBack) announce a progressbar from
|
|
65
|
+
* its `accessibilityRole` plus the resolved `accessibilityValue.text` /
|
|
66
|
+
* `accessibilityLabel`. These assertions lock that announced payload
|
|
67
|
+
* deterministically — the automatable proxy for the on-device smoke, which
|
|
68
|
+
* cannot capture live screen-reader speech in CI.
|
|
69
|
+
*/
|
|
70
|
+
describe("screen reader announcement contract", () => {
|
|
71
|
+
it.each([
|
|
72
|
+
["percent (max omitted)", { value: 50 }, "50%"],
|
|
73
|
+
["ratio (max !== 100)", { value: 3, max: 4 }, "3 / 4"],
|
|
74
|
+
["pending", { value: 2, pendingValue: 3, max: 10 }, "2 of 10, 3 pending"],
|
|
75
|
+
[
|
|
76
|
+
"custom label",
|
|
77
|
+
{ value: 50, accessibilityLabel: "Uploading file" },
|
|
78
|
+
"Uploading file",
|
|
79
|
+
],
|
|
80
|
+
])("announces %s as a progressbar with the derived text", (_n, props, text) => {
|
|
81
|
+
render(React.createElement(ProgressIndicator, Object.assign({}, props)));
|
|
82
|
+
const root = screen.getByRole("progressbar");
|
|
83
|
+
expect(root.props.accessibilityRole).toBe("progressbar");
|
|
84
|
+
expect(root.props.accessibilityLabel).toBe(text);
|
|
85
|
+
expect(root.props.accessibilityValue).toEqual(expect.objectContaining({ text }));
|
|
86
|
+
});
|
|
87
|
+
it("carries the numeric value/min/max alongside the announced text", () => {
|
|
88
|
+
render(React.createElement(ProgressIndicator, { value: 3, max: 4 }));
|
|
89
|
+
expect(screen.getByRole("progressbar").props.accessibilityValue).toEqual({
|
|
90
|
+
min: 0,
|
|
91
|
+
max: 4,
|
|
92
|
+
now: 3,
|
|
93
|
+
text: "3 / 4",
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
describe("sizes", () => {
|
|
98
|
+
it.each([
|
|
99
|
+
["smaller", 4],
|
|
100
|
+
["small", 8],
|
|
101
|
+
["base", 16],
|
|
102
|
+
])("applies %s height (%dpx) to the root", (size, height) => {
|
|
103
|
+
render(React.createElement(ProgressIndicator, { value: 50, size: size }));
|
|
104
|
+
expect(screen.getByRole("progressbar").props.style).toEqual(expect.arrayContaining([expect.objectContaining({ height })]));
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
describe("pending overlay (continuous)", () => {
|
|
108
|
+
it("renders a pending band when pendingValue > 0", () => {
|
|
109
|
+
render(React.createElement(ProgressIndicator, { value: 2, pendingValue: 3, max: 10 }));
|
|
110
|
+
expect(screen.getByTestId("progressindicator-pending")).toBeTruthy();
|
|
111
|
+
});
|
|
112
|
+
it("renders no pending band when pendingValue is 0", () => {
|
|
113
|
+
render(React.createElement(ProgressIndicator, { value: 4, max: 10 }));
|
|
114
|
+
expect(screen.queryByTestId("progressindicator-pending")).toBeNull();
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
describe("stepped variation", () => {
|
|
118
|
+
it("renders max segments with the first value filled", () => {
|
|
119
|
+
render(React.createElement(ProgressIndicator, { value: 2, max: 5, variation: "stepped" }));
|
|
120
|
+
expect(screen.getAllByTestId("progressindicator-step-filled")).toHaveLength(2);
|
|
121
|
+
expect(screen.getAllByTestId("progressindicator-step-empty")).toHaveLength(3);
|
|
122
|
+
});
|
|
123
|
+
it("classifies filled, pending, and empty segments", () => {
|
|
124
|
+
render(React.createElement(ProgressIndicator, { value: 3, pendingValue: 2, max: 6, variation: "stepped" }));
|
|
125
|
+
expect(screen.getAllByTestId("progressindicator-step-filled")).toHaveLength(3);
|
|
126
|
+
expect(screen.getAllByTestId("progressindicator-step-pending")).toHaveLength(2);
|
|
127
|
+
expect(screen.getAllByTestId("progressindicator-step-empty")).toHaveLength(1);
|
|
128
|
+
});
|
|
129
|
+
it("keeps a single progressbar on the root and none on segments", () => {
|
|
130
|
+
render(React.createElement(ProgressIndicator, { value: 2, max: 5, variation: "stepped" }));
|
|
131
|
+
expect(screen.getAllByRole("progressbar")).toHaveLength(1);
|
|
132
|
+
screen.getAllByTestId(/progressindicator-step-/).forEach(segment => {
|
|
133
|
+
expect(segment.props.accessibilityRole).toBeUndefined();
|
|
134
|
+
expect(segment.props.accessibilityValue).toBeUndefined();
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ProgressIndicator } from "./ProgressIndicator";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -26,6 +26,9 @@
|
|
|
26
26
|
"networkUnavailableDescription": "Check your internet connection and try again later.",
|
|
27
27
|
"ProgressBar.complete": "{current} of {total} complete",
|
|
28
28
|
"ProgressBar.inProgress": "{current} of {total} complete, {inProgress} in progress",
|
|
29
|
+
"ProgressIndicator.ariaLabelPercent": "{value}%",
|
|
30
|
+
"ProgressIndicator.ariaLabelRatio": "{value} / {max}",
|
|
31
|
+
"ProgressIndicator.ariaLabelPending": "{value} of {max}, {pendingValue} pending",
|
|
29
32
|
"save": "Save",
|
|
30
33
|
"Select.a11yHint": "Select to open the picker",
|
|
31
34
|
"Select.emptyValue": "Select an option",
|
|
@@ -26,6 +26,9 @@
|
|
|
26
26
|
"networkUnavailableDescription": "Compruebe su conexión a Internet e inténtelo de nuevo más adelante.",
|
|
27
27
|
"ProgressBar.complete": "{current} de {total} completo",
|
|
28
28
|
"ProgressBar.inProgress": "{current} de {total} completo, {inProgress} en progreso",
|
|
29
|
+
"ProgressIndicator.ariaLabelPercent": "{value}%",
|
|
30
|
+
"ProgressIndicator.ariaLabelRatio": "{value} / {max}",
|
|
31
|
+
"ProgressIndicator.ariaLabelPending": "{value} de {max}, {pendingValue} pendiente",
|
|
29
32
|
"save": "Guardar",
|
|
30
33
|
"Select.a11yHint": "Seleccionar para abrir el selector",
|
|
31
34
|
"Select.emptyValue": "Seleccione una opción",
|
package/dist/src/index.js
CHANGED