@jobber/components-native 0.11.0 → 0.13.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/Chip/Chip.js +43 -0
- package/dist/src/Chip/Chip.style.js +32 -0
- package/dist/src/Chip/index.js +1 -0
- package/dist/src/Heading/Heading.js +51 -0
- package/dist/src/Heading/index.js +1 -0
- package/dist/src/index.js +2 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/src/Chip/Chip.d.ts +45 -0
- package/dist/types/src/Chip/Chip.style.d.ts +29 -0
- package/dist/types/src/Chip/index.d.ts +2 -0
- package/dist/types/src/Heading/Heading.d.ts +37 -0
- package/dist/types/src/Heading/index.d.ts +2 -0
- package/dist/types/src/index.d.ts +2 -0
- package/package.json +2 -2
- package/src/Chip/Chip.style.ts +34 -0
- package/src/Chip/Chip.test.tsx +133 -0
- package/src/Chip/Chip.tsx +142 -0
- package/src/Chip/index.ts +2 -0
- package/src/Heading/Heading.test.tsx +83 -0
- package/src/Heading/Heading.tsx +132 -0
- package/src/Heading/__snapshots__/Heading.test.tsx.snap +270 -0
- package/src/Heading/index.ts +2 -0
- package/src/index.ts +2 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/// <reference types="react" />
|
|
2
|
+
import { AccessibilityRole } from "react-native";
|
|
3
|
+
import { IconNames } from "@jobber/design";
|
|
4
|
+
export type AccentType = "client" | "invoice" | "job" | "request" | "quote";
|
|
5
|
+
export interface ChipProps {
|
|
6
|
+
/**
|
|
7
|
+
* label of the chip.
|
|
8
|
+
*/
|
|
9
|
+
readonly label?: string;
|
|
10
|
+
/**
|
|
11
|
+
* chip's active status
|
|
12
|
+
*/
|
|
13
|
+
readonly isActive: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Boolean for chip's ability to be dismissed
|
|
16
|
+
*/
|
|
17
|
+
readonly isDismissible?: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Background color to be used for inactive chips
|
|
20
|
+
*
|
|
21
|
+
* @default "background"
|
|
22
|
+
*/
|
|
23
|
+
readonly inactiveBackgroundColor?: "surface" | "background";
|
|
24
|
+
/**
|
|
25
|
+
* Accessibility label for the component. It's also used for testing
|
|
26
|
+
*/
|
|
27
|
+
readonly accessibilityLabel?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Accessibility role for the component
|
|
30
|
+
*/
|
|
31
|
+
readonly accessibilityRole?: AccessibilityRole;
|
|
32
|
+
/**
|
|
33
|
+
* Press handler
|
|
34
|
+
*/
|
|
35
|
+
onPress?(): void;
|
|
36
|
+
/**
|
|
37
|
+
* Optional Icon
|
|
38
|
+
*/
|
|
39
|
+
readonly icon?: IconNames;
|
|
40
|
+
/**
|
|
41
|
+
* Background color to be used for Active chips
|
|
42
|
+
*/
|
|
43
|
+
readonly accent?: AccentType;
|
|
44
|
+
}
|
|
45
|
+
export declare function Chip({ icon, label, onPress, isDismissible, isActive, inactiveBackgroundColor, accessibilityLabel, accessibilityRole, accent, }: ChipProps): JSX.Element;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export declare const styles: {
|
|
2
|
+
container: {
|
|
3
|
+
alignItems: "center";
|
|
4
|
+
borderRadius: number;
|
|
5
|
+
flexDirection: "row";
|
|
6
|
+
height: number;
|
|
7
|
+
justifyContent: "center";
|
|
8
|
+
marginHorizontal: number;
|
|
9
|
+
marginTop: number;
|
|
10
|
+
paddingHorizontal: number;
|
|
11
|
+
};
|
|
12
|
+
iconLeft: {
|
|
13
|
+
marginHorizontal: number;
|
|
14
|
+
};
|
|
15
|
+
chipText: {
|
|
16
|
+
flexGrow: number;
|
|
17
|
+
flexShrink: number;
|
|
18
|
+
marginHorizontal: number;
|
|
19
|
+
};
|
|
20
|
+
dismissIcon: {
|
|
21
|
+
backgroundColor: string;
|
|
22
|
+
borderRadius: number;
|
|
23
|
+
marginLeft: number;
|
|
24
|
+
padding: number;
|
|
25
|
+
};
|
|
26
|
+
activeDismissIcon: {
|
|
27
|
+
backgroundColor: string;
|
|
28
|
+
};
|
|
29
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/// <reference types="react" />
|
|
2
|
+
import { TextAlign, TextColor, TruncateLength, TypographyProps } from "../Typography";
|
|
3
|
+
type HeadingColor = Extract<TextColor, "text" | "subdued" | "heading">;
|
|
4
|
+
export type HeadingLevel = "title" | "subtitle" | "heading" | "subHeading";
|
|
5
|
+
interface HeadingProps<T extends HeadingLevel> extends Pick<TypographyProps<"base">, "selectable"> {
|
|
6
|
+
/**
|
|
7
|
+
* Text to display.
|
|
8
|
+
*/
|
|
9
|
+
readonly children: string;
|
|
10
|
+
/**
|
|
11
|
+
* The type of heading, e.g., "Title"
|
|
12
|
+
*/
|
|
13
|
+
readonly level?: T;
|
|
14
|
+
/**
|
|
15
|
+
* The text color of heading
|
|
16
|
+
*/
|
|
17
|
+
readonly variation?: HeadingColor;
|
|
18
|
+
/**
|
|
19
|
+
* Uses the reverse variant of the text color for the heading
|
|
20
|
+
*/
|
|
21
|
+
readonly reverseTheme?: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Alignment of heading
|
|
24
|
+
*/
|
|
25
|
+
readonly align?: TextAlign;
|
|
26
|
+
/**
|
|
27
|
+
* The maximum amount of lines the text can occupy before being truncated with "...".
|
|
28
|
+
* Uses predefined string values that correspond to a doubling scale for the amount of lines.
|
|
29
|
+
*/
|
|
30
|
+
readonly maxLines?: TruncateLength;
|
|
31
|
+
/**
|
|
32
|
+
* Allow text to be resized based on user's device display scale
|
|
33
|
+
*/
|
|
34
|
+
readonly allowFontScaling?: boolean;
|
|
35
|
+
}
|
|
36
|
+
export declare function Heading<T extends HeadingLevel = "heading">({ children, level, variation, reverseTheme, align, maxLines, allowFontScaling, selectable, }: HeadingProps<T>): JSX.Element;
|
|
37
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jobber/components-native",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"main": "dist/src/index.js",
|
|
6
6
|
"module": "dist/src/index.js",
|
|
@@ -47,5 +47,5 @@
|
|
|
47
47
|
"react": "^18",
|
|
48
48
|
"react-native": ">=0.69.2"
|
|
49
49
|
},
|
|
50
|
-
"gitHead": "
|
|
50
|
+
"gitHead": "bfc022e4558ae4f056b05b1186c4f82eb137ae0e"
|
|
51
51
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { StyleSheet } from "react-native";
|
|
2
|
+
import { tokens } from "../utils/design";
|
|
3
|
+
|
|
4
|
+
const chipHeight = tokens["space-larger"] + tokens["space-small"];
|
|
5
|
+
|
|
6
|
+
export const styles = StyleSheet.create({
|
|
7
|
+
container: {
|
|
8
|
+
alignItems: "center",
|
|
9
|
+
borderRadius: tokens["radius-circle"],
|
|
10
|
+
flexDirection: "row",
|
|
11
|
+
height: chipHeight,
|
|
12
|
+
justifyContent: "center",
|
|
13
|
+
marginHorizontal: tokens["space-smaller"],
|
|
14
|
+
marginTop: tokens["space-small"],
|
|
15
|
+
paddingHorizontal: tokens["space-small"],
|
|
16
|
+
},
|
|
17
|
+
iconLeft: {
|
|
18
|
+
marginHorizontal: tokens["space-smallest"],
|
|
19
|
+
},
|
|
20
|
+
chipText: {
|
|
21
|
+
flexGrow: 1,
|
|
22
|
+
flexShrink: 1,
|
|
23
|
+
marginHorizontal: tokens["space-smallest"],
|
|
24
|
+
},
|
|
25
|
+
dismissIcon: {
|
|
26
|
+
backgroundColor: tokens["color-surface"],
|
|
27
|
+
borderRadius: tokens["radius-circle"],
|
|
28
|
+
marginLeft: tokens["space-smaller"],
|
|
29
|
+
padding: tokens["space-smaller"],
|
|
30
|
+
},
|
|
31
|
+
activeDismissIcon: {
|
|
32
|
+
backgroundColor: tokens["color-surface--background"],
|
|
33
|
+
},
|
|
34
|
+
});
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { fireEvent, render } from "@testing-library/react-native";
|
|
3
|
+
import { Chip } from "./Chip";
|
|
4
|
+
import { tokens } from "../utils/design";
|
|
5
|
+
|
|
6
|
+
it("renders an active Chip", () => {
|
|
7
|
+
const { getByText, getByLabelText } = render(
|
|
8
|
+
<Chip
|
|
9
|
+
label="Foo"
|
|
10
|
+
onPress={jest.fn()}
|
|
11
|
+
accessibilityLabel={"Foo chip"}
|
|
12
|
+
isActive
|
|
13
|
+
/>,
|
|
14
|
+
);
|
|
15
|
+
expect(getByText("Foo")).toBeDefined();
|
|
16
|
+
expect(getByLabelText("Foo chip").props.style).toContainEqual({
|
|
17
|
+
backgroundColor: tokens["color-surface--reverse"],
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("renders an active Chip without onPress as disabled", () => {
|
|
22
|
+
const { getByTestId } = render(
|
|
23
|
+
<Chip label="Foo" accessibilityLabel={"Foo chip"} isActive />,
|
|
24
|
+
);
|
|
25
|
+
expect(
|
|
26
|
+
getByTestId("chipTest").props.accessibilityState.disabled,
|
|
27
|
+
).toBeTruthy();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("renders an inactive Chip with a default backgroundColor", () => {
|
|
31
|
+
const { getByTestId } = render(
|
|
32
|
+
<Chip label="Foo" onPress={jest.fn()} isActive={false} />,
|
|
33
|
+
);
|
|
34
|
+
expect(getByTestId("chipTest").props.style).not.toContainEqual({
|
|
35
|
+
backgroundColor: tokens["color-surface--reverse"],
|
|
36
|
+
});
|
|
37
|
+
expect(getByTestId("chipTest").props.style).toContainEqual({
|
|
38
|
+
backgroundColor: tokens["color-surface--background"],
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("renders an inactive Chip with a surface backgroundColor", () => {
|
|
43
|
+
const { getByTestId } = render(
|
|
44
|
+
<Chip
|
|
45
|
+
label="Foo"
|
|
46
|
+
onPress={jest.fn()}
|
|
47
|
+
isActive={false}
|
|
48
|
+
inactiveBackgroundColor={"surface"}
|
|
49
|
+
/>,
|
|
50
|
+
);
|
|
51
|
+
expect(getByTestId("chipTest").props.style).toContainEqual({
|
|
52
|
+
backgroundColor: tokens["color-surface"],
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("renders an active Chip with icon", () => {
|
|
57
|
+
const { getByTestId } = render(
|
|
58
|
+
<Chip onPress={jest.fn()} icon="invoice" isActive />,
|
|
59
|
+
);
|
|
60
|
+
expect(getByTestId("invoice")).toBeDefined();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("renders an inactive Chip with icon", () => {
|
|
64
|
+
const { getByTestId } = render(
|
|
65
|
+
<Chip onPress={jest.fn()} icon="invoice" isActive={false} />,
|
|
66
|
+
);
|
|
67
|
+
expect(getByTestId("invoice")).toBeDefined();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("renders a Chip with the dismiss icon", () => {
|
|
71
|
+
const { getByTestId } = render(
|
|
72
|
+
<Chip label="Foo" onPress={jest.fn()} isActive isDismissible={true} />,
|
|
73
|
+
);
|
|
74
|
+
expect(getByTestId("remove")).toBeDefined();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("should call the handler with the new value", () => {
|
|
78
|
+
const pressHandler = jest.fn();
|
|
79
|
+
const accessibilityLabel = "test chip";
|
|
80
|
+
const { getByLabelText } = render(
|
|
81
|
+
<Chip
|
|
82
|
+
onPress={pressHandler}
|
|
83
|
+
label={"foo"}
|
|
84
|
+
accessibilityLabel={accessibilityLabel}
|
|
85
|
+
isActive
|
|
86
|
+
/>,
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
fireEvent.press(getByLabelText(accessibilityLabel));
|
|
90
|
+
expect(pressHandler).toHaveBeenCalled();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe("accessibilityLabel", () => {
|
|
94
|
+
it("uses accessibilityLabel if specified", () => {
|
|
95
|
+
const pressHandler = jest.fn();
|
|
96
|
+
const { getByLabelText } = render(
|
|
97
|
+
<Chip
|
|
98
|
+
onPress={pressHandler}
|
|
99
|
+
label="label"
|
|
100
|
+
accessibilityLabel="accessibilityLabel"
|
|
101
|
+
isActive
|
|
102
|
+
/>,
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
expect(getByLabelText("accessibilityLabel")).toBeTruthy();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("uses label if unspecified", () => {
|
|
109
|
+
const pressHandler = jest.fn();
|
|
110
|
+
const { getByLabelText } = render(
|
|
111
|
+
<Chip onPress={pressHandler} label="label" isActive />,
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
expect(getByLabelText("label")).toBeTruthy();
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe("accent", () => {
|
|
119
|
+
it("uses accent color when present and chip is Active", () => {
|
|
120
|
+
const { getByTestId } = render(
|
|
121
|
+
<Chip
|
|
122
|
+
label="Foo"
|
|
123
|
+
onPress={jest.fn()}
|
|
124
|
+
accessibilityLabel={"Foo chip"}
|
|
125
|
+
isActive={true}
|
|
126
|
+
accent={"client"}
|
|
127
|
+
/>,
|
|
128
|
+
);
|
|
129
|
+
expect(getByTestId("chipTest").props.style).toContainEqual({
|
|
130
|
+
backgroundColor: tokens["color-client"],
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
});
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import React, { useMemo } from "react";
|
|
2
|
+
import { AccessibilityRole, Pressable, View } from "react-native";
|
|
3
|
+
import { IconNames } from "@jobber/design";
|
|
4
|
+
import { styles } from "./Chip.style";
|
|
5
|
+
import { Icon } from "../Icon";
|
|
6
|
+
import { Typography } from "../Typography";
|
|
7
|
+
import { tokens } from "../utils/design";
|
|
8
|
+
|
|
9
|
+
export type AccentType = "client" | "invoice" | "job" | "request" | "quote";
|
|
10
|
+
|
|
11
|
+
export interface ChipProps {
|
|
12
|
+
/**
|
|
13
|
+
* label of the chip.
|
|
14
|
+
*/
|
|
15
|
+
readonly label?: string;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* chip's active status
|
|
19
|
+
*/
|
|
20
|
+
readonly isActive: boolean;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Boolean for chip's ability to be dismissed
|
|
24
|
+
*/
|
|
25
|
+
readonly isDismissible?: boolean;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Background color to be used for inactive chips
|
|
29
|
+
*
|
|
30
|
+
* @default "background"
|
|
31
|
+
*/
|
|
32
|
+
readonly inactiveBackgroundColor?: "surface" | "background";
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Accessibility label for the component. It's also used for testing
|
|
36
|
+
*/
|
|
37
|
+
readonly accessibilityLabel?: string;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Accessibility role for the component
|
|
41
|
+
*/
|
|
42
|
+
readonly accessibilityRole?: AccessibilityRole;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Press handler
|
|
46
|
+
*/
|
|
47
|
+
onPress?(): void;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Optional Icon
|
|
51
|
+
*/
|
|
52
|
+
readonly icon?: IconNames;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Background color to be used for Active chips
|
|
56
|
+
*/
|
|
57
|
+
readonly accent?: AccentType;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const defaultAccentColor = tokens["color-surface--reverse"];
|
|
61
|
+
|
|
62
|
+
export function Chip({
|
|
63
|
+
icon,
|
|
64
|
+
label,
|
|
65
|
+
onPress,
|
|
66
|
+
isDismissible,
|
|
67
|
+
isActive,
|
|
68
|
+
inactiveBackgroundColor = "background",
|
|
69
|
+
accessibilityLabel,
|
|
70
|
+
accessibilityRole = "radio",
|
|
71
|
+
accent,
|
|
72
|
+
}: ChipProps): JSX.Element {
|
|
73
|
+
const { chipStyle, iconCustomColor, dismissColor } = useMemo(() => {
|
|
74
|
+
const accentColor = accent ? tokens[`color-${accent}`] : defaultAccentColor;
|
|
75
|
+
|
|
76
|
+
const iconColor = isActive ? tokens["color-surface"] : accentColor;
|
|
77
|
+
const chip = [
|
|
78
|
+
styles.container,
|
|
79
|
+
{
|
|
80
|
+
backgroundColor:
|
|
81
|
+
inactiveBackgroundColor === "surface"
|
|
82
|
+
? tokens["color-surface"]
|
|
83
|
+
: tokens["color-surface--background"],
|
|
84
|
+
},
|
|
85
|
+
isActive && { backgroundColor: accentColor },
|
|
86
|
+
];
|
|
87
|
+
const dismiss =
|
|
88
|
+
(isActive || inactiveBackgroundColor === "surface") &&
|
|
89
|
+
styles.activeDismissIcon;
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
chipStyle: chip,
|
|
93
|
+
iconCustomColor: iconColor,
|
|
94
|
+
dismissColor: dismiss,
|
|
95
|
+
};
|
|
96
|
+
}, [accent, isActive, inactiveBackgroundColor]);
|
|
97
|
+
|
|
98
|
+
const accessibilityState = useMemo(() => {
|
|
99
|
+
const checkableRoles = ["radio", "switch", "togglebutton", "checkbox"];
|
|
100
|
+
if (checkableRoles.includes(accessibilityRole)) {
|
|
101
|
+
return { checked: isActive };
|
|
102
|
+
}
|
|
103
|
+
return {};
|
|
104
|
+
}, [accessibilityRole, isActive]);
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<Pressable
|
|
108
|
+
testID="chipTest"
|
|
109
|
+
hitSlop={{ top: 8, bottom: 8, left: 4, right: 4 }}
|
|
110
|
+
onPress={onPress}
|
|
111
|
+
disabled={typeof onPress !== "function"}
|
|
112
|
+
style={chipStyle}
|
|
113
|
+
accessibilityLabel={accessibilityLabel || label}
|
|
114
|
+
accessibilityRole={accessibilityRole}
|
|
115
|
+
accessibilityState={accessibilityState}
|
|
116
|
+
>
|
|
117
|
+
{icon && (
|
|
118
|
+
<View style={styles.iconLeft}>
|
|
119
|
+
<Icon name={icon} size={"base"} customColor={iconCustomColor} />
|
|
120
|
+
</View>
|
|
121
|
+
)}
|
|
122
|
+
{label && (
|
|
123
|
+
<View style={styles.chipText}>
|
|
124
|
+
<Typography
|
|
125
|
+
color="base"
|
|
126
|
+
fontWeight="medium"
|
|
127
|
+
maxFontScaleSize={tokens["typography--fontSize-large"]}
|
|
128
|
+
maxLines="single"
|
|
129
|
+
reverseTheme={isActive}
|
|
130
|
+
>
|
|
131
|
+
{label}
|
|
132
|
+
</Typography>
|
|
133
|
+
</View>
|
|
134
|
+
)}
|
|
135
|
+
{isDismissible && (
|
|
136
|
+
<View style={[styles.dismissIcon, dismissColor]}>
|
|
137
|
+
<Icon name={"remove"} size={"small"} />
|
|
138
|
+
</View>
|
|
139
|
+
)}
|
|
140
|
+
</Pressable>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { render } from "@testing-library/react-native";
|
|
3
|
+
import { Heading } from "./Heading";
|
|
4
|
+
|
|
5
|
+
describe("when Heading called with text as the only prop", () => {
|
|
6
|
+
it("should match snapshot", () => {
|
|
7
|
+
const view = render(<Heading>Default Heading</Heading>).toJSON();
|
|
8
|
+
|
|
9
|
+
expect(view).toMatchSnapshot();
|
|
10
|
+
});
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
describe("when Heading called with reverseTheme", () => {
|
|
14
|
+
it("should match snapshot", () => {
|
|
15
|
+
const view = render(
|
|
16
|
+
<Heading reverseTheme>Reverse Theme Heading</Heading>,
|
|
17
|
+
).toJSON();
|
|
18
|
+
|
|
19
|
+
expect(view).toMatchSnapshot();
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe("when Heading called with title variation", () => {
|
|
24
|
+
it("should match snapshot", () => {
|
|
25
|
+
const view = render(
|
|
26
|
+
<Heading level={"title"}>Title Heading</Heading>,
|
|
27
|
+
).toJSON();
|
|
28
|
+
|
|
29
|
+
expect(view).toMatchSnapshot();
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe("when Heading called with Subtitle variation", () => {
|
|
34
|
+
it("should match snapshot", () => {
|
|
35
|
+
const view = render(
|
|
36
|
+
<Heading level={"subtitle"}>Subtitle</Heading>,
|
|
37
|
+
).toJSON();
|
|
38
|
+
|
|
39
|
+
expect(view).toMatchSnapshot();
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe("when Heading called with sub-heading variation", () => {
|
|
44
|
+
it("should match snapshot", () => {
|
|
45
|
+
const view = render(
|
|
46
|
+
<Heading level={"subHeading"}>Sub-Heading</Heading>,
|
|
47
|
+
).toJSON();
|
|
48
|
+
|
|
49
|
+
expect(view).toMatchSnapshot();
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe("when Heading called with sub-heading variation and text-color", () => {
|
|
54
|
+
it("should match snapshot", () => {
|
|
55
|
+
const view = render(
|
|
56
|
+
<Heading level={"subHeading"} variation={"subdued"}>
|
|
57
|
+
Sub-Heading
|
|
58
|
+
</Heading>,
|
|
59
|
+
).toJSON();
|
|
60
|
+
|
|
61
|
+
expect(view).toMatchSnapshot();
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe("when Heading called with an alignment", () => {
|
|
66
|
+
it("should match snapshot", () => {
|
|
67
|
+
const view = render(
|
|
68
|
+
<Heading align={"end"}>Text Aligned Right</Heading>,
|
|
69
|
+
).toJSON();
|
|
70
|
+
|
|
71
|
+
expect(view).toMatchSnapshot();
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe("when Heading called with maxLines", () => {
|
|
76
|
+
it("should match snapshot", () => {
|
|
77
|
+
const view = render(
|
|
78
|
+
<Heading maxLines="single">Text Aligned Right</Heading>,
|
|
79
|
+
).toJSON();
|
|
80
|
+
|
|
81
|
+
expect(view).toMatchSnapshot();
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import {
|
|
3
|
+
FontFamily,
|
|
4
|
+
TextAlign,
|
|
5
|
+
TextColor,
|
|
6
|
+
TruncateLength,
|
|
7
|
+
Typography,
|
|
8
|
+
TypographyProps,
|
|
9
|
+
} from "../Typography";
|
|
10
|
+
import { tokens } from "../utils/design";
|
|
11
|
+
|
|
12
|
+
type HeadingStyle = Pick<
|
|
13
|
+
TypographyProps<FontFamily>,
|
|
14
|
+
"fontFamily" | "fontWeight" | "size" | "lineHeight" | "color"
|
|
15
|
+
>;
|
|
16
|
+
type HeadingColor = Extract<TextColor, "text" | "subdued" | "heading">;
|
|
17
|
+
export type HeadingLevel = "title" | "subtitle" | "heading" | "subHeading";
|
|
18
|
+
|
|
19
|
+
interface HeadingProps<T extends HeadingLevel>
|
|
20
|
+
extends Pick<TypographyProps<"base">, "selectable"> {
|
|
21
|
+
/**
|
|
22
|
+
* Text to display.
|
|
23
|
+
*/
|
|
24
|
+
readonly children: string;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The type of heading, e.g., "Title"
|
|
28
|
+
*/
|
|
29
|
+
readonly level?: T;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The text color of heading
|
|
33
|
+
*/
|
|
34
|
+
readonly variation?: HeadingColor;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Uses the reverse variant of the text color for the heading
|
|
38
|
+
*/
|
|
39
|
+
readonly reverseTheme?: boolean;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Alignment of heading
|
|
43
|
+
*/
|
|
44
|
+
readonly align?: TextAlign;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The maximum amount of lines the text can occupy before being truncated with "...".
|
|
48
|
+
* Uses predefined string values that correspond to a doubling scale for the amount of lines.
|
|
49
|
+
*/
|
|
50
|
+
readonly maxLines?: TruncateLength;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Allow text to be resized based on user's device display scale
|
|
54
|
+
*/
|
|
55
|
+
readonly allowFontScaling?: boolean;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const maxScaledFontSize: Record<HeadingLevel, number> = {
|
|
59
|
+
subHeading: tokens["typography--fontSize-base"] * 1.375,
|
|
60
|
+
heading: tokens["typography--fontSize-base"] * 1.5,
|
|
61
|
+
subtitle: tokens["typography--fontSize-base"] * 1.5,
|
|
62
|
+
title: tokens["typography--fontSize-base"] * 2.125,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export function Heading<T extends HeadingLevel = "heading">({
|
|
66
|
+
children,
|
|
67
|
+
level,
|
|
68
|
+
variation = "heading",
|
|
69
|
+
reverseTheme = false,
|
|
70
|
+
align,
|
|
71
|
+
maxLines = "unlimited",
|
|
72
|
+
allowFontScaling = true,
|
|
73
|
+
selectable,
|
|
74
|
+
}: HeadingProps<T>): JSX.Element {
|
|
75
|
+
const headingStyle = getHeadingStyle(level, variation);
|
|
76
|
+
const accessibilityRole = "header";
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<Typography
|
|
80
|
+
{...{
|
|
81
|
+
...headingStyle,
|
|
82
|
+
accessibilityRole,
|
|
83
|
+
reverseTheme,
|
|
84
|
+
align,
|
|
85
|
+
maxLines,
|
|
86
|
+
allowFontScaling,
|
|
87
|
+
}}
|
|
88
|
+
maxFontScaleSize={maxScaledFontSize[level as HeadingLevel]}
|
|
89
|
+
selectable={selectable}
|
|
90
|
+
>
|
|
91
|
+
{children}
|
|
92
|
+
</Typography>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function getHeadingStyle(
|
|
97
|
+
level: HeadingLevel = "heading",
|
|
98
|
+
variation: HeadingColor,
|
|
99
|
+
): HeadingStyle {
|
|
100
|
+
const headingLevelToStyle: Record<HeadingLevel, HeadingStyle> = {
|
|
101
|
+
title: {
|
|
102
|
+
fontFamily: "display",
|
|
103
|
+
fontWeight: "black",
|
|
104
|
+
size: "jumbo",
|
|
105
|
+
lineHeight: "jumbo",
|
|
106
|
+
color: variation,
|
|
107
|
+
},
|
|
108
|
+
subtitle: {
|
|
109
|
+
fontFamily: "display",
|
|
110
|
+
fontWeight: "extraBold",
|
|
111
|
+
size: "largest",
|
|
112
|
+
lineHeight: "largest",
|
|
113
|
+
color: variation,
|
|
114
|
+
},
|
|
115
|
+
heading: {
|
|
116
|
+
fontFamily: "display",
|
|
117
|
+
fontWeight: "extraBold",
|
|
118
|
+
size: "larger",
|
|
119
|
+
lineHeight: "large",
|
|
120
|
+
color: variation,
|
|
121
|
+
},
|
|
122
|
+
subHeading: {
|
|
123
|
+
fontFamily: "base",
|
|
124
|
+
fontWeight: "semiBold",
|
|
125
|
+
size: "default",
|
|
126
|
+
lineHeight: "base",
|
|
127
|
+
color: variation,
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
return headingLevelToStyle[level];
|
|
132
|
+
}
|