@jobber/components-native 0.111.0 → 0.112.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.
@@ -65,19 +65,19 @@ cases. The `small` size (16px) should be used on individual elements of an
65
65
  interface (e.g. inside a `Button` or `Card`) or when sitting next to short
66
66
  inline text.
67
67
 
68
- Layout is the consumer's responsibility — `ActivityIndicator` does not expose
69
- an `inline` prop. Place it inside your own flex / inline-block container, or
70
- pass a `className` / `style`, to control surrounding layout.
68
+ Layout is the consumer's responsibility — `ActivityIndicator` does not expose an
69
+ `inline` prop. Place it inside your own flex / inline-block container, or pass a
70
+ `className` / `style`, to control surrounding layout.
71
71
 
72
72
  ## Accessibility
73
73
 
74
74
  `ActivityIndicator` announces itself politely to assistive technology:
75
75
 
76
- * `role="status"` marks the element as a polite live region, appropriate for
77
- a non-urgent loading state.
78
- * `aria-label` defaults to the literal English string `"Loading"`. Pass a
79
- custom `ariaLabel` to localize or to describe the specific activity
80
- (e.g. `ariaLabel="Uploading file"`).
76
+ * `role="status"` marks the element as a polite live region, appropriate for a
77
+ non-urgent loading state.
78
+ * `aria-label` defaults to the literal English string `"Loading"`. Pass a custom
79
+ `ariaLabel` to localize or to describe the specific activity (e.g.
80
+ `ariaLabel="Uploading file"`).
81
81
 
82
82
  The indicator does not participate in the keyboard tab order.
83
83
 
@@ -85,9 +85,9 @@ The indicator does not participate in the keyboard tab order.
85
85
 
86
86
  When the user's operating system reports
