@jobber/components-native 0.16.0 → 0.18.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.
@@ -0,0 +1,3 @@
1
+ import { PropsWithChildren } from "react";
2
+ import { FlexProps } from "./types";
3
+ export declare function Flex({ template, align, gap, children, }: PropsWithChildren<FlexProps>): JSX.Element;
@@ -0,0 +1,9 @@
1
+ import { ViewStyle } from "react-native";
2
+ import { ColumnKeys } from "./types";
3
+ export declare const styles: {
4
+ row: {
5
+ flexDirection: "row";
6
+ };
7
+ };
8
+ export declare const columnStyles: Record<ColumnKeys, ViewStyle>;
9
+ export declare const gapStyles: Record<"none" | "small" | "base" | "smallest" | "smaller" | "large", ViewStyle>;
@@ -0,0 +1 @@
1
+ export * from "./Flex";
@@ -0,0 +1,29 @@
1
+ import { ViewStyle } from "react-native";
2
+ export type ColumnKeys = "shrink" | "grow";
3
+ export interface FlexProps {
4
+ /**
5
+ * Determine how the children gets laid out on the flex grid. If there are more
6
+ * Children than elements in the template, it will render multiple rows.
7
+ *
8
+ * **Supported keys**
9
+ * - `"grow"` - Grows to the space available. If all children are set to
10
+ * grow, then they'll have equal width.
11
+ * - `"shrink"` - Shrinks to the smallest size possible. Normally the size of
12
+ * the child.
13
+ *
14
+ * By default, this will set every children to grow in equal widths.
15
+ */
16
+ readonly template?: ColumnKeys[];
17
+ /**
18
+ * It works the same way as `alignItems` style with flex.
19
+ */
20
+ readonly align?: ViewStyle["alignItems"];
21
+ /**
22
+ * The spacing between the children.
23
+ */
24
+ readonly gap?: Spacing;
25
+ }
26
+ export declare const spacing: readonly ["none", "smallest", "smaller", "small", "base", "large"];
27
+ type ValuesOfSpacing<T extends typeof spacing> = T[number];
28
+ export type Spacing = ValuesOfSpacing<typeof spacing>;
29
+ export {};
@@ -0,0 +1,35 @@
1
+ import React from "react";
2
+ import { IconColorNames, IconNames } from "@jobber/design";
3
+ interface IconButtonProps {
4
+ /**
5
+ * Press handler
6
+ */
7
+ onPress?(): void;
8
+ /** The icon to show. */
9
+ readonly name: IconNames;
10
+ /**
11
+ * Accessibilty label for the component. It's also used for testing
12
+ */
13
+ readonly accessibilityLabel: string;
14
+ /**
15
+ * Determines the color of the icon. If not specified, some icons have a default system colour
16
+ * like quotes, jobs, and invoices.
17
+ * Others that don't have a system colour fall back to greyBlue.
18
+ */
19
+ readonly color?: IconColorNames;
20
+ /**
21
+ * Sets a custom color for the icon. Can be a rgb() or hex value.
22
+ */
23
+ readonly customColor?: string;
24
+ /**
25
+ * a component that would render over the icon
26
+ * e.g. the number of notifications over the activity feed icon
27
+ */
28
+ readonly badge?: React.ReactNode;
29
+ /**
30
+ * Used to locate this button in tests
31
+ */
32
+ readonly testID?: string;
33
+ }
34
+ export declare function IconButton({ badge, name, color, customColor, onPress, accessibilityLabel, testID, }: IconButtonProps): JSX.Element;
35
+ export {};
@@ -0,0 +1,8 @@
1
+ export declare const styles: {
2
+ container: {
3
+ width: number;
4
+ height: number;
5
+ justifyContent: "center";
6
+ alignItems: "center";
7
+ };
8
+ };
@@ -0,0 +1 @@
1
+ export { IconButton } from "./IconButton";
@@ -1,17 +1,19 @@
1
- export * from "./Icon";
2
- export * from "./Divider";
3
- export * from "./Typography";
4
- export * from "./Text";
5
- export * from "./ErrorMessageWrapper";
1
+ export * from "./ActionItem";
6
2
  export * from "./ActionLabel";
