@jobber/components-native 0.113.0 → 0.113.2
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/package.json +3 -4
- package/dist/src/ActivityIndicator/ActivityIndicator.js +40 -10
- package/dist/src/ActivityIndicator/ActivityIndicator.test.js +35 -0
- package/dist/src/FormatFile/components/MediaView/MediaView.js +10 -8
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +3 -4
- package/src/ActivityIndicator/ActivityIndicator.test.tsx +46 -0
- package/src/ActivityIndicator/ActivityIndicator.tsx +98 -32
- package/src/ActivityIndicator/guide.md +43 -18
- package/src/FormatFile/components/MediaView/MediaView.tsx +12 -2
package/dist/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jobber/components-native",
|
|
3
|
-
"version": "0.113.
|
|
3
|
+
"version": "0.113.2",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "React Native implementation of Atlantis",
|
|
6
6
|
"repository": {
|
|
@@ -53,8 +53,7 @@
|
|
|
53
53
|
"compile": "tsc -p tsconfig.build.json",
|
|
54
54
|
"build:clean": "rm -rf ./dist",
|
|
55
55
|
"storybook": "storybook dev -p 6008 --disable-telemetry",
|
|
56
|
-
"storybook:build": "storybook build --disable-telemetry"
|
|
57
|
-
"lint:locales": "node scripts/check-locale-parity.mjs"
|
|
56
|
+
"storybook:build": "storybook build --disable-telemetry"
|
|
58
57
|
},
|
|
59
58
|
"dependencies": {
|
|
60
59
|
"@react-native-clipboard/clipboard": "^1.11.2",
|
|
@@ -125,5 +124,5 @@
|
|
|
125
124
|
"react-native-screens": ">=4.18.0",
|
|
126
125
|
"react-native-svg": ">=12.0.0"
|
|
127
126
|
},
|
|
128
|
-
"gitHead": "
|
|
127
|
+
"gitHead": "106e281adc9a3b0e87e125e1a9f1c945ce2de3cd"
|
|
129
128
|
}
|
|
@@ -19,6 +19,26 @@ const RADIUS = 20;
|
|
|
19
19
|
// "200" used as the gap value exceeds the circumference, ensuring only
|
|
20
20
|
// one stroke segment is visible at a time.
|
|
21
21
|
const GAP = 200;
|
|
22
|
+
/**
|
|
23
|
+
* Per-size motion profile. Adding a size is an additive map entry — not a
|
|
24
|
+
* new boolean branch. `layers: 1` is Layer 1 only (fixed arc); `layers: 3`
|
|
25
|
+
* runs the full Material 3 indeterminate ring (Layers 1–3).
|
|
26
|
+
*
|
|
27
|
+
* Small matches web's ActivityIndicator.module.css (`.small .svg` /
|
|
28
|
+
* `.small .arc`): Layer 1 only, spun ~1.8× faster. Web hardcodes 1000ms
|
|
29
|
+
* (≈1803ms ÷ 1.8); we match that absolute duration.
|
|
30
|
+
*/
|
|
31
|
+
const MOTION_BY_SIZE = {
|
|
32
|
+
small: {
|
|
33
|
+
layers: 1,
|
|
34
|
+
linearRotateMs: 1000,
|
|
35
|
+
dasharray: `40, ${GAP}`,
|
|
36
|
+
},
|
|
37
|
+
base: {
|
|
38
|
+
layers: 3,
|
|
39
|
+
linearRotateMs: tokens["timing-indicator--linear-rotate"],
|
|
40
|
+
},
|
|
41
|
+
};
|
|
22
42
|
function resolveLegacySize(size) {
|
|
23
43
|
if (size === "large") {
|
|
24
44
|
if (__DEV__) {
|
|
@@ -61,23 +81,28 @@ export function ActivityIndicator({ size = "base", accessibilityLabel, style, te
|
|
|
61
81
|
if (reducedMotion) {
|
|
62
82
|
return (React.createElement(ReducedMotionRing, { pixelSize: pixelSize, strokeWidth: strokeWidth, ringColor: themeTokens["color-icon"], style: style, testID: testID, a11yProps: a11yProps }));
|
|
63
83
|
}
|
|
64
|
-
return (React.createElement(FullMotionRing, { pixelSize: pixelSize, strokeWidth: strokeWidth, arcColor: themeTokens["color-icon"], trackColor: themeTokens["color-surface--active"], style: style, testID: testID, a11yProps: a11yProps }));
|
|
84
|
+
return (React.createElement(FullMotionRing, { size: resolvedSize, pixelSize: pixelSize, strokeWidth: strokeWidth, arcColor: themeTokens["color-icon"], trackColor: themeTokens["color-surface--active"], style: style, testID: testID, a11yProps: a11yProps }));
|
|
65
85
|
}
|
|
66
|
-
function FullMotionRing({ pixelSize, strokeWidth, arcColor, trackColor, style, testID, a11yProps, }) {
|
|
86
|
+
function FullMotionRing({ size, pixelSize, strokeWidth, arcColor, trackColor, style, testID, a11yProps, }) {
|
|
87
|
+
const motion = MOTION_BY_SIZE[size];
|
|
67
88
|
// Layer 1 — outer linear rotation. Continuous, constant speed.
|
|
68
89
|
const outerRotate = useSharedValue(0);
|
|
69
90
|
// Layer 2 — inner 8-phase rotation. Eight 135° eased segments per cycle
|
|
70
91
|
// (totalling 1080° over `timing-indicator--cycle`) reproduce the
|
|
71
|
-
// canonical Material Web "8-phase" rhythm.
|
|
92
|
+
// canonical Material Web "8-phase" rhythm. Only when motion.layers === 3.
|
|
72
93
|
const innerRotate = useSharedValue(0);
|
|
73
94
|
// Layer 3 — arc length and dash offset. Drives the visible arc growing
|
|
74
95
|
// from ~1 viewBox-unit to ~90 viewBox-units and sliding around the ring.
|
|
96
|
+
// Only when motion.layers === 3; fixed-arc sizes use motion.dasharray.
|
|
75
97
|
const arcProgress = useSharedValue(0);
|
|
76
98
|
useEffect(() => {
|
|
77
99
|
outerRotate.value = withRepeat(withTiming(360, {
|
|
78
|
-
duration:
|
|
100
|
+
duration: motion.linearRotateMs,
|
|
79
101
|
easing: Easing.linear,
|
|
80
102
|
}), -1);
|
|
103
|
+
if (motion.layers === 1) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
81
106
|
const phaseDuration = tokens["timing-indicator--cycle"] / 8;
|
|
82
107
|
const phaseEasing = Easing.bezier(0.4, 0, 0.2, 1);
|
|
83
108
|
innerRotate.value = withRepeat(withSequence(withTiming(135, { duration: phaseDuration, easing: phaseEasing }), withTiming(270, { duration: phaseDuration, easing: phaseEasing }), withTiming(405, { duration: phaseDuration, easing: phaseEasing }), withTiming(540, { duration: phaseDuration, easing: phaseEasing }), withTiming(675, { duration: phaseDuration, easing: phaseEasing }), withTiming(810, { duration: phaseDuration, easing: phaseEasing }), withTiming(945, { duration: phaseDuration, easing: phaseEasing }), withTiming(1080, { duration: phaseDuration, easing: phaseEasing })), -1);
|
|
@@ -87,7 +112,7 @@ function FullMotionRing({ pixelSize, strokeWidth, arcColor, trackColor, style, t
|
|
|
87
112
|
}), -1);
|
|
88
113
|
// We intentionally do not include the shared values in the dependency
|
|
89
114
|
// array — they are stable references created by useSharedValue.
|
|
90
|
-
}, []);
|
|
115
|
+
}, [motion]);
|
|
91
116
|
const outerStyle = useAnimatedStyle(() => ({
|
|
92
117
|
transform: [{ rotate: `${outerRotate.value}deg` }],
|
|
93
118
|
}));
|
|
@@ -123,12 +148,17 @@ function FullMotionRing({ pixelSize, strokeWidth, arcColor, trackColor, style, t
|
|
|
123
148
|
strokeDashoffset: offset,
|
|
124
149
|
};
|
|
125
150
|
});
|
|
151
|
+
const track = (React.createElement(Circle, { cx: CENTER, cy: CENTER, r: RADIUS, fill: "none", stroke: trackColor, strokeWidth: strokeWidth }));
|
|
152
|
+
const ring = motion.layers === 1 ? (
|
|
153
|
+
// Fixed-length arc, Layer 1 rotation only — no Layer 2 wrapper.
|
|
154
|
+
React.createElement(Svg, { width: pixelSize, height: pixelSize, viewBox: `0 0 ${VIEWBOX} ${VIEWBOX}` },
|
|
155
|
+
track,
|
|
156
|
+
React.createElement(Circle, { cx: CENTER, cy: CENTER, r: RADIUS, fill: "none", stroke: arcColor, strokeWidth: strokeWidth, strokeLinecap: "round", strokeDasharray: motion.dasharray, strokeDashoffset: 0 }))) : (React.createElement(Animated.View, { style: [{ width: pixelSize, height: pixelSize }, innerStyle] },
|
|
157
|
+
React.createElement(Svg, { width: pixelSize, height: pixelSize, viewBox: `0 0 ${VIEWBOX} ${VIEWBOX}` },
|
|
158
|
+
track,
|
|
159
|
+
React.createElement(AnimatedCircle, { cx: CENTER, cy: CENTER, r: RADIUS, fill: "none", stroke: arcColor, strokeWidth: strokeWidth, strokeLinecap: "round", animatedProps: arcAnimatedProps }))));
|
|
126
160
|
return (React.createElement(Animated.View, Object.assign({ style: [{ justifyContent: "center", alignItems: "center" }, style] }, a11yProps, { testID: testID }),
|
|
127
|
-
React.createElement(Animated.View, { style: [{ width: pixelSize, height: pixelSize }, outerStyle] },
|
|
128
|
-
React.createElement(Animated.View, { style: [{ width: pixelSize, height: pixelSize }, innerStyle] },
|
|
129
|
-
React.createElement(Svg, { width: pixelSize, height: pixelSize, viewBox: `0 0 ${VIEWBOX} ${VIEWBOX}` },
|
|
130
|
-
React.createElement(Circle, { cx: CENTER, cy: CENTER, r: RADIUS, fill: "none", stroke: trackColor, strokeWidth: strokeWidth }),
|
|
131
|
-
React.createElement(AnimatedCircle, { cx: CENTER, cy: CENTER, r: RADIUS, fill: "none", stroke: arcColor, strokeWidth: strokeWidth, strokeLinecap: "round", animatedProps: arcAnimatedProps }))))));
|
|
161
|
+
React.createElement(Animated.View, { style: [{ width: pixelSize, height: pixelSize }, outerStyle] }, ring)));
|
|
132
162
|
}
|
|
133
163
|
function ReducedMotionRing({ pixelSize, strokeWidth, ringColor, style, testID, a11yProps, }) {
|
|
134
164
|
// Reduced motion: hide rotation entirely, render one static ring in the
|
|
@@ -60,6 +60,41 @@ describe("ActivityIndicator", () => {
|
|
|
60
60
|
expect(flattenedStyle.width).toBe(44);
|
|
61
61
|
expect(flattenedStyle.height).toBe(44);
|
|
62
62
|
});
|
|
63
|
+
it("uses a fixed-length arc for small (no Layer 2/3 wrappers)", () => {
|
|
64
|
+
const { getByTestId } = render(React.createElement(ActivityIndicator, { size: "small" }));
|
|
65
|
+
const outer = getByTestId(testId).children[0];
|
|
66
|
+
const outerChildren = React.Children.toArray(outer.props.children);
|
|
67
|
+
function typeName(node) {
|
|
68
|
+
var _a, _b;
|
|
69
|
+
if (!node || typeof node !== "object")
|
|
70
|
+
return "";
|
|
71
|
+
const element = node;
|
|
72
|
+
return ((_a = element.type) === null || _a === void 0 ? void 0 : _a.displayName) || ((_b = element.type) === null || _b === void 0 ? void 0 : _b.name) || "";
|
|
73
|
+
}
|
|
74
|
+
// Small: outer rotation wrapper → Svg directly (no inner Layer 2 view).
|
|
75
|
+
expect(outerChildren.some(child => typeName(child).startsWith("Svg"))).toBe(true);
|
|
76
|
+
function findArcCircle(node) {
|
|
77
|
+
var _a, _b;
|
|
78
|
+
if (!node || typeof node !== "object")
|
|
79
|
+
return undefined;
|
|
80
|
+
const element = node;
|
|
81
|
+
const name = typeName(element);
|
|
82
|
+
if ((name === "Circle" || name === "RNSVGCircle") &&
|
|
83
|
+
element.props.strokeDasharray === "40, 200") {
|
|
84
|
+
return element;
|
|
85
|
+
}
|
|
86
|
+
const kids = React.Children.toArray((_b = (_a = element.props) === null || _a === void 0 ? void 0 : _a.children) !== null && _b !== void 0 ? _b : []);
|
|
87
|
+
for (const kid of kids) {
|
|
88
|
+
const found = findArcCircle(kid);
|
|
89
|
+
if (found)
|
|
90
|
+
return found;
|
|
91
|
+
}
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
const arc = findArcCircle(outer);
|
|
95
|
+
expect(arc).toBeTruthy();
|
|
96
|
+
expect(arc === null || arc === void 0 ? void 0 : arc.props.strokeDashoffset).toBe(0);
|
|
97
|
+
});
|
|
63
98
|
});
|
|
64
99
|
describe('deprecated size="large" alias', () => {
|
|
65
100
|
let warnSpy;
|
|
@@ -63,11 +63,12 @@ export function MediaView({ accessibilityLabel, showOverlay, showError, file, st
|
|
|
63
63
|
handleMediaLoadEnd();
|
|
64
64
|
setDecodeFailed(true);
|
|
65
65
|
} },
|
|
66
|
-
React.createElement(Overlay, { isLoading: isLoading, showOverlay: showOverlay, hasError: hasError, file: file, onUploadComplete: onUploadComplete, styles: styles }))));
|
|
66
|
+
React.createElement(Overlay, { isLoading: isLoading, showOverlay: showOverlay, hasError: hasError, file: file, onUploadComplete: onUploadComplete, styles: styles, styleInGrid: styleInGrid }))));
|
|
67
67
|
}
|
|
68
|
-
function Overlay({ isLoading, showOverlay, hasError, file, onUploadComplete, styles, }) {
|
|
69
|
-
if (isLoading)
|
|
70
|
-
return React.createElement(ActivityIndicator,
|
|
68
|
+
function Overlay({ isLoading, showOverlay, hasError, file, onUploadComplete, styles, styleInGrid, }) {
|
|
69
|
+
if (isLoading) {
|
|
70
|
+
return React.createElement(ActivityIndicator, { size: styleInGrid ? "small" : "base" });
|
|
71
|
+
}
|
|
71
72
|
if (hasError)
|
|
72
73
|
return React.createElement(ErrorOverlay, { styles: styles });
|
|
73
74
|
if (showOverlay) {
|
|
@@ -97,11 +98,12 @@ function VideoPlaceholder({ a11yLabel, styleInGrid, surfaceColor, styles, isLoad
|
|
|
97
98
|
surfaceColor ? { backgroundColor: surfaceColor } : undefined,
|
|
98
99
|
], testID: "format-file-video-placeholder" },
|
|
99
100
|
file.showFileTypeIndicator && React.createElement(Icon, { name: "videoFile", size: "large" }),
|
|
100
|
-
React.createElement(PlaceholderStatusOverlay, { isLoading: isLoading, showOverlay: showOverlay, hasError: hasError, file: file, onUploadComplete: onUploadComplete, styles: styles })));
|
|
101
|
+
React.createElement(PlaceholderStatusOverlay, { isLoading: isLoading, showOverlay: showOverlay, hasError: hasError, file: file, onUploadComplete: onUploadComplete, styles: styles, styleInGrid: styleInGrid })));
|
|
101
102
|
}
|
|
102
|
-
function PlaceholderStatusOverlay({ isLoading, showOverlay, hasError, file, onUploadComplete, styles, }) {
|
|
103
|
-
if (isLoading)
|
|
104
|
-
return React.createElement(ActivityIndicator,
|
|
103
|
+
function PlaceholderStatusOverlay({ isLoading, showOverlay, hasError, file, onUploadComplete, styles, styleInGrid, }) {
|
|
104
|
+
if (isLoading) {
|
|
105
|
+
return React.createElement(ActivityIndicator, { size: styleInGrid ? "small" : "base" });
|
|
106
|
+
}
|
|
105
107
|
if (hasError)
|
|
106
108
|
return React.createElement(ErrorOverlay, { styles: styles });
|
|
107
109
|
if (showOverlay) {
|