@jobber/components-native 0.36.0 → 0.37.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/Disclosure/Disclosure.js +50 -0
- package/dist/src/Disclosure/Disclosure.style.js +21 -0
- package/dist/src/Disclosure/constants.js +1 -0
- package/dist/src/Disclosure/index.js +1 -0
- package/dist/src/index.js +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/src/Disclosure/Disclosure.d.ts +35 -0
- package/dist/types/src/Disclosure/Disclosure.style.d.ts +19 -0
- package/dist/types/src/Disclosure/constants.d.ts +1 -0
- package/dist/types/src/Disclosure/index.d.ts +1 -0
- package/dist/types/src/index.d.ts +1 -0
- package/package.json +2 -2
- package/src/Disclosure/Disclosure.style.ts +22 -0
- package/src/Disclosure/Disclosure.test.tsx +71 -0
- package/src/Disclosure/Disclosure.tsx +162 -0
- package/src/Disclosure/__snapshots__/Disclosure.test.tsx.snap +488 -0
- package/src/Disclosure/constants.ts +1 -0
- package/src/Disclosure/index.ts +1 -0
- package/src/index.ts +1 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
interface DisclosureProps {
|
|
3
|
+
/**
|
|
4
|
+
* Specifies the main content of the disclosure component.
|
|
5
|
+
* It can be any React Node - simple text, JSX, or a complex React component.
|
|
6
|
+
*/
|
|
7
|
+
readonly content: React.ReactNode;
|
|
8
|
+
/**
|
|
9
|
+
* Defines the header of the disclosure component.
|
|
10
|
+
* Similar to `content`, it can be any React Node.
|
|
11
|
+
*/
|
|
12
|
+
readonly header: React.ReactNode;
|
|
13
|
+
/**
|
|
14
|
+
* A boolean that determines whether the disclosure component is in an open or closed state.
|
|
15
|
+
* If `open` is true, the disclosure is in an open state; if false, it's closed.
|
|
16
|
+
*/
|
|
17
|
+
readonly open: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* A boolean that indicates whether the disclosure component is empty or not.
|
|
20
|
+
* If `isEmpty` is `true`, there is no content in the disclosure; if false, there is some content.
|
|
21
|
+
*/
|
|
22
|
+
readonly isEmpty: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* An optional property that determines the duration of the opening and closing animation of the disclosure component.
|
|
25
|
+
* It's defined in milliseconds.
|
|
26
|
+
* @default tokens["timing-slowest"]
|
|
27
|
+
*/
|
|
28
|
+
readonly animationDuration?: number;
|
|
29
|
+
/**
|
|
30
|
+
* A function that is called whenever the disclosure component is toggled between its open and closed states.
|
|
31
|
+
*/
|
|
32
|
+
onToggle(): void;
|
|
33
|
+
}
|
|
34
|
+
export declare function Disclosure({ content, header, open, onToggle, isEmpty, animationDuration, }: DisclosureProps): JSX.Element;
|
|
35
|
+
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export declare const styles: {
|
|
2
|
+
container: {
|
|
3
|
+
width: string;
|
|
4
|
+
};
|
|
5
|
+
headerContainer: {
|
|
6
|
+
flexDirection: "row";
|
|
7
|
+
alignItems: "flex-start";
|
|
8
|
+
justifyContent: "space-between";
|
|
9
|
+
};
|
|
10
|
+
countColumn: {
|
|
11
|
+
paddingRight: number;
|
|
12
|
+
};
|
|
13
|
+
titleContainer: {
|
|
14
|
+
flexDirection: "row";
|
|
15
|
+
};
|
|
16
|
+
contentContainer: {
|
|
17
|
+
paddingTop: number;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const EASE_CUBIC_IN_OUT: readonly [0.645, 0.045, 0.355, 1];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Disclosure } from "./Disclosure";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jobber/components-native",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.37.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "React Native implementation of Atlantis",
|
|
6
6
|
"repository": {
|
|
@@ -75,5 +75,5 @@
|
|
|
75
75
|
"react-native": ">=0.69.2",
|
|
76
76
|
"react-native-modal-datetime-picker": " >=13.0.0"
|
|
77
77
|
},
|
|
78
|
-
"gitHead": "
|
|
78
|
+
"gitHead": "ed441eb97b781488d538135bd8a747f4281d7c47"
|
|
79
79
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
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
|
+
},
|
|
8
|
+
headerContainer: {
|
|
9
|
+
flexDirection: "row",
|
|
10
|
+
alignItems: "flex-start",
|
|
11
|
+
justifyContent: "space-between",
|
|
12
|
+
},
|
|
13
|
+
countColumn: {
|
|
14
|
+
paddingRight: tokens["space-base"],
|
|
15
|
+
},
|
|
16
|
+
titleContainer: {
|
|
17
|
+
flexDirection: "row",
|
|
18
|
+
},
|
|
19
|
+
contentContainer: {
|
|
20
|
+
paddingTop: tokens["space-small"],
|
|
21
|
+
},
|
|
22
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { fireEvent, render } from "@testing-library/react-native";
|
|
3
|
+
import { ReactTestInstance } from "react-test-renderer";
|
|
4
|
+
import { Disclosure } from ".";
|
|
5
|
+
import { Text } from "../Text";
|
|
6
|
+
|
|
7
|
+
jest.mock("react-native-svg", () => {
|
|
8
|
+
return {
|
|
9
|
+
__esModule: true,
|
|
10
|
+
...jest.requireActual("react-native-svg"),
|
|
11
|
+
Path: "Path",
|
|
12
|
+
default: "SVGMock",
|
|
13
|
+
};
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
function fireLayoutEvent(disclosureContent: ReactTestInstance) {
|
|
17
|
+
fireEvent(disclosureContent, "onLayout", {
|
|
18
|
+
nativeEvent: {
|
|
19
|
+
layout: {
|
|
20
|
+
height: 100,
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
it("renders a Disclosure with a header and a content when open is true", () => {
|
|
27
|
+
const disclosure = render(
|
|
28
|
+
<Disclosure
|
|
29
|
+
header={<Text>This is the header</Text>}
|
|
30
|
+
content={<Text>This is the content</Text>}
|
|
31
|
+
open={true}
|
|
32
|
+
isEmpty={false}
|
|
33
|
+
onToggle={() => {
|
|
34
|
+
return;
|
|
35
|
+
}}
|
|
36
|
+
/>,
|
|
37
|
+
);
|
|
38
|
+
fireLayoutEvent(disclosure.getByTestId("content"));
|
|
39
|
+
expect(disclosure).toMatchSnapshot();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("renders a Disclosure with a header and with a content of size 0 when closed is false", () => {
|
|
43
|
+
const disclosure = render(
|
|
44
|
+
<Disclosure
|
|
45
|
+
header={<Text>This is the header</Text>}
|
|
46
|
+
content={<Text>This is the content</Text>}
|
|
47
|
+
open={false}
|
|
48
|
+
isEmpty={false}
|
|
49
|
+
onToggle={() => {
|
|
50
|
+
return;
|
|
51
|
+
}}
|
|
52
|
+
/>,
|
|
53
|
+
);
|
|
54
|
+
fireLayoutEvent(disclosure.getByTestId("content"));
|
|
55
|
+
expect(disclosure).toMatchSnapshot();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("should not render the caret when the Disclosure is empty", () => {
|
|
59
|
+
const disclosure = render(
|
|
60
|
+
<Disclosure
|
|
61
|
+
header={<Text>This is the header</Text>}
|
|
62
|
+
content={<Text>This is the content</Text>}
|
|
63
|
+
open={false}
|
|
64
|
+
isEmpty={true}
|
|
65
|
+
onToggle={() => {
|
|
66
|
+
return;
|
|
67
|
+
}}
|
|
68
|
+
/>,
|
|
69
|
+
);
|
|
70
|
+
expect(disclosure).toMatchSnapshot();
|
|
71
|
+
});
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
LayoutChangeEvent,
|
|
4
|
+
ScrollView,
|
|
5
|
+
TouchableOpacity,
|
|
6
|
+
View,
|
|
7
|
+
} from "react-native";
|
|
8
|
+
import Reanimated, {
|
|
9
|
+
Easing,
|
|
10
|
+
useAnimatedStyle,
|
|
11
|
+
useSharedValue,
|
|
12
|
+
withTiming,
|
|
13
|
+
} from "react-native-reanimated";
|
|
14
|
+
import { EASE_CUBIC_IN_OUT } from "./constants";
|
|
15
|
+
import { styles } from "./Disclosure.style";
|
|
16
|
+
import { tokens } from "../utils/design";
|
|
17
|
+
import { Icon } from "../Icon";
|
|
18
|
+
|
|
19
|
+
const ReanimatedView = Reanimated.createAnimatedComponent(View);
|
|
20
|
+
const ReanimatedScrollView = Reanimated.createAnimatedComponent(ScrollView);
|
|
21
|
+
|
|
22
|
+
interface DisclosureProps {
|
|
23
|
+
/**
|
|
24
|
+
* Specifies the main content of the disclosure component.
|
|
25
|
+
* It can be any React Node - simple text, JSX, or a complex React component.
|
|
26
|
+
*/
|
|
27
|
+
readonly content: React.ReactNode;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Defines the header of the disclosure component.
|
|
31
|
+
* Similar to `content`, it can be any React Node.
|
|
32
|
+
*/
|
|
33
|
+
readonly header: React.ReactNode;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* A boolean that determines whether the disclosure component is in an open or closed state.
|
|
37
|
+
* If `open` is true, the disclosure is in an open state; if false, it's closed.
|
|
38
|
+
*/
|
|
39
|
+
readonly open: boolean;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* A boolean that indicates whether the disclosure component is empty or not.
|
|
43
|
+
* If `isEmpty` is `true`, there is no content in the disclosure; if false, there is some content.
|
|
44
|
+
*/
|
|
45
|
+
readonly isEmpty: boolean;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* An optional property that determines the duration of the opening and closing animation of the disclosure component.
|
|
49
|
+
* It's defined in milliseconds.
|
|
50
|
+
* @default tokens["timing-slowest"]
|
|
51
|
+
*/
|
|
52
|
+
readonly animationDuration?: number;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* A function that is called whenever the disclosure component is toggled between its open and closed states.
|
|
56
|
+
*/
|
|
57
|
+
onToggle(): void;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function Disclosure({
|
|
61
|
+
content,
|
|
62
|
+
header,
|
|
63
|
+
open,
|
|
64
|
+
onToggle,
|
|
65
|
+
isEmpty,
|
|
66
|
+
animationDuration = tokens["timing-slowest"],
|
|
67
|
+
}: DisclosureProps): JSX.Element {
|
|
68
|
+
return (
|
|
69
|
+
<View style={styles.container}>
|
|
70
|
+
<DisclosureHeader
|
|
71
|
+
{...{ header, onToggle, isEmpty, open, animationDuration }}
|
|
72
|
+
/>
|
|
73
|
+
<DisclosureContent {...{ content, open, animationDuration }} />
|
|
74
|
+
</View>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
type DisclosureHeaderProps = Pick<
|
|
79
|
+
DisclosureProps,
|
|
80
|
+
"header" | "onToggle" | "isEmpty" | "open" | "animationDuration"
|
|
81
|
+
>;
|
|
82
|
+
|
|
83
|
+
function DisclosureHeader({
|
|
84
|
+
header,
|
|
85
|
+
onToggle,
|
|
86
|
+
isEmpty,
|
|
87
|
+
open,
|
|
88
|
+
animationDuration,
|
|
89
|
+
}: DisclosureHeaderProps) {
|
|
90
|
+
const rotateZ = useSharedValue(0);
|
|
91
|
+
|
|
92
|
+
rotateZ.value = withTiming(open ? 0 : -180, {
|
|
93
|
+
easing: Easing.bezier(...EASE_CUBIC_IN_OUT),
|
|
94
|
+
duration: animationDuration,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const animatedStyle = useAnimatedStyle(() => {
|
|
98
|
+
return {
|
|
99
|
+
transform: [{ rotateZ: `${rotateZ.value}deg` }],
|
|
100
|
+
};
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<TouchableOpacity
|
|
105
|
+
activeOpacity={tokens["opacity-pressed"]}
|
|
106
|
+
onPress={onToggle}
|
|
107
|
+
disabled={isEmpty}
|
|
108
|
+
>
|
|
109
|
+
<View style={styles.headerContainer}>
|
|
110
|
+
{header}
|
|
111
|
+
{!isEmpty && (
|
|
112
|
+
<ReanimatedView style={[animatedStyle]}>
|
|
113
|
+
<Icon name={"arrowUp"} color="grey" />
|
|
114
|
+
</ReanimatedView>
|
|
115
|
+
)}
|
|
116
|
+
</View>
|
|
117
|
+
</TouchableOpacity>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
type DisclosureContentProps = Pick<
|
|
122
|
+
DisclosureProps,
|
|
123
|
+
"content" | "open" | "animationDuration"
|
|
124
|
+
>;
|
|
125
|
+
|
|
126
|
+
function DisclosureContent({
|
|
127
|
+
content,
|
|
128
|
+
open,
|
|
129
|
+
animationDuration,
|
|
130
|
+
}: DisclosureContentProps) {
|
|
131
|
+
const [maxHeight, setMaxHeight] = useState(0);
|
|
132
|
+
const height = useSharedValue(0);
|
|
133
|
+
|
|
134
|
+
const onContentLayoutChange = (event: LayoutChangeEvent) => {
|
|
135
|
+
const newHeight = event.nativeEvent.layout.height;
|
|
136
|
+
setMaxHeight(newHeight);
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
height.value = withTiming(open ? maxHeight : 0, {
|
|
140
|
+
duration: animationDuration,
|
|
141
|
+
easing: Easing.bezier(...EASE_CUBIC_IN_OUT),
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const animatedStyle = useAnimatedStyle(() => {
|
|
145
|
+
return {
|
|
146
|
+
height: height.value,
|
|
147
|
+
};
|
|
148
|
+
}, []);
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
<ReanimatedScrollView
|
|
152
|
+
scrollEnabled={false}
|
|
153
|
+
showsHorizontalScrollIndicator={false}
|
|
154
|
+
showsVerticalScrollIndicator={false}
|
|
155
|
+
style={[styles.contentContainer, animatedStyle]}
|
|
156
|
+
>
|
|
157
|
+
<View testID={"content"} onLayout={onContentLayoutChange}>
|
|
158
|
+
{content}
|
|
159
|
+
</View>
|
|
160
|
+
</ReanimatedScrollView>
|
|
161
|
+
);
|
|
162
|
+
}
|