@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jobber/components-native",
3
- "version": "0.113.1",
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": "06559aba2040cdb7808c71ac988826c60c9e7cd9"
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: tokens["timing-indicator--linear-rotate"],
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;
@@ -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
- }, [bottomViewRef, scrollViewRef, windowHeight]);
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
- current: { scrollTo: mockScrollTo },
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, null);
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, null);
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) {