@jobber/components-native 0.113.1 → 0.113.3
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 +2 -2
- package/dist/src/ActivityIndicator/ActivityIndicator.js +40 -10
- package/dist/src/ActivityIndicator/ActivityIndicator.test.js +35 -0
- package/dist/src/Form/Form.js +1 -12
- package/dist/src/Form/Form.test.js +49 -4
- package/dist/src/FormatFile/components/MediaView/MediaView.js +10 -8
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/jobber-components-native.podspec +0 -1
- package/package.json +2 -2
- package/src/ActivityIndicator/ActivityIndicator.test.tsx +46 -0
- package/src/ActivityIndicator/ActivityIndicator.tsx +98 -32
- package/src/ActivityIndicator/guide.md +43 -18
- package/src/Form/Form.test.tsx +61 -4
- package/src/Form/Form.tsx +1 -11
- 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.3",
|
|
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": "895ad886700e1a7e11927702a0e5f49cb554c4e3"
|
|
128
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;
|
package/dist/src/Form/Form.js
CHANGED
|
@@ -96,20 +96,9 @@ function InternalForm({ children, onBeforeSubmit, onSubmit, onSubmitError, onSub
|
|
|
96
96
|
setKeyboardScreenY(windowHeight - event.height);
|
|
97
97
|
}, [windowHeight]);
|
|
98
98
|
const handleKeyboardHide = useCallback(() => {
|
|
99
|
-
var _a;
|
|
100
|
-
(_a = bottomViewRef === null || bottomViewRef === void 0 ? void 0 : bottomViewRef.current) === null || _a === void 0 ? void 0 : _a.measureInWindow(
|
|
101
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
102
|
-
(_x, y, _width, _height) => {
|
|
103
|
-
var _a;
|
|
104
|
-
// This fixes extra whitespace below the form if it was scrolled down while the keyboard was open
|
|
105
|
-
// i.e. a View below the form is higher than the bottom of the window
|
|
106
|
-
if (y < windowHeight) {
|
|
107
|
-
(_a = scrollViewRef === null || scrollViewRef === void 0 ? void 0 : scrollViewRef.current) === null || _a === void 0 ? void 0 : _a.scrollToEnd();
|
|
108
|
-
}
|
|
109
|
-
});
|
|
110
99
|
setKeyboardHeight(0);
|
|
111
100
|
setKeyboardScreenY(0);
|
|
112
|
-
}, [
|
|
101
|
+
}, []);
|
|
113
102
|
useEffect(() => {
|
|
114
103
|
const showEvent = Platform.OS === "ios" ? "keyboardWillShow" : "keyboardDidShow";
|
|
115
104
|
const hideEvent = Platform.OS === "ios" ? "keyboardWillHide" : "keyboardDidHide";
|
|
@@ -10,6 +10,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
10
10
|
import React from "react";
|
|
11
11
|
import { act, fireEvent, render, waitFor } from "@testing-library/react-native";
|
|
12
12
|
import { Alert, Keyboard } from "react-native";
|
|
13
|
+
import { KeyboardEvents } from "react-native-keyboard-controller";
|
|
13
14
|
import { Form, FormBannerMessageType } from ".";
|
|
14
15
|
import { FormSubmitErrorType } from "./types";
|
|
15
16
|
import { atlantisContextDefaultValues } from "../AtlantisContext";
|
|
@@ -35,14 +36,34 @@ const onChangeMock = jest.fn();
|
|
|
35
36
|
const onChangeSelectMock = jest.fn();
|
|
36
37
|
const onChangeSwitchMock = jest.fn();
|
|
37
38
|
const mockScrollTo = jest.fn();
|
|
39
|
+
const mockScrollToEnd = jest.fn();
|
|
38
40
|
const mockScrollToTop = jest.fn();
|
|
41
|
+
const mockMeasureInWindow = jest.fn((callback) => {
|
|
42
|
+
callback(0, 0, 0, 0);
|
|
43
|
+
});
|
|
44
|
+
const mockScrollViewRef = {
|
|
45
|
+
get current() {
|
|
46
|
+
return { scrollTo: mockScrollTo, scrollToEnd: mockScrollToEnd };
|
|
47
|
+
},
|
|
48
|
+
// React assigns the ref while rendering KeyboardAwareScrollView. Keep the
|
|
49
|
+
// test double available to the keyboard-hide callback instead.
|
|
50
|
+
set current(value) {
|
|
51
|
+
void value;
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
const mockBottomViewRef = {
|
|
55
|
+
get current() {
|
|
56
|
+
return { measureInWindow: mockMeasureInWindow };
|
|
57
|
+
},
|
|
58
|
+
set current(value) {
|
|
59
|
+
void value;
|
|
60
|
+
},
|
|
61
|
+
};
|
|
39
62
|
jest.mock("./hooks/useFormViewRefs", () => ({
|
|
40
63
|
useFormViewRefs: () => {
|
|
41
64
|
return {
|
|
42
|
-
scrollViewRef:
|
|
43
|
-
|
|
44
|
-
},
|
|
45
|
-
bottomViewRef: { current: {} },
|
|
65
|
+
scrollViewRef: mockScrollViewRef,
|
|
66
|
+
bottomViewRef: mockBottomViewRef,
|
|
46
67
|
scrollToTop: mockScrollToTop,
|
|
47
68
|
};
|
|
48
69
|
},
|
|
@@ -321,6 +342,30 @@ describe("Form", () => {
|
|
|
321
342
|
expect(queryByTestId("ATL-FormSafeArea")).toBeNull();
|
|
322
343
|
});
|
|
323
344
|
});
|
|
345
|
+
describe("Keyboard dismissal", () => {
|
|
346
|
+
it("does not imperatively scroll the form to its end", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
347
|
+
var _a;
|
|
348
|
+
const listeners = new Map();
|
|
349
|
+
const addListener = KeyboardEvents.addListener;
|
|
350
|
+
const registerListener = (eventName, listener) => {
|
|
351
|
+
listeners.set(eventName, listener);
|
|
352
|
+
return { remove: jest.fn() };
|
|
353
|
+
};
|
|
354
|
+
addListener
|
|
355
|
+
.mockImplementationOnce(registerListener)
|
|
356
|
+
.mockImplementationOnce(registerListener);
|
|
357
|
+
render(React.createElement(FormTest, { onSubmit: onSubmitMock }));
|
|
358
|
+
const hideListener = (_a = [...listeners.entries()].find(([eventName]) => eventName.endsWith("Hide"))) === null || _a === void 0 ? void 0 : _a[1];
|
|
359
|
+
expect(hideListener).toBeDefined();
|
|
360
|
+
if (!hideListener) {
|
|
361
|
+
throw new Error("Expected a keyboard hide listener");
|
|
362
|
+
}
|
|
363
|
+
yield act(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
364
|
+
hideListener();
|
|
365
|
+
}));
|
|
366
|
+
expect(mockScrollToEnd).not.toHaveBeenCalled();
|
|
367
|
+
}));
|
|
368
|
+
});
|
|
324
369
|
describe("Leaving the form", () => {
|
|
325
370
|
let mockUseConfirmBeforeBack;
|
|
326
371
|
let mockRemoveLocalCache;
|
|
@@ -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) {
|