@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.
Files changed (48) hide show
  1. package/dist/docs/ProgressBar/ProgressBar.md +1 -1
  2. package/dist/docs/ProgressIndicator/ProgressIndicator.md +117 -0
  3. package/dist/docs/index.md +1 -0
  4. package/dist/package.json +2 -2
  5. package/dist/src/ProgressBar/ProgressBar.js +6 -0
  6. package/dist/src/ProgressBar/ProgressBar.test.js +2 -0
  7. package/dist/src/ProgressIndicator/ProgressIndicator.js +106 -0
  8. package/dist/src/ProgressIndicator/ProgressIndicator.size.style.js +15 -0
  9. package/dist/src/ProgressIndicator/ProgressIndicator.style.js +51 -0
  10. package/dist/src/ProgressIndicator/ProgressIndicator.test.js +138 -0
  11. package/dist/src/ProgressIndicator/index.js +1 -0
  12. package/dist/src/ProgressIndicator/types.js +1 -0
  13. package/dist/src/hooks/useAtlantisI18n/locales/en.json +3 -0
  14. package/dist/src/hooks/useAtlantisI18n/locales/es.json +3 -0
  15. package/dist/src/index.js +1 -0
  16. package/dist/src/utils/meta/meta.json +1 -0
  17. package/dist/tsconfig.build.tsbuildinfo +1 -1
  18. package/dist/types/src/ProgressBar/ProgressBar.d.ts +3 -0
  19. package/dist/types/src/ProgressBar/types.d.ts +3 -0
  20. package/dist/types/src/ProgressIndicator/ProgressIndicator.d.ts +3 -0
  21. package/dist/types/src/ProgressIndicator/ProgressIndicator.size.style.d.ts +19 -0
  22. package/dist/types/src/ProgressIndicator/ProgressIndicator.style.d.ts +48 -0
  23. package/dist/types/src/ProgressIndicator/ProgressIndicator.test.d.ts +1 -0
  24. package/dist/types/src/ProgressIndicator/index.d.ts +2 -0
  25. package/dist/types/src/ProgressIndicator/types.d.ts +44 -0
  26. package/dist/types/src/index.d.ts +1 -0
  27. package/package.json +2 -2
  28. package/src/ProgressBar/ProgressBar.stories.tsx +2 -0
  29. package/src/ProgressBar/ProgressBar.test.tsx +2 -0
  30. package/src/ProgressBar/ProgressBar.tsx +6 -0
  31. package/src/ProgressBar/docs/ProgressBarBasic.tsx +2 -0
  32. package/src/ProgressBar/docs/ProgressBarWithHeader.tsx +2 -0
  33. package/src/ProgressBar/types.ts +3 -0
  34. package/src/ProgressIndicator/ProgressIndicator.size.style.ts +20 -0
  35. package/src/ProgressIndicator/ProgressIndicator.stories.tsx +154 -0
  36. package/src/ProgressIndicator/ProgressIndicator.style.ts +53 -0
  37. package/src/ProgressIndicator/ProgressIndicator.test.tsx +214 -0
  38. package/src/ProgressIndicator/ProgressIndicator.tsx +198 -0
  39. package/src/ProgressIndicator/docs/ProgressIndicatorBasic.tsx +22 -0
  40. package/src/ProgressIndicator/docs/ProgressIndicatorPending.tsx +22 -0
  41. package/src/ProgressIndicator/docs/index.ts +2 -0
  42. package/src/ProgressIndicator/guide.md +104 -0
  43. package/src/ProgressIndicator/index.ts +2 -0
  44. package/src/ProgressIndicator/types.ts +51 -0
  45. package/src/hooks/useAtlantisI18n/locales/en.json +3 -0
  46. package/src/hooks/useAtlantisI18n/locales/es.json +3 -0
  47. package/src/index.ts +1 -0
  48. package/src/utils/meta/meta.json +1 -0
@@ -1,6 +1,6 @@
1
1
  # Progress Bar
2
2
 
3
- > **Deprecated.** Use [ProgressIndicator](/components/ProgressIndicator) for new
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... |
@@ -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.109.1",
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": "879bbb072a470c876e4ee7e9a9757f5edb0f3b82"
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();
@@ -1,3 +1,5 @@
1
+ /* eslint-disable import/no-deprecated -- This file tests the deprecated
2
+ * ProgressBar component until it is removed or shimmed. */
1
3
  import React from "react";
2
4
  import { render, screen } from "@testing-library/react-native";
3
5
  import { ProgressBar } from ".";
@@ -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
@@ -37,6 +37,7 @@ export * from "./InputSearch";
37
37
  export * from "./InputText";
38
38
  export * from "./InputTime";
39
39
  export * from "./ProgressBar";
40
+ export * from "./ProgressIndicator";
40
41
  export * from "./Select";
41
42
  export * from "./StatusLabel";
42
43
  export * from "./Switch";
@@ -59,6 +59,7 @@
59
59
  "InputTime",
60
60
  "Option",
61
61
  "ProgressBar",
62
+ "ProgressIndicator",
62
63
  "Select",
63
64
  "Select.Content",
64
65
  "Select.Group",