87
87
  [`prefers-reduced-motion: reduce`](https://developer.mozilla.org/docs/Web/CSS/@media/prefers-reduced-motion),
88
- the indicator hides its rocking and rotating layers, repaints a single
89
- static ring in the icon foreground colour, and gently pulses its opacity so
90
- the indicator still reads as "busy" without rotational motion.
88
+ the indicator hides its rocking and rotating layers, repaints a single static
89
+ ring in the icon foreground colour, and gently pulses its opacity so the
90
+ indicator still reads as "busy" without rotational motion.
91
91
 
92
92
  ## Relationship to Spinner
93
93
 
@@ -427,7 +427,6 @@ export function IconSizesExample() {
427
427
  | | `import` |
428
428
  | | `merge` |
429
429
  | | `redo` |
430
- | | `remove` |
431
430
  | | `search` |
432
431
  | | `sort` |
433
432
  | | `sync` |
@@ -487,6 +486,7 @@ export function IconSizesExample() {
487
486
  | | `sidebar` |
488
487
  | | `signature` |
489
488
  | | `sneaker` |
489
+ | | `soundwave` |
490
490
  | | `sparkles` |
491
491
  | | `speaker` |
492
492
  | | `sprout` |
@@ -511,6 +511,25 @@ export function IconSizesExample() {
511
511
  | | `plus2` |
512
512
 
513
513
 
514
+ ## Validating icon names at runtime
515
+
516
+ When icon names arrive as runtime data (API responses, configuration, user
517
+ input), validate them before rendering with the exports from
518
+ `@jobber/components` (also available from `@jobber/design`):
519
+
520
+ * `iconNames`: a readonly array of every valid icon name
521
+ * `isIconName(value)`: a type guard that narrows a value to `IconNames`
522
+
523
+ ```tsx
524
+ import { Icon, isIconName } from "@jobber/components";
525
+
526
+ function StatusIcon({ iconName }: { readonly iconName: string }) {
527
+ if (!isIconName(iconName)) return null;
528
+
529
+ return <Icon name={iconName} />;
530
+ }
531
+ ```
532
+
514
533
  ## Component customization
515
534
 
516
535
  ### UNSAFE\_ props (advanced usage)
@@ -21,7 +21,8 @@ for the “emptiness” may include, but are not limited to…
21
21
  * Search results with no match
22
22
  * An error has resulted in the user not being able to access content they
23
23
  otherwise could
24
- * A specific field doesn't have a value (for example, a custom field that the user hasn't populated)
24
+ * A specific field doesn't have a value (for example, a custom field that the
25
+ user hasn't populated)
25
26
 
26
27
  ## Solution
27
28
 
@@ -70,22 +71,29 @@ When a field doesn't have a value, use a dash to indicate the lack of a value.
70
71
  This provides:
71
72
 
72
73
  * confirmation that nothing was entered
73
- * confirmation that something *can* be entered there should the information require an update
74
+ * confirmation that something *can* be entered there should the information
75
+ require an update
74
76
 
75
77
  ```tsx
76
78
  <Card>
77
79
  <Content>
78
80
  <Heading level={4}>Additional details</Heading>
79
81
  <Stack gap="smallest">
80
- <Text size="small" variation="subdued">Preferred service date</Text>
82
+ <Text size="small" variation="subdued">
83
+ Preferred service date
84
+ </Text>
81
85
  <Text>June 25, 2026</Text>
82
86
  </Stack>
83
87
  <Stack gap="smallest">
84
- <Text size="small" variation="subdued">Gate code</Text>
88
+ <Text size="small" variation="subdued">
89
+ Gate code
90
+ </Text>
85
91
  <Text>—</Text>
86
92
  </Stack>
87
93
  <Stack gap="smallest">
88
- <Text size="small" variation="subdued">Additional requests</Text>
94
+ <Text size="small" variation="subdued">
95
+ Additional requests
96
+ </Text>
89
97
  <Text>—</Text>
90
98
  </Stack>
91
99
  </Content>
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jobber/components-native",
3
- "version": "0.111.0",
3
+ "version": "0.112.0",
4
4
  "license": "MIT",
5
5
  "description": "React Native implementation of Atlantis",
6
6
  "repository": {
@@ -73,7 +73,7 @@
73
73
  "devDependencies": {
74
74
  "@babel/runtime": "^7.29.2",
75
75
  "@gorhom/bottom-sheet": "^5.2.8",
76
- "@jobber/design": "0.109.0",
76
+ "@jobber/design": "0.111.0",
77
77
  "@jobber/hooks": "2.21.0",
78
78
  "@react-native-community/datetimepicker": "^8.4.5",
79
79
  "@react-native/babel-preset": "^0.82.1",
@@ -124,5 +124,5 @@
124
124
  "react-native-screens": ">=4.18.0",
125
125
  "react-native-svg": ">=12.0.0"
126
126
  },
127
- "gitHead": "6346e405a6b057bc5b03e75a03412811e44571b5"
127
+ "gitHead": "71ed27026cd539889c5ce6c627bd74a8d283112e"
128
128
  }
@@ -64,7 +64,7 @@ function Footer({ styles, close, cancelLabel, }) {
64
64
  return (React.createElement(View, null,
65
65
  React.createElement(View, { style: styles.footerDivider },
66
66
  React.createElement(Divider, null)),
67
- React.createElement(BottomSheetOption, { text: cancelLabel, icon: "remove", onPress: close })));
67
+ React.createElement(BottomSheetOption, { text: cancelLabel, icon: "cross", onPress: close })));
68
68
  }
69
69
  function dismissKeyboard() {
70
70
  //Dismisses the keyboard before opening the bottom sheet.
@@ -42,7 +42,7 @@ export function Chip({ icon, label, onPress, isDismissible, isActive, inactiveBa
42
42
  label && (React.createElement(View, { style: styles.chipText },
43
43
  React.createElement(Typography, { color: "base", fontWeight: "medium", maxFontScaleSize: tokens["typography--fontSize-large"], maxLines: "single", reverseTheme: isActive }, label))),
44
44
  isDismissible && (React.createElement(View, { style: [styles.dismissIcon, dismissColor] },
45
- React.createElement(Icon, { name: "remove", size: "small" })))));
45
+ React.createElement(Icon, { name: "cross", size: "small" })))));
46
46
  }
47
47
  function getBorderStyle(inactiveBackgroundColor, tokens) {
48
48
  let borderColor = "transparent";
@@ -38,7 +38,7 @@ it("renders an inactive Chip with icon", () => {
38
38
  });
39
39
  it("renders a Chip with the dismiss icon", () => {
40
40
  const { getByTestId } = render(React.createElement(Chip, { label: "Foo", onPress: jest.fn(), isActive: true, isDismissible: true }));
41
- expect(getByTestId("remove")).toBeDefined();
41
+ expect(getByTestId("cross")).toBeDefined();
42
42
  });
43
43
  it("should call the handler with the new value", () => {
44
44
  const pressHandler = jest.fn();
@@ -7,14 +7,18 @@ export function Icon({ name, color, size = "base", customColor, testID, }) {
7
7
  const { svgStyle, paths, viewBox } = getIcon({
8
8
  name,
9
9
  color,
10
+ customColor,
10
11
  size,
11
12
  platform: "mobile",
12
13
  format: "js",
13
14
  tokens,
14
15
  });
16
+ // Paths with their own fill are static artwork; only unfilled paths take
17
+ // the themed color and customColor.
15
18
  // getIcon returns undefined paths for an unknown icon name; render nothing rather than crash.
16
- const icon = (paths !== null && paths !== void 0 ? paths : []).map((path) => {
17
- return React.createElement(Path, { key: path, d: path, fill: customColor || svgStyle.fill });
19
+ const icon = (paths !== null && paths !== void 0 ? paths : []).map(path => {
20
+ var _a;
21
+ return (React.createElement(Path, { key: path.d, d: path.d, fill: (_a = path.fill) !== null && _a !== void 0 ? _a : (customColor || svgStyle.fill), fillOpacity: path.fillOpacity, opacity: path.opacity }));
18
22
  });
19
23
  return (React.createElement(Svg, { style: Object.assign(Object.assign({}, svgStyle), { display: "flex" }), testID: testID || name, viewBox: viewBox }, icon));
20
24
  }
@@ -45,6 +45,20 @@ it("renders an empty svg without throwing for an unknown icon name", () => {
45
45
  const svg = render(React.createElement(Icon, { name: "someNonexistentIcon" }));
46
46
  expect(svg.UNSAFE_root.findAllByType(Path)).toHaveLength(0);
47
47
  });
48
+ it("renders the multi-color truck icon with its static fills", () => {
49
+ const svg = render(React.createElement(Icon, { name: "truck" }));
50
+ const fills = svg.UNSAFE_root.findAllByType(Path).map(path => path.props.fill);
51
+ expect(fills).toHaveLength(5);
52
+ expect(fills).toContain("#2B2B2B");
53
+ expect(fills.filter(fill => fill.startsWith("#"))).toHaveLength(4);
54
+ });
55
+ it("applies customColor to the truck body path but not its static artwork", () => {
56
+ const svg = render(React.createElement(Icon, { name: "truck", customColor: "#abc123" }));
57
+ const fills = svg.UNSAFE_root.findAllByType(Path).map(path => path.props.fill);
58
+ expect(fills).toHaveLength(5);
59
+ expect(fills.filter(fill => fill === "#abc123")).toHaveLength(1);
60
+ expect(fills).toEqual(expect.arrayContaining(["#2B2B2B", "#000000", "#FFFFFF", "#2A2A2A"]));
61
+ });
48
62
  describe("theme-aware fill", () => {
49
63
  function getPathFills(svg) {
50
64
  return svg.UNSAFE_root.findAllByType(Path).map(p => p.props.fill);
@@ -21,7 +21,7 @@ function DefaultToast({ text1 }) {
21
21
  React.createElement(TouchableOpacity, { style: styles.toastMessage, accessibilityRole: "alert" },
22
22
  React.createElement(Text, { reverseTheme: true }, text1)),
23
23
  React.createElement(View, { style: styles.toastIcon },
24
- React.createElement(IconButton, { onPress: Toast.hide, name: "remove", customColor: tokens["color-greyBlue--light"], accessibilityLabel: t("dismiss") })))));
24
+ React.createElement(IconButton, { onPress: Toast.hide, name: "cross", customColor: tokens["color-greyBlue--light"], accessibilityLabel: t("dismiss") })))));
25
25
  }
26
26
  const toastConfig = {
27
27
  default: DefaultToast,