@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,3 +1,6 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import type { ProgressBarProps } from "./types";
|
|
3
|
+
/**
|
|
4
|
+
* @deprecated Use `ProgressIndicator` from `@jobber/components-native` instead.
|
|
5
|
+
*/
|
|
3
6
|
export declare function ProgressBar({ loading, total, current, inProgress, reverseTheme, header, variation, size, UNSAFE_style, }: ProgressBarProps): React.JSX.Element;
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
2
|
import type { StyleProp, ViewStyle } from "react-native";
|
|
3
|
+
/**
|
|
4
|
+
* @deprecated Use `ProgressIndicator` from `@jobber/components-native` instead.
|
|
5
|
+
*/
|
|
3
6
|
export interface ProgressBarProps {
|
|
4
7
|
/**
|
|
5
8
|
* The total number of items to be completed
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ProgressIndicatorProps } from "./types";
|
|
2
|
+
type Size = NonNullable<ProgressIndicatorProps["size"]>;
|
|
3
|
+
/**
|
|
4
|
+
* Bar heights per size. Matches the web `ProgressIndicator` ladder and today's
|
|
5
|
+
* mobile `ProgressBar` sizing.
|
|
6
|
+
*/
|
|
7
|
+
export declare const sizeHeights: Record<Size, number>;
|
|
8
|
+
export declare const sizeStyles: {
|
|
9
|
+
smaller: {
|
|
10
|
+
height: number;
|
|
11
|
+
};
|
|
12
|
+
small: {
|
|
13
|
+
height: number;
|
|
14
|
+
};
|
|
15
|
+
base: {
|
|
16
|
+
height: number;
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ProgressIndicator
|
|
3
|
+
*
|
|
4
|
+
* Colour tokens mirror the web `ProgressIndicator`: the fill / filled segments
|
|
5
|
+
* use `color-interactive--subtle`; the track / empty segments use
|
|
6
|
+
* `color-surface--active`. The mobile-only pending band / pending segments use
|
|
7
|
+
* `color-informative`, carried forward from today's `ProgressBar` `inProgress`
|
|
8
|
+
* overlay.
|
|
9
|
+
*
|
|
10
|
+
* The bar height is set by the size style; the border radius is derived from
|
|
11
|
+
* that height at render time so the fill and segments stay pill-shaped at every
|
|
12
|
+
* size.
|
|
13
|
+
*/
|
|
14
|
+
export declare const useStyles: () => {
|
|
15
|
+
continuous: {
|
|
16
|
+
width: "100%";
|
|
17
|
+
flexDirection: "row";
|
|
18
|
+
overflow: "hidden";
|
|
19
|
+
backgroundColor: string;
|
|
20
|
+
};
|
|
21
|
+
fill: {
|
|
22
|
+
height: "100%";
|
|
23
|
+
backgroundColor: string;
|
|
24
|
+
};
|
|
25
|
+
pending: {
|
|
26
|
+
height: "100%";
|
|
27
|
+
position: "absolute";
|
|
28
|
+
left: number;
|
|
29
|
+
top: number;
|
|
30
|
+
backgroundColor: string;
|
|
31
|
+
};
|
|
32
|
+
stepped: {
|
|
33
|
+
width: "100%";
|
|
34
|
+
flexDirection: "row";
|
|
35
|
+
gap: number;
|
|
36
|
+
};
|
|
37
|
+
segment: {
|
|
38
|
+
flex: number;
|
|
39
|
+
height: "100%";
|
|
40
|
+
backgroundColor: string;
|
|
41
|
+
};
|
|
42
|
+
segmentFilled: {
|
|
43
|
+
backgroundColor: string;
|
|
44
|
+
};
|
|
45
|
+
segmentPending: {
|
|
46
|
+
backgroundColor: string;
|
|
47
|
+
};
|
|
48
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { StyleProp, ViewStyle } from "react-native";
|
|
2
|
+
export interface ProgressIndicatorProps {
|
|
3
|
+
/**
|
|
4
|
+
* Current progress. Expected to be in the range `0..max` inclusive.
|
|
5
|
+
*/
|
|
6
|
+
readonly value: number;
|
|
7
|
+
/**
|
|
8
|
+
* Maximum value.
|
|
9
|
+
* @default 100
|
|
10
|
+
*/
|
|
11
|
+
readonly max?: number;
|
|
12
|
+
/**
|
|
13
|
+
* Additional in-flight progress beyond `value`, measured on the same scale as
|
|
14
|
+
* `value` and `max`. The bar visually fills `value + pendingValue` of `max`,
|
|
15
|
+
* with the `value` portion shown as completed and the `pendingValue` portion
|
|
16
|
+
* shown as pending. Mobile-only; there is no web counterpart.
|
|
17
|
+
* @default 0
|
|
18
|
+
*/
|
|
19
|
+
readonly pendingValue?: number;
|
|
20
|
+
/**
|
|
21
|
+
* Set the variation of the progress indicator. `continuous` renders a single
|
|
22
|
+
* fill; `stepped` renders one segment per integer step from `1..max`.
|
|
23
|
+
* @default continuous
|
|
24
|
+
*/
|
|
25
|
+
readonly variation?: "continuous" | "stepped";
|
|
26
|
+
/**
|
|
27
|
+
* Set the size of the progress indicator. Corresponds to bar heights of
|
|
28
|
+
* 4px / 8px / 16px.
|
|
29
|
+
* @default base
|
|
30
|
+
*/
|
|
31
|
+
readonly size?: "smaller" | "small" | "base";
|
|
32
|
+
/**
|
|
33
|
+
* Accessible label override. When omitted, the label is derived via
|
|
34
|
+
* `useAtlantisI18n`: `"{value}%"` when `max` is `100` (or omitted) and
|
|
35
|
+
* `pendingValue` is `0`; `"{value} / {max}"` when `max` is not `100` and
|
|
36
|
+
* `pendingValue` is `0`; `"{value} of {max}, {pendingValue} pending"` when
|
|
37
|
+
* `pendingValue` is greater than `0`.
|
|
38
|
+
*/
|
|
39
|
+
readonly accessibilityLabel?: string;
|
|
40
|
+
/**
|
|
41
|
+
* Custom style applied to the root element.
|
|
42
|
+
*/
|
|
43
|
+
readonly style?: StyleProp<ViewStyle>;
|
|
44
|
+
}
|
package/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,5 @@
|
|
|
1
|
+
/* eslint-disable import/no-deprecated -- This file documents the deprecated
|
|
2
|
+
* ProgressBar component until it is removed or shimmed. */
|
|
1
3
|
import type { ComponentProps } from "react";
|
|
2
4
|
import type { Meta, StoryObj } from "@storybook/react-native-web-vite";
|
|
3
5
|
import { ProgressBar } from "@jobber/components-native";
|
|
@@ -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 type { ProgressBarProps } from ".";
|
|
@@ -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 type { ProgressBarProps } from "./types";
|
|
@@ -8,6 +11,9 @@ import { sizeStyles } from "./ProgressBar.size.style";
|
|
|
8
11
|
import { useAtlantisI18n } from "../hooks/useAtlantisI18n";
|
|
9
12
|
import { useAtlantisTheme } from "../AtlantisThemeContext";
|
|
10
13
|
|
|
14
|
+
/**
|
|
15
|
+
* @deprecated Use `ProgressIndicator` from `@jobber/components-native` instead.
|
|
16
|
+
*/
|
|
11
17
|
export function ProgressBar({
|
|
12
18
|
loading,
|
|
13
19
|
total,
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* eslint-disable import/no-deprecated -- This example documents the deprecated
|
|
2
|
+
* ProgressBar component until it is removed or shimmed. */
|
|
1
3
|
import React from "react";
|
|
2
4
|
import { ProgressBar } from "@jobber/components-native";
|
|
3
5
|
import type { ProgressBarProps } from "@jobber/components-native";
|
package/src/ProgressBar/types.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
2
|
import type { StyleProp, ViewStyle } from "react-native";
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* @deprecated Use `ProgressIndicator` from `@jobber/components-native` instead.
|
|
6
|
+
*/
|
|
4
7
|
export interface ProgressBarProps {
|
|
5
8
|
/**
|
|
6
9
|
* The total number of items to be completed
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { StyleSheet } from "react-native";
|
|
2
|
+
import type { ProgressIndicatorProps } from "./types";
|
|
3
|
+
|
|
4
|
+
type Size = NonNullable<ProgressIndicatorProps["size"]>;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Bar heights per size. Matches the web `ProgressIndicator` ladder and today's
|
|
8
|
+
* mobile `ProgressBar` sizing.
|
|
9
|
+
*/
|
|
10
|
+
export const sizeHeights: Record<Size, number> = {
|
|
11
|
+
smaller: 4,
|
|
12
|
+
small: 8,
|
|
13
|
+
base: 16,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const sizeStyles = StyleSheet.create({
|
|
17
|
+
smaller: { height: sizeHeights.smaller },
|
|
18
|
+
small: { height: sizeHeights.small },
|
|
19
|
+
base: { height: sizeHeights.base },
|
|
20
|
+
});
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import React, { useEffect, useState } from "react";
|
|
2
|
+
import { View } from "react-native";
|
|
3
|
+
import type { ComponentProps } from "react";
|
|
4
|
+
import type { Meta, StoryObj } from "@storybook/react-native-web-vite";
|
|
5
|
+
import { ProgressIndicator } from "@jobber/components-native";
|
|
6
|
+
import { ProgressIndicatorBasic, ProgressIndicatorPending } from "./docs";
|
|
7
|
+
|
|
8
|
+
type ProgressIndicatorStoryArgs = Partial<
|
|
9
|
+
Pick<
|
|
10
|
+
ComponentProps<typeof ProgressIndicator>,
|
|
11
|
+
| "value"
|
|
12
|
+
| "max"
|
|
13
|
+
| "pendingValue"
|
|
14
|
+
| "variation"
|
|
15
|
+
| "size"
|
|
16
|
+
| "accessibilityLabel"
|
|
17
|
+
>
|
|
18
|
+
>;
|
|
19
|
+
|
|
20
|
+
const meta = {
|
|
21
|
+
title: "Components/Status and Feedback/ProgressIndicator",
|
|
22
|
+
component: ProgressIndicator,
|
|
23
|
+
parameters: {
|
|
24
|
+
viewport: { defaultViewport: "mobile1" },
|
|
25
|
+
showNativeOnWebDisclaimer: true,
|
|
26
|
+
},
|
|
27
|
+
argTypes: {
|
|
28
|
+
variation: {
|
|
29
|
+
control: { type: "radio" },
|
|
30
|
+
options: ["continuous", "stepped"],
|
|
31
|
+
},
|
|
32
|
+
size: {
|
|
33
|
+
control: { type: "radio" },
|
|
34
|
+
options: ["smaller", "small", "base"],
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
} satisfies Meta<typeof ProgressIndicator>;
|
|
38
|
+
|
|
39
|
+
export default meta;
|
|
40
|
+
|
|
41
|
+
type Story = StoryObj<ProgressIndicatorStoryArgs>;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The default continuous bar, filled to `value` out of `max`.
|
|
45
|
+
*/
|
|
46
|
+
export const Basic: Story = {
|
|
47
|
+
render: ProgressIndicatorBasic,
|
|
48
|
+
args: {
|
|
49
|
+
value: 75,
|
|
50
|
+
size: "base",
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The three sizes stacked so the heights (4 / 8 / 16px) are visually
|
|
56
|
+
* comparable.
|
|
57
|
+
*/
|
|
58
|
+
export const Sizes: Story = {
|
|
59
|
+
render: args => (
|
|
60
|
+
<View style={{ gap: 16 }}>
|
|
61
|
+
<ProgressIndicator {...args} value={args.value ?? 50} size="smaller" />
|
|
62
|
+
<ProgressIndicator {...args} value={args.value ?? 50} size="small" />
|
|
63
|
+
<ProgressIndicator {...args} value={args.value ?? 50} size="base" />
|
|
64
|
+
</View>
|
|
65
|
+
),
|
|
66
|
+
args: {
|
|
67
|
+
value: 50,
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The stepped variation renders one segment per step and fills the first
|
|
73
|
+
* `value` of `max`.
|
|
74
|
+
*/
|
|
75
|
+
export const Stepped: Story = {
|
|
76
|
+
render: ProgressIndicatorBasic,
|
|
77
|
+
args: {
|
|
78
|
+
value: 2,
|
|
79
|
+
max: 5,
|
|
80
|
+
variation: "stepped",
|
|
81
|
+
size: "base",
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Mobile-only pending overlay: `value` of `max` completed plus an additional
|
|
87
|
+
* `pendingValue` of `max` in flight.
|
|
88
|
+
*/
|
|
89
|
+
export const Pending: Story = {
|
|
90
|
+
render: ProgressIndicatorPending,
|
|
91
|
+
args: {
|
|
92
|
+
value: 2,
|
|
93
|
+
max: 10,
|
|
94
|
+
pendingValue: 3,
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The stepped variation with a pending overlay: the first `value` segments are
|
|
100
|
+
* filled and the next `pendingValue` segments are shown as in flight.
|
|
101
|
+
*/
|
|
102
|
+
export const SteppedPending: Story = {
|
|
103
|
+
render: ProgressIndicatorPending,
|
|
104
|
+
args: {
|
|
105
|
+
value: 3,
|
|
106
|
+
max: 6,
|
|
107
|
+
pendingValue: 2,
|
|
108
|
+
variation: "stepped",
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Demonstrates the default `"{value}%"` accessibility label when `max` is
|
|
114
|
+
* omitted or equal to 100.
|
|
115
|
+
*/
|
|
116
|
+
export const Percentage: Story = {
|
|
117
|
+
render: ProgressIndicatorBasic,
|
|
118
|
+
args: {
|
|
119
|
+
value: 42,
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Demonstrates the default `"{value} / {max}"` accessibility label when
|
|
125
|
+
* `max !== 100`.
|
|
126
|
+
*/
|
|
127
|
+
export const Ratio: Story = {
|
|
128
|
+
render: ProgressIndicatorBasic,
|
|
129
|
+
args: {
|
|
130
|
+
value: 3,
|
|
131
|
+
max: 4,
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Drives `value` over time so the determinate width transition is visible. The
|
|
137
|
+
* transition is intentionally preserved under a reduce-motion preference
|
|
138
|
+
* because it conveys functional progress (WCAG 2.3.3).
|
|
139
|
+
*/
|
|
140
|
+
export const WithState: Story = {
|
|
141
|
+
render: function WithStateStory() {
|
|
142
|
+
const [value, setValue] = useState(0);
|
|
143
|
+
|
|
144
|
+
useEffect(() => {
|
|
145
|
+
const interval = setInterval(() => {
|
|
146
|
+
setValue(current => (current >= 100 ? 0 : current + 10));
|
|
147
|
+
}, 1000);
|
|
148
|
+
|
|
149
|
+
return () => clearInterval(interval);
|
|
150
|
+
}, []);
|
|
151
|
+
|
|
152
|
+
return <ProgressIndicator value={value} />;
|
|
153
|
+
},
|
|
154
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { buildThemedStyles } from "../AtlantisThemeContext";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ProgressIndicator
|
|
5
|
+
*
|
|
6
|
+
* Colour tokens mirror the web `ProgressIndicator`: the fill / filled segments
|
|
7
|
+
* use `color-interactive--subtle`; the track / empty segments use
|
|
8
|
+
* `color-surface--active`. The mobile-only pending band / pending segments use
|
|
9
|
+
* `color-informative`, carried forward from today's `ProgressBar` `inProgress`
|
|
10
|
+
* overlay.
|
|
11
|
+
*
|
|
12
|
+
* The bar height is set by the size style; the border radius is derived from
|
|
13
|
+
* that height at render time so the fill and segments stay pill-shaped at every
|
|
14
|
+
* size.
|
|
15
|
+
*/
|
|
16
|
+
export const useStyles = buildThemedStyles(tokens => ({
|
|
17
|
+
/* Continuous: the root IS the track; the fill / pending sit on top. */
|
|
18
|
+
continuous: {
|
|
19
|
+
width: "100%",
|
|
20
|
+
flexDirection: "row",
|
|
21
|
+
overflow: "hidden",
|
|
22
|
+
backgroundColor: tokens["color-surface--active"],
|
|
23
|
+
},
|
|
24
|
+
fill: {
|
|
25
|
+
height: "100%",
|
|
26
|
+
backgroundColor: tokens["color-interactive--subtle"],
|
|
27
|
+
},
|
|
28
|
+
pending: {
|
|
29
|
+
height: "100%",
|
|
30
|
+
position: "absolute",
|
|
31
|
+
left: 0,
|
|
32
|
+
top: 0,
|
|
33
|
+
backgroundColor: tokens["color-informative"],
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
/* Stepped: the root IS the flex row; segments are direct children. */
|
|
37
|
+
stepped: {
|
|
38
|
+
width: "100%",
|
|
39
|
+
flexDirection: "row",
|
|
40
|
+
gap: tokens["space-small"],
|
|
41
|
+
},
|
|
42
|
+
segment: {
|
|
43
|
+
flex: 1,
|
|
44
|
+
height: "100%",
|
|
45
|
+
backgroundColor: tokens["color-surface--active"],
|
|
46
|
+
},
|
|
47
|
+
segmentFilled: {
|
|
48
|
+
backgroundColor: tokens["color-interactive--subtle"],
|
|
49
|
+
},
|
|
50
|
+
segmentPending: {
|
|
51
|
+
backgroundColor: tokens["color-informative"],
|
|
52
|
+
},
|
|
53
|
+
}));
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { render, screen } from "@testing-library/react-native";
|
|
3
|
+
import { ProgressIndicator } from ".";
|
|
4
|
+
|
|
5
|
+
describe("ProgressIndicator", () => {
|
|
6
|
+
describe("public API", () => {
|
|
7
|
+
it("renders with role='progressbar'", () => {
|
|
8
|
+
render(<ProgressIndicator value={50} />);
|
|
9
|
+
|
|
10
|
+
expect(screen.getByRole("progressbar")).toBeTruthy();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("sets accessibilityValue now/min/max", () => {
|
|
14
|
+
render(<ProgressIndicator value={50} />);
|
|
15
|
+
|
|
16
|
+
expect(screen.getByRole("progressbar").props.accessibilityValue).toEqual(
|
|
17
|
+
expect.objectContaining({ now: 50, min: 0, max: 100 }),
|
|
18
|
+
);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("uses max in accessibilityValue when provided", () => {
|
|
22
|
+
render(<ProgressIndicator value={3} max={4} />);
|
|
23
|
+
|
|
24
|
+
expect(screen.getByRole("progressbar").props.accessibilityValue).toEqual(
|
|
25
|
+
expect.objectContaining({ now: 3, max: 4 }),
|
|
26
|
+
);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("renders a continuous fill by default", () => {
|
|
30
|
+
render(<ProgressIndicator value={50} />);
|
|
31
|
+
|
|
32
|
+
expect(screen.getByTestId("progressindicator-fill")).toBeTruthy();
|
|
33
|
+
expect(screen.queryByTestId("progressindicator-step-filled")).toBeNull();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("applies style to the root", () => {
|
|
37
|
+
render(<ProgressIndicator value={50} style={{ margin: 8 }} />);
|
|
38
|
+
|
|
39
|
+
expect(screen.getByRole("progressbar").props.style).toEqual(
|
|
40
|
+
expect.arrayContaining([expect.objectContaining({ margin: 8 })]),
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("rounds the continuous track so both ends are pill-shaped", () => {
|
|
45
|
+
render(<ProgressIndicator value={50} size="base" />);
|
|
46
|
+
|
|
47
|
+
// base height is 16px, so the track radius is 8. Without this the
|
|
48
|
+
// unfilled (right) end of the track renders with square corners.
|
|
49
|
+
expect(screen.getByRole("progressbar").props.style).toEqual(
|
|
50
|
+
expect.arrayContaining([expect.objectContaining({ borderRadius: 8 })]),
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("does not round the stepped root (segments carry their own radius)", () => {
|
|
55
|
+
render(<ProgressIndicator value={2} max={5} variation="stepped" />);
|
|
56
|
+
|
|
57
|
+
const rootStyles = screen
|
|
58
|
+
.getByRole("progressbar")
|
|
59
|
+
.props.style.filter(Boolean);
|
|
60
|
+
expect(rootStyles).not.toContainEqual(
|
|
61
|
+
expect.objectContaining({ borderRadius: expect.anything() }),
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe("accessibility label", () => {
|
|
67
|
+
it("defaults to '{value}%' when max is omitted", () => {
|
|
68
|
+
render(<ProgressIndicator value={42} />);
|
|
69
|
+
|
|
70
|
+
expect(screen.getByLabelText("42%")).toBeTruthy();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("defaults to '{value}%' when max === 100", () => {
|
|
74
|
+
render(<ProgressIndicator value={42} max={100} />);
|
|
75
|
+
|
|
76
|
+
expect(screen.getByLabelText("42%")).toBeTruthy();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("defaults to '{value} / {max}' when max !== 100", () => {
|
|
80
|
+
render(<ProgressIndicator value={3} max={4} />);
|
|
81
|
+
|
|
82
|
+
expect(screen.getByLabelText("3 / 4")).toBeTruthy();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("defaults to the pending template when pendingValue > 0", () => {
|
|
86
|
+
render(<ProgressIndicator value={2} pendingValue={3} max={10} />);
|
|
87
|
+
|
|
88
|
+
expect(screen.getByLabelText("2 of 10, 3 pending")).toBeTruthy();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("uses a consumer-supplied accessibilityLabel when provided", () => {
|
|
92
|
+
render(
|
|
93
|
+
<ProgressIndicator value={50} accessibilityLabel="Uploading file" />,
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
expect(screen.getByLabelText("Uploading file")).toBeTruthy();
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Native screen readers (VoiceOver / TalkBack) announce a progressbar from
|
|
102
|
+
* its `accessibilityRole` plus the resolved `accessibilityValue.text` /
|
|
103
|
+
* `accessibilityLabel`. These assertions lock that announced payload
|
|
104
|
+
* deterministically — the automatable proxy for the on-device smoke, which
|
|
105
|
+
* cannot capture live screen-reader speech in CI.
|
|
106
|
+
*/
|
|
107
|
+
describe("screen reader announcement contract", () => {
|
|
108
|
+
it.each([
|
|
109
|
+
["percent (max omitted)", { value: 50 }, "50%"],
|
|
110
|
+
["ratio (max !== 100)", { value: 3, max: 4 }, "3 / 4"],
|
|
111
|
+
["pending", { value: 2, pendingValue: 3, max: 10 }, "2 of 10, 3 pending"],
|
|
112
|
+
[
|
|
113
|
+
"custom label",
|
|
114
|
+
{ value: 50, accessibilityLabel: "Uploading file" },
|
|
115
|
+
"Uploading file",
|
|
116
|
+
],
|
|
117
|
+
])(
|
|
118
|
+
"announces %s as a progressbar with the derived text",
|
|
119
|
+
(_n, props, text) => {
|
|
120
|
+
render(<ProgressIndicator {...props} />);
|
|
121
|
+
|
|
122
|
+
const root = screen.getByRole("progressbar");
|
|
123
|
+
expect(root.props.accessibilityRole).toBe("progressbar");
|
|
124
|
+
expect(root.props.accessibilityLabel).toBe(text);
|
|
125
|
+
expect(root.props.accessibilityValue).toEqual(
|
|
126
|
+
expect.objectContaining({ text }),
|
|
127
|
+
);
|
|
128
|
+
},
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
it("carries the numeric value/min/max alongside the announced text", () => {
|
|
132
|
+
render(<ProgressIndicator value={3} max={4} />);
|
|
133
|
+
|
|
134
|
+
expect(screen.getByRole("progressbar").props.accessibilityValue).toEqual({
|
|
135
|
+
min: 0,
|
|
136
|
+
max: 4,
|
|
137
|
+
now: 3,
|
|
138
|
+
text: "3 / 4",
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe("sizes", () => {
|
|
144
|
+
it.each([
|
|
145
|
+
["smaller", 4],
|
|
146
|
+
["small", 8],
|
|
147
|
+
["base", 16],
|
|
148
|
+
] as const)("applies %s height (%dpx) to the root", (size, height) => {
|
|
149
|
+
render(<ProgressIndicator value={50} size={size} />);
|
|
150
|
+
|
|
151
|
+
expect(screen.getByRole("progressbar").props.style).toEqual(
|
|
152
|
+
expect.arrayContaining([expect.objectContaining({ height })]),
|
|
153
|
+
);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe("pending overlay (continuous)", () => {
|
|
158
|
+
it("renders a pending band when pendingValue > 0", () => {
|
|
159
|
+
render(<ProgressIndicator value={2} pendingValue={3} max={10} />);
|
|
160
|
+
|
|
161
|
+
expect(screen.getByTestId("progressindicator-pending")).toBeTruthy();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("renders no pending band when pendingValue is 0", () => {
|
|
165
|
+
render(<ProgressIndicator value={4} max={10} />);
|
|
166
|
+
|
|
167
|
+
expect(screen.queryByTestId("progressindicator-pending")).toBeNull();
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe("stepped variation", () => {
|
|
172
|
+
it("renders max segments with the first value filled", () => {
|
|
173
|
+
render(<ProgressIndicator value={2} max={5} variation="stepped" />);
|
|
174
|
+
|
|
175
|
+
expect(
|
|
176
|
+
screen.getAllByTestId("progressindicator-step-filled"),
|
|
177
|
+
).toHaveLength(2);
|
|
178
|
+
expect(
|
|
179
|
+
screen.getAllByTestId("progressindicator-step-empty"),
|
|
180
|
+
).toHaveLength(3);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("classifies filled, pending, and empty segments", () => {
|
|
184
|
+
render(
|
|
185
|
+
<ProgressIndicator
|
|
186
|
+
value={3}
|
|
187
|
+
pendingValue={2}
|
|
188
|
+
max={6}
|
|
189
|
+
variation="stepped"
|
|
190
|
+
/>,
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
expect(
|
|
194
|
+
screen.getAllByTestId("progressindicator-step-filled"),
|
|
195
|
+
).toHaveLength(3);
|
|
196
|
+
expect(
|
|
197
|
+
screen.getAllByTestId("progressindicator-step-pending"),
|
|
198
|
+
).toHaveLength(2);
|
|
199
|
+
expect(
|
|
200
|
+
screen.getAllByTestId("progressindicator-step-empty"),
|
|
201
|
+
).toHaveLength(1);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("keeps a single progressbar on the root and none on segments", () => {
|
|
205
|
+
render(<ProgressIndicator value={2} max={5} variation="stepped" />);
|
|
206
|
+
|
|
207
|
+
expect(screen.getAllByRole("progressbar")).toHaveLength(1);
|
|
208
|
+
screen.getAllByTestId(/progressindicator-step-/).forEach(segment => {
|
|
209
|
+
expect(segment.props.accessibilityRole).toBeUndefined();
|
|
210
|
+
expect(segment.props.accessibilityValue).toBeUndefined();
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
});
|