7
- export * from "./Content";
8
3
  export * from "./ActivityIndicator";
9
- export * from "./Card";
10
- export * from "./StatusLabel";
11
4
  export * from "./AtlantisContext";
12
5
  export * from "./Button";
6
+ export * from "./Card";
7
+ export * from "./Chip";
8
+ export * from "./Content";
9
+ export * from "./Divider";
10
+ export * from "./ErrorMessageWrapper";
11
+ export * from "./Flex";
12
+ export * from "./Heading";
13
+ export * from "./Icon";
14
+ export * from "./IconButton";
13
15
  export * from "./InputFieldWrapper";
14
16
  export * from "./ProgressBar";
15
- export * from "./Heading";
16
- export * from "./Chip";
17
- export * from "./ActionItem";
17
+ export * from "./StatusLabel";
18
+ export * from "./Text";
19
+ export * from "./Typography";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jobber/components-native",
3
- "version": "0.16.0",
3
+ "version": "0.18.0",
4
4
  "license": "MIT",
5
5
  "main": "dist/src/index.js",
6
6
  "module": "dist/src/index.js",
@@ -22,6 +22,7 @@
22
22
  },
23
23
  "dependencies": {
24
24
  "@jobber/design": "^0.40.0",
25
+ "lodash.chunk": "^4.2.0",
25
26
  "react-hook-form": "^7.30.0",
26
27
  "react-intl": "^6.4.2",
27
28
  "react-native-gesture-handler": "^2.5.0",
@@ -35,6 +36,7 @@
35
36
  "@testing-library/jest-native": "^5.4.2",
36
37
  "@testing-library/react-hooks": "^7.0.2",
37
38
  "@testing-library/react-native": "^12.0.1",
39
+ "@types/lodash.chunk": "^4.2.7",
38
40
  "@types/react": "^18.0.28",
39
41
  "@types/react-native": "^0.71.6",
40
42
  "@types/react-native-uuid": "^1.4.0",
@@ -47,5 +49,5 @@
47
49
  "react": "^18",
48
50
  "react-native": ">=0.69.2"
49
51
  },
50
- "gitHead": "0315bc6a7dea11378a1f187a78ee7d55f0600c0c"
52
+ "gitHead": "792c5dfee7847194e09713617184b61f70431e30"
51
53
  }
