@jobber/components-native 0.7.0 → 0.9.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.
- package/dist/src/Card/Card.js +38 -0
- package/dist/src/Card/Card.style.js +31 -0
- package/dist/src/Card/components/InternalCardHeader.js +14 -0
- package/dist/src/Card/components/InternalCardHeader.style.js +16 -0
- package/dist/src/Card/components/index.js +1 -0
- package/dist/src/Card/index.js +1 -0
- package/dist/src/StatusLabel/StatusLabel.js +21 -0
- package/dist/src/StatusLabel/StatusLabel.style.js +27 -0
- package/dist/src/StatusLabel/index.js +1 -0
- package/dist/src/Typography/Typography.style.js +101 -164
- package/dist/src/Typography/webFonts.js +34 -0
- package/dist/src/index.js +2 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/src/Card/Card.d.ts +40 -0
- package/dist/types/src/Card/Card.style.d.ts +56 -0
- package/dist/types/src/Card/components/InternalCardHeader.d.ts +9 -0
- package/dist/types/src/Card/components/InternalCardHeader.style.d.ts +14 -0
- package/dist/types/src/Card/components/index.d.ts +1 -0
- package/dist/types/src/Card/index.d.ts +2 -0
- package/dist/types/src/StatusLabel/StatusLabel.d.ts +22 -0
- package/dist/types/src/StatusLabel/StatusLabel.style.d.ts +23 -0
- package/dist/types/src/StatusLabel/index.d.ts +2 -0
- package/dist/types/src/Typography/Typography.style.d.ts +6 -6
- package/dist/types/src/Typography/webFonts.d.ts +4 -0
- package/dist/types/src/index.d.ts +2 -0
- package/package.json +2 -2
- package/src/Card/Card.style.ts +46 -0
- package/src/Card/Card.test.tsx +128 -0
- package/src/Card/Card.tsx +145 -0
- package/src/Card/components/InternalCardHeader.style.ts +19 -0
- package/src/Card/components/InternalCardHeader.test.tsx +31 -0
- package/src/Card/components/InternalCardHeader.tsx +41 -0
- package/src/Card/components/index.ts +1 -0
- package/src/Card/index.ts +2 -0
- package/src/StatusLabel/StatusLabel.style.ts +30 -0
- package/src/StatusLabel/StatusLabel.test.tsx +68 -0
- package/src/StatusLabel/StatusLabel.tsx +73 -0
- package/src/StatusLabel/__snapshots__/StatusLabel.test.tsx.snap +554 -0
- package/src/StatusLabel/index.ts +2 -0
- package/src/Typography/Typography.style.ts +33 -18
- package/src/Typography/webFonts.ts +43 -0
- package/src/index.ts +2 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { ReactNode } from "react";
|
|
2
|
+
import { IconNames } from "@jobber/design";
|
|
3
|
+
import { XOR } from "ts-xor";
|
|
4
|
+
interface CardProps {
|
|
5
|
+
/**
|
|
6
|
+
* @deprecated Use <ActionItem /> with the title and onPress properties instead
|
|
7
|
+
*/
|
|
8
|
+
readonly header?: HeaderProps;
|
|
9
|
+
readonly footer?: FooterProps;
|
|
10
|
+
readonly children?: ReactNode;
|
|
11
|
+
readonly reportCardHeight?: (height: number) => void;
|
|
12
|
+
readonly testID?: string;
|
|
13
|
+
readonly error?: string;
|
|
14
|
+
readonly elevation?: elevationProp;
|
|
15
|
+
}
|
|
16
|
+
type elevationProp = "none" | "low" | "base" | "high";
|
|
17
|
+
export type HeaderProps = HeaderCommonProps & HeaderActionProps;
|
|
18
|
+
interface FooterProps {
|
|
19
|
+
readonly onPress: () => void;
|
|
20
|
+
readonly title: string;
|
|
21
|
+
}
|
|
22
|
+
interface HeaderCommonProps {
|
|
23
|
+
readonly title: string;
|
|
24
|
+
}
|
|
25
|
+
type HeaderActionProps = {
|
|
26
|
+
readonly onPress?: never;
|
|
27
|
+
readonly actionItem?: never;
|
|
28
|
+
} | {
|
|
29
|
+
readonly onPress: () => void;
|
|
30
|
+
readonly actionItem: ActionItem;
|
|
31
|
+
};
|
|
32
|
+
interface IconAction {
|
|
33
|
+
readonly iconName: IconNames;
|
|
34
|
+
}
|
|
35
|
+
interface ButtonAction {
|
|
36
|
+
readonly label: string;
|
|
37
|
+
}
|
|
38
|
+
export type ActionItem = XOR<IconAction, ButtonAction>;
|
|
39
|
+
export declare function Card({ header, footer, children, reportCardHeight: onCardHeightChange, testID, error, elevation, }: CardProps): JSX.Element;
|
|
40
|
+
export {};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export declare const styles: {
|
|
2
|
+
container: {
|
|
3
|
+
width: string;
|
|
4
|
+
backgroundColor: string;
|
|
5
|
+
};
|
|
6
|
+
lowElevation: {
|
|
7
|
+
shadowColor: string;
|
|
8
|
+
shadowOffset: {
|
|
9
|
+
width: number;
|
|
10
|
+
height: number;
|
|
11
|
+
};
|
|
12
|
+
shadowOpacity: number;
|
|
13
|
+
shadowRadius: number;
|
|
14
|
+
elevation: number;
|
|
15
|
+
};
|
|
16
|
+
baseElevation: {
|
|
17
|
+
shadowColor: string;
|
|
18
|
+
shadowOffset: {
|
|
19
|
+
width: number;
|
|
20
|
+
height: number;
|
|
21
|
+
};
|
|
22
|
+
shadowOpacity: number;
|
|
23
|
+
shadowRadius: number;
|
|
24
|
+
elevation: number;
|
|
25
|
+
};
|
|
26
|
+
highElevation: {
|
|
27
|
+
shadowColor: string;
|
|
28
|
+
shadowOffset: {
|
|
29
|
+
width: number;
|
|
30
|
+
height: number;
|
|
31
|
+
};
|
|
32
|
+
shadowOpacity: number;
|
|
33
|
+
shadowRadius: number;
|
|
34
|
+
elevation: number;
|
|
35
|
+
};
|
|
36
|
+
headerTitle: {
|
|
37
|
+
flexGrow: number;
|
|
38
|
+
flex: number;
|
|
39
|
+
};
|
|
40
|
+
footer: {
|
|
41
|
+
height: number;
|
|
42
|
+
flex: number;
|
|
43
|
+
justifyContent: "center";
|
|
44
|
+
alignItems: "center";
|
|
45
|
+
};
|
|
46
|
+
pressed: {
|
|
47
|
+
opacity: number;
|
|
48
|
+
};
|
|
49
|
+
actionItem: {
|
|
50
|
+
height: number;
|
|
51
|
+
justifyContent: "center";
|
|
52
|
+
};
|
|
53
|
+
actionLabel: {
|
|
54
|
+
paddingTop: number;
|
|
55
|
+
};
|
|
56
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ReactNode } from "react";
|
|
2
|
+
interface InternalCardHeaderProps {
|
|
3
|
+
readonly children: ReactNode[] | ReactNode;
|
|
4
|
+
readonly onPress?: () => void;
|
|
5
|
+
readonly testID?: string;
|
|
6
|
+
readonly collapsable: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare function InternalCardHeader({ onPress, children, testID, collapsable, }: InternalCardHeaderProps): JSX.Element;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { InternalCardHeader } from "./InternalCardHeader";
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/// <reference types="react" />
|
|
2
|
+
export type StatusType = "success" | "warning" | "critical" | "inactive" | "informative";
|
|
3
|
+
export interface StatusLabelType {
|
|
4
|
+
readonly statusLabel: string;
|
|
5
|
+
readonly statusType?: StatusType;
|
|
6
|
+
}
|
|
7
|
+
interface StatusLabelProps {
|
|
8
|
+
/**
|
|
9
|
+
* Text to display.
|
|
10
|
+
*/
|
|
11
|
+
readonly text: string;
|
|
12
|
+
/**
|
|
13
|
+
* Alignment of text
|
|
14
|
+
*/
|
|
15
|
+
readonly alignment?: "start" | "end";
|
|
16
|
+
/**
|
|
17
|
+
* Status color of the square beside text
|
|
18
|
+
*/
|
|
19
|
+
readonly status?: StatusType;
|
|
20
|
+
}
|
|
21
|
+
export declare function StatusLabel({ text, alignment, status, }: StatusLabelProps): JSX.Element;
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export declare const styles: {
|
|
2
|
+
statusLabelRow: {
|
|
3
|
+
flexDirection: "row";
|
|
4
|
+
justifyContent: "flex-end";
|
|
5
|
+
flexWrap: "nowrap";
|
|
6
|
+
};
|
|
7
|
+
statusLabelText: {
|
|
8
|
+
flexShrink: number;
|
|
9
|
+
};
|
|
10
|
+
statusLabelIcon: {
|
|
11
|
+
borderRadius: number;
|
|
12
|
+
backgroundColor: string;
|
|
13
|
+
width: number;
|
|
14
|
+
height: number;
|
|
15
|
+
marginTop: number;
|
|
16
|
+
};
|
|
17
|
+
labelTextStartAligned: {
|
|
18
|
+
flexDirection: "row-reverse";
|
|
19
|
+
};
|
|
20
|
+
innerPad: {
|
|
21
|
+
width: number;
|
|
22
|
+
};
|
|
23
|
+
};
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import { TextStyle } from "react-native";
|
|
2
|
+
/**
|
|
3
|
+
* Reusable typography tokens to ensure consistency for any client facing texts.
|
|
4
|
+
*/
|
|
5
|
+
export declare const typographyTokens: {
|
|
6
|
+
[index: string]: TextStyle;
|
|
7
|
+
};
|
|
2
8
|
/**
|
|
3
9
|
* `StyleSheet` for Typography.tsx.
|
|
4
10
|
*
|
|
@@ -9,12 +15,6 @@ import { TextStyle } from "react-native";
|
|
|
9
15
|
* import { typographyStyles } from "@jobber/components-native"
|
|
10
16
|
* ```
|
|
11
17
|
*/
|
|
12
|
-
/**
|
|
13
|
-
* Reusable typography tokens to ensure consistency for any client facing texts.
|
|
14
|
-
*/
|
|
15
|
-
export declare const typographyTokens: {
|
|
16
|
-
[index: string]: TextStyle;
|
|
17
|
-
};
|
|
18
18
|
export declare const typographyStyles: {
|
|
19
19
|
[index: string]: TextStyle;
|
|
20
20
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jobber/components-native",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"main": "dist/src/index.js",
|
|
6
6
|
"module": "dist/src/index.js",
|
|
@@ -44,5 +44,5 @@
|
|
|
44
44
|
"react": "^18",
|
|
45
45
|
"react-native": ">=0.69.2"
|
|
46
46
|
},
|
|
47
|
-
"gitHead": "
|
|
47
|
+
"gitHead": "f884bf403f42a1c85f7bf840a6317ae1642653e1"
|
|
48
48
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { StyleSheet } from "react-native";
|
|
2
|
+
import { tokens } from "../utils/design";
|
|
3
|
+
|
|
4
|
+
export const styles = StyleSheet.create({
|
|
5
|
+
container: {
|
|
6
|
+
width: "100%",
|
|
7
|
+
backgroundColor: tokens["color-surface"],
|
|
8
|
+
},
|
|
9
|
+
|
|
10
|
+
lowElevation: {
|
|
11
|
+
...tokens["shadow-low"],
|
|
12
|
+
},
|
|
13
|
+
|
|
14
|
+
baseElevation: {
|
|
15
|
+
...tokens["shadow-base"],
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
highElevation: {
|
|
19
|
+
...tokens["shadow-high"],
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
headerTitle: {
|
|
23
|
+
flexGrow: 1,
|
|
24
|
+
flex: 1,
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
footer: {
|
|
28
|
+
height: tokens["space-largest"],
|
|
29
|
+
flex: 1,
|
|
30
|
+
justifyContent: "center",
|
|
31
|
+
alignItems: "center",
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
pressed: {
|
|
35
|
+
opacity: tokens["opacity-pressed"],
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
actionItem: {
|
|
39
|
+
height: tokens["typography--lineHeight-base"],
|
|
40
|
+
justifyContent: "center",
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
actionLabel: {
|
|
44
|
+
paddingTop: tokens["space-smallest"],
|
|
45
|
+
},
|
|
46
|
+
});
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { fireEvent, render } from "@testing-library/react-native";
|
|
3
|
+
import { Card } from "./Card";
|
|
4
|
+
import { Text } from "../Text";
|
|
5
|
+
|
|
6
|
+
const cardHeaderTestID = "cardHeader";
|
|
7
|
+
const cardFooterTestID = "cardFooter";
|
|
8
|
+
|
|
9
|
+
it("renders with only a header title", () => {
|
|
10
|
+
const title = "Foobar";
|
|
11
|
+
const { getByTestId, getByText, queryAllByRole } = render(
|
|
12
|
+
<Card header={{ title }} />,
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
expect(getByText(title)).toBeDefined();
|
|
16
|
+
expect(getByTestId(cardHeaderTestID)).toBeDefined();
|
|
17
|
+
expect(queryAllByRole("button")).toEqual([]);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("renders with an icon action", () => {
|
|
21
|
+
const iconName = "plus2";
|
|
22
|
+
const { getByTestId } = render(
|
|
23
|
+
<Card
|
|
24
|
+
header={{
|
|
25
|
+
title: "Foobar",
|
|
26
|
+
actionItem: { iconName },
|
|
27
|
+
onPress: jest.fn(),
|
|
28
|
+
}}
|
|
29
|
+
/>,
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
expect(getByTestId(iconName)).toBeDefined();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("renders with a button action", () => {
|
|
36
|
+
const label = "Edit";
|
|
37
|
+
const { getByText } = render(
|
|
38
|
+
<Card
|
|
39
|
+
header={{
|
|
40
|
+
title: "Foobar",
|
|
41
|
+
actionItem: { label },
|
|
42
|
+
onPress: jest.fn(),
|
|
43
|
+
}}
|
|
44
|
+
/>,
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
expect(getByText(label)).toBeDefined();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("renders without a header and footer", () => {
|
|
51
|
+
const content = "I am the content";
|
|
52
|
+
const { getByText, queryByTestId } = render(
|
|
53
|
+
<Card>
|
|
54
|
+
<Text>{content}</Text>
|
|
55
|
+
</Card>,
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
expect(getByText(content)).toBeDefined();
|
|
59
|
+
expect(queryByTestId(cardHeaderTestID)).toBeNull();
|
|
60
|
+
expect(queryByTestId(cardFooterTestID)).toBeNull();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("should call the onPress when pressing the header", () => {
|
|
64
|
+
const pressHandler = jest.fn();
|
|
65
|
+
|
|
66
|
+
const { getByText } = render(
|
|
67
|
+
<Card
|
|
68
|
+
header={{
|
|
69
|
+
title: "Header",
|
|
70
|
+
actionItem: { iconName: "plus2" },
|
|
71
|
+
onPress: pressHandler,
|
|
72
|
+
}}
|
|
73
|
+
/>,
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
fireEvent.press(getByText("Header"));
|
|
77
|
+
expect(pressHandler).toHaveBeenCalled();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("should call the onPress when pressing the action button", () => {
|
|
81
|
+
const pressHandler = jest.fn();
|
|
82
|
+
|
|
83
|
+
const { getByText } = render(
|
|
84
|
+
<Card
|
|
85
|
+
header={{
|
|
86
|
+
title: "Header",
|
|
87
|
+
actionItem: { label: "Edit" },
|
|
88
|
+
onPress: pressHandler,
|
|
89
|
+
}}
|
|
90
|
+
/>,
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
fireEvent.press(getByText("Edit"));
|
|
94
|
+
expect(pressHandler).toHaveBeenCalled();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("renders with a footer", () => {
|
|
98
|
+
const title = "View All";
|
|
99
|
+
const { getByText, getByTestId, queryByTestId } = render(
|
|
100
|
+
<Card footer={{ title: title, onPress: jest.fn() }} />,
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
expect(getByText(title)).toBeDefined();
|
|
104
|
+
expect(getByTestId(cardFooterTestID)).toBeDefined();
|
|
105
|
+
expect(queryByTestId(cardHeaderTestID)).toBeNull();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("should call the onPress when pressing the footer", () => {
|
|
109
|
+
const headerPressHandler = jest.fn();
|
|
110
|
+
const footerPressHandler = jest.fn();
|
|
111
|
+
|
|
112
|
+
const { getByText } = render(
|
|
113
|
+
<Card
|
|
114
|
+
header={{
|
|
115
|
+
title: "Header",
|
|
116
|
+
actionItem: { iconName: "plus2" },
|
|
117
|
+
onPress: headerPressHandler,
|
|
118
|
+
}}
|
|
119
|
+
footer={{
|
|
120
|
+
title: "View All",
|
|
121
|
+
onPress: footerPressHandler,
|
|
122
|
+
}}
|
|
123
|
+
/>,
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
fireEvent.press(getByText("View All"));
|
|
127
|
+
expect(footerPressHandler).toHaveBeenCalled();
|
|
128
|
+
});
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import React, { ReactNode } from "react";
|
|
2
|
+
import {
|
|
3
|
+
LayoutChangeEvent,
|
|
4
|
+
Pressable,
|
|
5
|
+
PressableStateCallbackType,
|
|
6
|
+
View,
|
|
7
|
+
} from "react-native";
|
|
8
|
+
import { IconNames } from "@jobber/design";
|
|
9
|
+
import { XOR } from "ts-xor";
|
|
10
|
+
import { styles } from "./Card.style";
|
|
11
|
+
// eslint-disable-next-line import/no-internal-modules
|
|
12
|
+
import { InternalCardHeader } from "./components/InternalCardHeader";
|
|
13
|
+
import { ErrorMessageWrapper } from "../ErrorMessageWrapper";
|
|
14
|
+
import { ActionLabel } from "../ActionLabel";
|
|
15
|
+
import { Typography } from "../Typography";
|
|
16
|
+
import { Icon } from "../Icon";
|
|
17
|
+
|
|
18
|
+
interface CardProps {
|
|
19
|
+
/**
|
|
20
|
+
* @deprecated Use <ActionItem /> with the title and onPress properties instead
|
|
21
|
+
*/
|
|
22
|
+
readonly header?: HeaderProps;
|
|
23
|
+
|
|
24
|
+
readonly footer?: FooterProps;
|
|
25
|
+
readonly children?: ReactNode;
|
|
26
|
+
readonly reportCardHeight?: (height: number) => void;
|
|
27
|
+
readonly testID?: string;
|
|
28
|
+
readonly error?: string;
|
|
29
|
+
readonly elevation?: elevationProp;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
type elevationProp = "none" | "low" | "base" | "high";
|
|
33
|
+
|
|
34
|
+
export type HeaderProps = HeaderCommonProps & HeaderActionProps;
|
|
35
|
+
|
|
36
|
+
interface FooterProps {
|
|
37
|
+
readonly onPress: () => void;
|
|
38
|
+
readonly title: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface HeaderCommonProps {
|
|
42
|
+
readonly title: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
type HeaderActionProps =
|
|
46
|
+
| {
|
|
47
|
+
readonly onPress?: never;
|
|
48
|
+
readonly actionItem?: never;
|
|
49
|
+
}
|
|
50
|
+
| {
|
|
51
|
+
readonly onPress: () => void;
|
|
52
|
+
readonly actionItem: ActionItem;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
interface IconAction {
|
|
56
|
+
readonly iconName: IconNames;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface ButtonAction {
|
|
60
|
+
readonly label: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export type ActionItem = XOR<IconAction, ButtonAction>;
|
|
64
|
+
|
|
65
|
+
function getElevationStyle(elevation: elevationProp) {
|
|
66
|
+
if (elevation === "none") {
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
return styles[`${elevation}Elevation`];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function Card({
|
|
73
|
+
header,
|
|
74
|
+
footer,
|
|
75
|
+
children,
|
|
76
|
+
reportCardHeight: onCardHeightChange,
|
|
77
|
+
testID = "card",
|
|
78
|
+
error,
|
|
79
|
+
elevation = "none",
|
|
80
|
+
}: CardProps): JSX.Element {
|
|
81
|
+
return (
|
|
82
|
+
<ErrorMessageWrapper message={error} wrapFor="card">
|
|
83
|
+
<View
|
|
84
|
+
onLayout={handleLayoutChange}
|
|
85
|
+
style={[styles.container, getElevationStyle(elevation)]}
|
|
86
|
+
testID={testID}
|
|
87
|
+
>
|
|
88
|
+
{header && (
|
|
89
|
+
<>
|
|
90
|
+
<InternalCardHeader
|
|
91
|
+
onPress={header.onPress}
|
|
92
|
+
testID={`${testID}Header`}
|
|
93
|
+
collapsable={!!children}
|
|
94
|
+
>
|
|
95
|
+
<View style={styles.headerTitle}>
|
|
96
|
+
<Typography
|
|
97
|
+
color="heading"
|
|
98
|
+
fontFamily="base"
|
|
99
|
+
fontWeight="bold"
|
|
100
|
+
size="default"
|
|
101
|
+
lineHeight="base"
|
|
102
|
+
accessibilityRole="header"
|
|
103
|
+
>
|
|
104
|
+
{header.title}
|
|
105
|
+
</Typography>
|
|
106
|
+
</View>
|
|
107
|
+
<View style={styles.actionItem}>
|
|
108
|
+
{!!header.actionItem?.label && (
|
|
109
|
+
<View style={styles.actionLabel}>
|
|
110
|
+
<ActionLabel type="cardTitle">
|
|
111
|
+
{header.actionItem.label}
|
|
112
|
+
</ActionLabel>
|
|
113
|
+
</View>
|
|
114
|
+
)}
|
|
115
|
+
|
|
116
|
+
{header.actionItem?.iconName && (
|
|
117
|
+
<Icon name={header.actionItem.iconName} color="interactive" />
|
|
118
|
+
)}
|
|
119
|
+
</View>
|
|
120
|
+
</InternalCardHeader>
|
|
121
|
+
</>
|
|
122
|
+
)}
|
|
123
|
+
{children}
|
|
124
|
+
{footer && (
|
|
125
|
+
<Pressable
|
|
126
|
+
testID={`${testID}Footer`}
|
|
127
|
+
onPress={footer.onPress}
|
|
128
|
+
style={({ pressed }: PressableStateCallbackType) => [
|
|
129
|
+
styles.footer,
|
|
130
|
+
pressed && styles.pressed,
|
|
131
|
+
]}
|
|
132
|
+
accessibilityRole={"button"}
|
|
133
|
+
>
|
|
134
|
+
<ActionLabel>{footer.title}</ActionLabel>
|
|
135
|
+
</Pressable>
|
|
136
|
+
)}
|
|
137
|
+
</View>
|
|
138
|
+
</ErrorMessageWrapper>
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
function handleLayoutChange(event: LayoutChangeEvent) {
|
|
142
|
+
const { height } = event.nativeEvent.layout;
|
|
143
|
+
onCardHeightChange?.(height);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { StyleSheet } from "react-native";
|
|
2
|
+
import { tokens } from "../../utils/design";
|
|
3
|
+
|
|
4
|
+
export const styles = StyleSheet.create({
|
|
5
|
+
header: {
|
|
6
|
+
flexDirection: "row",
|
|
7
|
+
alignItems: "flex-start",
|
|
8
|
+
paddingTop: tokens["space-small"] + tokens["space-smaller"],
|
|
9
|
+
paddingHorizontal: tokens["space-base"],
|
|
10
|
+
},
|
|
11
|
+
|
|
12
|
+
pressed: {
|
|
13
|
+
opacity: tokens["opacity-pressed"],
|
|
14
|
+
},
|
|
15
|
+
|
|
16
|
+
noChildren: {
|
|
17
|
+
paddingBottom: tokens["space-small"] + tokens["space-smaller"],
|
|
18
|
+
},
|
|
19
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { fireEvent, render } from "@testing-library/react-native";
|
|
3
|
+
import { InternalCardHeader } from "./InternalCardHeader";
|
|
4
|
+
|
|
5
|
+
it("should render a pressable header", () => {
|
|
6
|
+
const handlePress = jest.fn();
|
|
7
|
+
const screen = render(
|
|
8
|
+
<InternalCardHeader collapsable onPress={handlePress} testID="cardHeader">
|
|
9
|
+
🍩
|
|
10
|
+
</InternalCardHeader>,
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
const header = screen.getByTestId("cardHeader");
|
|
14
|
+
expect(header).toBeDefined();
|
|
15
|
+
expect(header.props.accessibilityRole).toBe("button");
|
|
16
|
+
|
|
17
|
+
fireEvent.press(header);
|
|
18
|
+
expect(handlePress).toHaveBeenCalled();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("should render an un-pressable header", () => {
|
|
22
|
+
const screen = render(
|
|
23
|
+
<InternalCardHeader collapsable testID="cardHeader">
|
|
24
|
+
🌞
|
|
25
|
+
</InternalCardHeader>,
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
const header = screen.getByTestId("cardHeader");
|
|
29
|
+
expect(header).toBeDefined();
|
|
30
|
+
expect(header.props.onPress).toBeUndefined();
|
|
31
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import React, { ReactNode } from "react";
|
|
2
|
+
import { Pressable, View } from "react-native";
|
|
3
|
+
import { styles } from "./InternalCardHeader.style";
|
|
4
|
+
|
|
5
|
+
interface InternalCardHeaderProps {
|
|
6
|
+
readonly children: ReactNode[] | ReactNode;
|
|
7
|
+
readonly onPress?: () => void;
|
|
8
|
+
readonly testID?: string;
|
|
9
|
+
readonly collapsable: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function InternalCardHeader({
|
|
13
|
+
onPress,
|
|
14
|
+
children,
|
|
15
|
+
testID,
|
|
16
|
+
collapsable,
|
|
17
|
+
}: InternalCardHeaderProps): JSX.Element {
|
|
18
|
+
const conditionalChildStyling = collapsable ? undefined : styles.noChildren;
|
|
19
|
+
if (onPress) {
|
|
20
|
+
return (
|
|
21
|
+
<Pressable
|
|
22
|
+
testID={testID}
|
|
23
|
+
onPress={onPress}
|
|
24
|
+
style={({ pressed }) => [
|
|
25
|
+
styles.header,
|
|
26
|
+
pressed && styles.pressed,
|
|
27
|
+
conditionalChildStyling,
|
|
28
|
+
]}
|
|
29
|
+
accessibilityRole={"button"}
|
|
30
|
+
>
|
|
31
|
+
{children}
|
|
32
|
+
</Pressable>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<View testID={testID} style={[styles.header, conditionalChildStyling]}>
|
|
38
|
+
{children}
|
|
39
|
+
</View>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { InternalCardHeader } from "./InternalCardHeader";
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { StyleSheet } from "react-native";
|
|
2
|
+
import { tokens } from "../utils/design";
|
|
3
|
+
|
|
4
|
+
const statusLabelIconDiameter = tokens["space-base"] - tokens["space-smaller"]; //12px
|
|
5
|
+
|
|
6
|
+
const indicatorOffset = tokens["space-smallest"] + tokens["space-minuscule"];
|
|
7
|
+
|
|
8
|
+
export const styles = StyleSheet.create({
|
|
9
|
+
statusLabelRow: {
|
|
10
|
+
flexDirection: "row",
|
|
11
|
+
justifyContent: "flex-end",
|
|
12
|
+
flexWrap: "nowrap",
|
|
13
|
+
},
|
|
14
|
+
statusLabelText: {
|
|
15
|
+
flexShrink: 1,
|
|
16
|
+
},
|
|
17
|
+
statusLabelIcon: {
|
|
18
|
+
borderRadius: tokens["radius-base"],
|
|
19
|
+
backgroundColor: tokens["color-success"],
|
|
20
|
+
width: statusLabelIconDiameter,
|
|
21
|
+
height: statusLabelIconDiameter,
|
|
22
|
+
marginTop: indicatorOffset,
|
|
23
|
+
},
|
|
24
|
+
labelTextStartAligned: {
|
|
25
|
+
flexDirection: "row-reverse",
|
|
26
|
+
},
|
|
27
|
+
innerPad: {
|
|
28
|
+
width: tokens["space-small"],
|
|
29
|
+
},
|
|
30
|
+
});
|