@@ -0,0 +1,29 @@
1
+ import { StyleSheet, ViewStyle } from "react-native";
2
+ import { ColumnKeys, Spacing, spacing } from "./types";
3
+ import { tokens } from "../utils/design";
4
+
5
+ export const styles = StyleSheet.create({
6
+ row: { flexDirection: "row" },
7
+ });
8
+
9
+ export const columnStyles: Record<ColumnKeys, ViewStyle> = StyleSheet.create({
10
+ shrink: {
11
+ flexGrow: 0,
12
+ flexShrink: 1,
13
+ },
14
+ grow: {
15
+ flexGrow: 1,
16
+ flexShrink: 0,
17
+ flexBasis: 0,
18
+ },
19
+ });
20
+
21
+ export const gapStyles = StyleSheet.create(
22
+ spacing.reduce((gapObj, space) => {
23
+ let paddingLeft = 0;
24
+ if (space !== "none") paddingLeft = tokens[`space-${space}`];
25
+
26
+ gapObj[space] = { paddingLeft };
27
+ return gapObj;
28
+ }, {} as Record<Spacing, ViewStyle>),
29
+ );
@@ -0,0 +1,129 @@
1
+ import React from "react";
2
+ import { render } from "@testing-library/react-native";
3
+ import { View, ViewStyle } from "react-native";
4
+ import { ReactTestInstance } from "react-test-renderer";
5
+ import { JobberStyle } from "@jobber/design/foundation";
6
+ import { Flex } from "./Flex";
7
+ import { FlexProps, Spacing } from "./types";
8
+ import { columnStyles } from "./Flex.styles";
9
+ import { Text } from "../Text";
10
+ import { Icon } from "../Icon";
11
+
12
+ function getContentComponent(parentView: ReactTestInstance) {
13
+ return (parentView?.children[0] as ReactTestInstance)
14
+ ?.children[0] as ReactTestInstance;
15
+ }
16
+
17
+ function getFlexCol(flexRow: ReactTestInstance) {
18
+ return flexRow.children as ReactTestInstance[];
19
+ }
20
+ function setUp(props?: FlexProps) {
21
+ const container = render(
22
+ <View accessibilityLabel="contentView">
23
+ <Flex align={props?.align} template={props?.template} gap={props?.gap}>
24
+ <Icon name={"email"} />
25
+ <Text>Hi onLookers!</Text>
26
+ <Text>You look Great Today!</Text>
27
+ <Text>Thanks for coming to my Ted Talk :D</Text>
28
+ </Flex>
29
+ </View>,
30
+ );
31
+ const contentView = getContentComponent(
32
+ container.getByLabelText("contentView"),
33
+ );
34
+ const flexRow = container.getAllByTestId("ATL-Flex-Row");
35
+ const flexCol = getFlexCol(flexRow[0]);
36
+ return { ...container, contentView, flexRow, flexCol };
37
+ }
38
+
39
+ describe("Gap", () => {
40
+ const gapTestCases: [Spacing, number][] = [
41
+ ["none", 0],
42
+ ["smallest", JobberStyle["space-smallest"]],
43
+ ["smaller", JobberStyle["space-smaller"]],
44
+ ["small", JobberStyle["space-small"]],
45
+ ["base", JobberStyle["space-base"]],
46
+ ["large", JobberStyle["space-large"]],
47
+ ];
48
+ it.each(gapTestCases)(
49
+ "Should have a gap of %s around the children components",
50
+ (a, expected) => {
51
+ const { contentView, flexCol } = setUp({
52
+ template: ["grow", "grow", "shrink"],
53
+ gap: a,
54
+ });
55
+ expect(flexCol[1].props.style).toContainEqual({
56
+ paddingLeft: expected,
57
+ });
58
+ expect(contentView.props.childSpacing).toEqual(a);
59
+ },
60
+ );
61
+ });
62
+
63
+ describe("Vertical alignment", () => {
64
+ it("should align children to center by default if align is not specified", () => {
65
+ const { flexRow } = setUp({
66
+ template: ["grow", "grow", "shrink"],
67
+ gap: "large",
68
+ });
69
+
70
+ expect(flexRow[0].props.style).toContainEqual({ alignItems: "center" });
71
+ });
72
+
73
+ const alignTestCases: [ViewStyle["alignItems"]][] = [
74
+ ["flex-start"],
75
+ ["flex-end"],
76
+ ["center"],
77
+ ["baseline"],
78
+ ["stretch"],
79
+ ];
80
+ it.each(alignTestCases)("should align children to %s", a => {
81
+ const { flexRow } = setUp({
82
+ template: ["grow", "grow", "shrink"],
83
+ align: a,
84
+ });
85
+
86
+ expect(flexRow[0].props.style).toContainEqual({
87
+ alignItems: a,
88
+ });
89
+ });
90
+ });
91
+
92
+ describe("Layout", () => {
93
+ it("should by default display a 1 row flex grid with equal spacing between each children", () => {
94
+ const { flexCol, flexRow } = setUp({});
95
+
96
+ expect(flexCol[0].props.style).toContainEqual(columnStyles.grow);
97
+ expect(flexCol[1].props.style).toContainEqual(columnStyles.grow);
98
+ expect(flexCol[2].props.style).toContainEqual(columnStyles.grow);
99
+ expect(flexCol[3].props.style).toContainEqual(columnStyles.grow);
100
+ expect(flexRow.length).toEqual(1);
101
+ });
102
+
103
+ it("should follow the template to decide whether to grow or shrink", () => {
104
+ const { flexCol } = setUp({
105
+ template: ["grow", "grow", "shrink"],
106
+ });
107
+
108
+ expect(flexCol[0].props.style).toContainEqual(columnStyles.grow);
109
+ expect(flexCol[1].props.style).toContainEqual(columnStyles.grow);
110
+ expect(flexCol[2].props.style).toContainEqual(columnStyles.shrink);
111
+ });
112
+
113
+ it("should create a flex grid with 2 rows", () => {
114
+ const { flexRow } = setUp({
115
+ template: ["grow", "grow", "shrink"],
116
+ });
117
+
118
+ expect(flexRow.length).toEqual(2);
119
+ });
120
+
121
+ it("should inject extra children on the last row of a multiRow flex grid if needed", () => {
122
+ const { flexRow } = setUp({
123
+ template: ["grow", "grow", "shrink"],
124
+ });
125
+ const flexCol2 = getFlexCol(flexRow[1]);
126
+ expect(flexRow.length > 1).toBeTruthy();
127
+ expect(flexCol2.length).toEqual(3);
128
+ });
129
+ });
@@ -0,0 +1,71 @@
1
+ import React, { Children, PropsWithChildren } from "react";
2
+ import { View } from "react-native";
3
+ import chunk from "lodash.chunk";
4
+ import { columnStyles, gapStyles, styles } from "./Flex.styles";
5
+ import { FlexProps } from "./types";
6
+ import { Content } from "../Content";
7
+
8
+ export function Flex({
9
+ template = [],
10
+ align = "center",
11
+ gap = "base",
12
+ children,
13
+ }: PropsWithChildren<FlexProps>): JSX.Element {
14
+ if (template.length === 1) {
15
+ console.warn("Please use <Content /> component for a stacked layout");
16
+ }
17
+
18
+ const childrenArray = Children.toArray(children);
19
+ const chunkedChildren = chunk(
20
+ childrenArray,
21
+ template.length || childrenArray.length,
22
+ );
23
+
24
+ return (
25
+ <Content spacing="none" childSpacing={gap}>
26
+ {chunkedChildren.map((childArray, rowIndex) => (
27
+ <Row key={rowIndex} template={template} align={align} gap={gap}>
28
+ {injectChild(childArray)}
29
+ </Row>
30
+ ))}
31
+ </Content>
32
+ );
33
+
34
+ function injectChild(value: ReturnType<typeof Children.toArray>) {
35
+ const hasMoreRows = chunkedChildren.length > 1;
36
+ const childrenCount = value.length;
37
+ const templateCount = template.length;
38
+
39
+ if (hasMoreRows && childrenCount < templateCount) {
40
+ const missingChildCount = templateCount - childrenCount;
41
+
42
+ for (let index = 0; index < missingChildCount; index++) {
43
+ value.push(<React.Fragment key={index} />);
44
+ }
45
+ }
46
+
47
+ return value;
48
+ }
49
+ }
50
+
51
+ function Row({
52
+ template = [],
53
+ align = "center",
54
+ gap = "base",
55
+ children,
56
+ }: PropsWithChildren<FlexProps>): JSX.Element {
57
+ return (
58
+ <View testID="ATL-Flex-Row" style={[styles.row, { alignItems: align }]}>
59
+ {Children.map(children, (child, index) => (
60
+ <View
61
+ style={[
62
+ columnStyles[template[index]] || columnStyles.grow,
63
+ index > 0 && gap && gapStyles[gap],
64
+ ]}
65
+ >
66
+ {child}
67
+ </View>
68
+ ))}
69
+ </View>
70
+ );
71
+ }
@@ -0,0 +1 @@
1
+ export * from "./Flex";
@@ -0,0 +1,41 @@
1
+ import { ViewStyle } from "react-native";
2
+
3
+ export type ColumnKeys = "shrink" | "grow";
4
+
5
+ export interface FlexProps {
6
+ /**
7
+ * Determine how the children gets laid out on the flex grid. If there are more
8
+ * Children than elements in the template, it will render multiple rows.
9
+ *
10
+ * **Supported keys**
11
+ * - `"grow"` - Grows to the space available. If all children are set to
12
+ * grow, then they'll have equal width.
13
+ * - `"shrink"` - Shrinks to the smallest size possible. Normally the size of
14
+ * the child.
15
+ *
16
+ * By default, this will set every children to grow in equal widths.
17
+ */
18
+ readonly template?: ColumnKeys[];
19
+
20
+ /**
21
+ * It works the same way as `alignItems` style with flex.
22
+ */
23
+ readonly align?: ViewStyle["alignItems"];
24
+
25
+ /**
26
+ * The spacing between the children.
27
+ */
28
+ readonly gap?: Spacing;
29
+ }
30
+
31
+ export const spacing = [
32
+ "none",
33
+ "smallest",
34
+ "smaller",
35
+ "small",
36
+ "base",
37
+ "large",
38
+ ] as const;
39
+
40
+ type ValuesOfSpacing<T extends typeof spacing> = T[number];
41
+ export type Spacing = ValuesOfSpacing<typeof spacing>;
@@ -0,0 +1,11 @@
1
+ import { StyleSheet } from "react-native";
2
+ import { tokens } from "../utils/design";
3
+
4
+ export const styles = StyleSheet.create({
5
+ container: {
6
+ width: tokens["space-largest"],
7
+ height: tokens["space-largest"],
8
+ justifyContent: "center",
9
+ alignItems: "center",
10
+ },
11
+ });
@@ -0,0 +1,79 @@
1
+ import React from "react";
2
+ import { fireEvent, render } from "@testing-library/react-native";
3
+ import { IconButton } from "./IconButton";
4
+ import { Text } from "../Text";
5
+
6
+ describe("IconButton", () => {
7
+ it("renders an IconButton", () => {
8
+ const pressHandler = jest.fn();
9
+ const { getByTestId } = render(
10
+ <IconButton
11
+ onPress={pressHandler}
12
+ name="job"
13
+ accessibilityLabel="Job Button"
14
+ testID="JobButton"
15
+ />,
16
+ );
17
+
18
+ expect(getByTestId("JobButton")).toBeDefined();
19
+ });
20
+
21
+ it("should call the onPress", () => {
22
+ const pressHandler = jest.fn();
23
+ const { getByLabelText } = render(
24
+ <IconButton
25
+ onPress={pressHandler}
26
+ name="job"
27
+ accessibilityLabel={"Job Button"}
28
+ />,
29
+ );
30
+ fireEvent.press(getByLabelText("Job Button"));
31
+ expect(pressHandler).toHaveBeenCalled();
32
+ });
33
+
34
+ it("should render badge component", () => {
35
+ const pressHandler = jest.fn();
36
+ const badge = <Text>Hi</Text>;
37
+ const { getByText } = render(
38
+ <IconButton
39
+ onPress={pressHandler}
40
+ name="job"
41
+ badge={badge}
42
+ accessibilityLabel={"Job Button"}
43
+ />,
44
+ );
45
+ expect(getByText("Hi")).toBeDefined();
46
+ });
47
+
48
+ it("should render IconButton with custom color", () => {
49
+ const pressHandler = jest.fn();
50
+ const { getByLabelText } = render(
51
+ <IconButton
52
+ onPress={pressHandler}
53
+ name="job"
54
+ customColor="#f33323"
55
+ accessibilityLabel={"Job Button"}
56
+ />,
57
+ );
58
+ const iconBtnColorProp = getByLabelText("Job Button").findByProps({
59
+ customColor: "#f33323",
60
+ }).props.customColor;
61
+
62
+ expect(iconBtnColorProp).toEqual("#f33323");
63
+ });
64
+
65
+ it("should expose testID", () => {
66
+ const pressHandler = jest.fn();
67
+ const testID = "JobButton";
68
+ const { getByTestId } = render(
69
+ <IconButton
70
+ onPress={pressHandler}
71
+ name="job"
72
+ accessibilityLabel={"Job Button"}
73
+ testID={testID}
74
+ />,
75
+ );
76
+ fireEvent.press(getByTestId(testID));
77
+ expect(pressHandler).toHaveBeenCalled();
78
+ });
79
+ });
@@ -0,0 +1,66 @@
1
+ import React from "react";
2
+ import { TouchableOpacity } from "react-native";
3
+ import { IconColorNames, IconNames } from "@jobber/design";
4
+ import { styles } from "./IconButton.style";
5
+ import { Icon } from "../Icon";
6
+
7
+ interface IconButtonProps {
8
+ /**
9
+ * Press handler
10
+ */
11
+ onPress?(): void;
12
+
13
+ /** The icon to show. */
14
+ readonly name: IconNames;
15
+
16
+ /**
17
+ * Accessibilty label for the component. It's also used for testing
18
+ */
19
+ readonly accessibilityLabel: string;
20
+
21
+ /**
22
+ * Determines the color of the icon. If not specified, some icons have a default system colour
23
+ * like quotes, jobs, and invoices.
24
+ * Others that don't have a system colour fall back to greyBlue.
25
+ */
26
+ readonly color?: IconColorNames;
27
+
28
+ /**
29
+ * Sets a custom color for the icon. Can be a rgb() or hex value.
30
+ */
31
+ readonly customColor?: string;
32
+
33
+ /**
34
+ * a component that would render over the icon
35
+ * e.g. the number of notifications over the activity feed icon
36
+ */
37
+ readonly badge?: React.ReactNode;
38
+
39
+ /**
40
+ * Used to locate this button in tests
41
+ */
42
+ readonly testID?: string;
43
+ }
44
+
45
+ export function IconButton({
46
+ badge,
47
+ name,
48
+ color,
49
+ customColor,
50
+ onPress,
51
+ accessibilityLabel,
52
+ testID,
53
+ }: IconButtonProps): JSX.Element {
54
+ return (
55
+ <TouchableOpacity
56
+ onPress={onPress}
57
+ style={styles.container}
58
+ accessibilityLabel={accessibilityLabel}
59
+ accessibilityRole="button"
60
+ testID={testID}
61
+ >
62
+ {badge}
63
+ <Icon name={name} color={color} customColor={customColor} />
64
+ </TouchableOpacity>
65
+ );
66
+ }
@@ -0,0 +1 @@
1
+ export { IconButton } from "./IconButton";
package/src/index.ts CHANGED
@@ -1,17 +1,19 @@
1
- export * from "./Icon";
2
- export * from "./Divider";
3
- export * from "./Typography";
4
- export * from "./Text";
5
- export * from "./ErrorMessageWrapper";
1
+ export * from "./ActionItem";
6
2
  export * from "./ActionLabel";
7
- export * from "./Content";
8
3
  export * from "./ActivityIndicator";
9
- export * from "./Card";
10
- export * from "./StatusLabel";
11
4
  export * from "./AtlantisContext";
12
5
  export * from "./Button";
6
+ export * from "./Card";
7
+ export * from "./Chip";
8
+ export * from "./Content";
9
+ export * from "./Divider";
10
+ export * from "./ErrorMessageWrapper";
11
+ export * from "./Flex";
12
+ export * from "./Heading";
13
+ export * from "./Icon";
14
+ export * from "./IconButton";
13
15
  export * from "./InputFieldWrapper";
14
16
  export * from "./ProgressBar";
15
- export * from "./Heading";
16
- export * from "./Chip";
17
- export * from "./ActionItem";
17
+ export * from "./StatusLabel";
18
+ export * from "./Text";
19
+ export * from "./Typography";