@jobber/components-native 0.22.0 → 0.23.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,16 @@
1
+ /// <reference types="react" />
2
+ import { XOR } from "ts-xor";
3
+ import { BaseSwitchProps } from "./components/BaseSwitch";
4
+ interface WithLabelProps extends BaseSwitchProps {
5
+ /**
6
+ * Optional label to display in front of the switch
7
+ */
8
+ readonly label: string;
9
+ /**
10
+ * Optional descriptive text to display under the switch
11
+ */
12
+ readonly description?: string;
13
+ }
14
+ type SwitchProps = XOR<BaseSwitchProps, WithLabelProps>;
15
+ export declare function Switch(props: SwitchProps): JSX.Element;
16
+ export {};
@@ -0,0 +1,18 @@
1
+ export declare const styles: {
2
+ container: {
3
+ marginTop: number;
4
+ marginBottom: number;
5
+ };
6
+ row: {
7
+ flexDirection: "row";
8
+ width: string;
9
+ };
10
+ label: {
11
+ flex: number;
12
+ justifyContent: "center";
13
+ marginRight: number;
14
+ };
15
+ description: {
16
+ marginTop: number;
17
+ };
18
+ };
@@ -0,0 +1,28 @@
1
+ /// <reference types="react" />
2
+ export interface BaseSwitchProps {
3
+ /**
4
+ * Value of the switch
5
+ */
6
+ readonly value?: boolean;
7
+ /**
8
+ * Default value of the switch when uncontrolled
9
+ */
10
+ readonly defaultValue?: boolean;
11
+ /**
12
+ * Callback to handle value changes
13
+ */
14
+ onValueChange?(val: boolean): void;
15
+ /**
16
+ * When true, the switch cannot be toggled
17
+ */
18
+ readonly disabled?: boolean;
19
+ /**
20
+ * Accessibility label for this switch
21
+ */
22
+ readonly accessibilityLabel?: string;
23
+ /**
24
+ * Name of the input.
25
+ */
26
+ readonly name?: string;
27
+ }
28
+ export declare function BaseSwitch({ value, defaultValue, onValueChange, disabled, accessibilityLabel, name, }: BaseSwitchProps): JSX.Element;
@@ -0,0 +1,2 @@
1
+ export { BaseSwitch } from "./BaseSwitch";
2
+ export type { BaseSwitchProps } from "./BaseSwitch";
@@ -0,0 +1 @@
1
+ export { Switch } from "./Switch";
@@ -18,5 +18,6 @@ export * from "./InputPressable";
18
18
  export * from "./InputText";
19
19
  export * from "./ProgressBar";
20
20
  export * from "./StatusLabel";
21
+ export * from "./Switch";
21
22
  export * from "./Text";
22
23
  export * from "./Typography";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jobber/components-native",
3
- "version": "0.22.0",
3
+ "version": "0.23.0",
4
4
  "license": "MIT",
5
5
  "main": "dist/src/index.js",
6
6
  "module": "dist/src/index.js",
@@ -51,5 +51,5 @@
51
51
  "react": "^18",
52
52
  "react-native": ">=0.69.2"
53
53
  },
54
- "gitHead": "4924f4190c152e4d11b9f52adcd8dd91a727ae16"
54
+ "gitHead": "4a2124e4c8284dd7377668648eb94d72e7cb118d"
55
55
  }
@@ -0,0 +1,21 @@
1
+ import { StyleSheet } from "react-native";
2
+ import { tokens } from "../utils/design";
3
+
4
+ export const styles = StyleSheet.create({
5
+ container: {
6
+ marginTop: tokens["space-base"],
7
+ marginBottom: tokens["space-base"],
8
+ },
9
+ row: {
10
+ flexDirection: "row",
11
+ width: "100%",
12
+ },
13
+ label: {
14
+ flex: 1,
15
+ justifyContent: "center",
16
+ marginRight: tokens["space-small"],
17
+ },
18
+ description: {
19
+ marginTop: tokens["space-smaller"],
20
+ },
21
+ });
@@ -0,0 +1,98 @@
1
+ import React from "react";
2
+ import { fireEvent, render } from "@testing-library/react-native";
3
+ import { ViewStyle } from "react-native";
4
+ import { Switch } from "./Switch";
5
+
6
+ it("renders a labeled Switch with value true", () => {
7
+ const { getByLabelText } = render(
8
+ <Switch value={true} label="true switch" />,
9
+ );
10
+ const valueProp = getByLabelText("true switch").props.value;
11
+ expect(valueProp).toEqual(true);
12
+ });
13
+
14
+ it("renders a labeled Switch with value false", () => {
15
+ const { getByLabelText } = render(
16
+ <Switch value={false} label="false switch" />,
17
+ );
18
+ const valueProp = getByLabelText("false switch").props.value;
19
+ expect(valueProp).toEqual(false);
20
+ });
21
+
22
+ it("renders an unlabeled Switch with value true", () => {
23
+ const { getByRole } = render(<Switch value={true} />);
24
+ const valueProp = getByRole("switch").props.value;
25
+ expect(valueProp).toEqual(true);
26
+ });
27
+
28
+ it("renders an unlabeled Switch with value false", () => {
29
+ const { getByRole } = render(<Switch value={false} />);
30
+ const valueProp = getByRole("switch").props.value;
31
+ expect(valueProp).toEqual(false);
32
+ });
33
+
34
+ describe("description", () => {
35
+ it("renders a Switch with label and description", () => {
36
+ const { getByText } = render(
37
+ <Switch label="Label" description="Description" />,
38
+ );
39
+
40
+ expect(getByText("Label")).toBeDefined();
41
+ expect(getByText("Description")).toBeDefined();
42
+ });
43
+
44
+ it("matches the width of the label and description", () => {
45
+ const { getByTestId } = render(
46
+ <Switch label="Label" description="Description" />,
47
+ );
48
+ const labelView = getByTestId("switch-label-view");
49
+ const descriptionView = getByTestId("switch-description-view");
50
+
51
+ fireEvent(labelView, "onLayout", {
52
+ nativeEvent: {
53
+ layout: {
54
+ width: 123,
55
+ },
56
+ },
57
+ });
58
+
59
+ const flattenedStyle = descriptionView.props.style.reduce(
60
+ (style: ViewStyle, additionalStyles: ViewStyle) => ({
61
+ ...style,
62
+ ...additionalStyles,
63
+ }),
64
+ {},
65
+ );
66
+ expect(flattenedStyle).toEqual(
67
+ expect.objectContaining({
68
+ maxWidth: 123,
69
+ }),
70
+ );
71
+ });
72
+ });
73
+
74
+ describe("accessibilityLabel", () => {
75
+ it("uses accessibilityLabel if specified", () => {
76
+ const { getByLabelText } = render(
77
+ <Switch
78
+ value={true}
79
+ label="label"
80
+ accessibilityLabel="accessibilityLabel"
81
+ />,
82
+ );
83
+
84
+ expect(getByLabelText("accessibilityLabel")).toBeTruthy();
85
+ });
86
+
87
+ it("uses placeholder if unspecified", () => {
88
+ const { getByLabelText } = render(<Switch value={true} label="label" />);
89
+
90
+ expect(getByLabelText("label")).toBeTruthy();
91
+ });
92
+
93
+ it("unavailable if unspecified", () => {
94
+ const { queryByLabelText } = render(<Switch value={true} />);
95
+
96
+ expect(queryByLabelText("label")).not.toBeTruthy();
97
+ });
98
+ });
@@ -0,0 +1,58 @@
1
+ import React, { useState } from "react";
2
+ import { View } from "react-native";
3
+ import { XOR } from "ts-xor";
4
+ import { BaseSwitch, BaseSwitchProps } from "./components/BaseSwitch";
5
+ import { styles } from "./Switch.styles";
6
+ import { Text } from "../Text";
7
+
8
+ interface WithLabelProps extends BaseSwitchProps {
9
+ /**
10
+ * Optional label to display in front of the switch
11
+ */
12
+ readonly label: string;
13
+
14
+ /**
15
+ * Optional descriptive text to display under the switch
16
+ */
17
+ readonly description?: string;
18
+ }
19
+
20
+ type SwitchProps = XOR<BaseSwitchProps, WithLabelProps>;
21
+
22
+ export function Switch(props: SwitchProps): JSX.Element {
23
+ const switchProps: SwitchProps = {
24
+ ...props,
25
+ accessibilityLabel: props.accessibilityLabel || props.label,
26
+ };
27
+
28
+ const [labelWidth, setLabelWidth] = useState<number | undefined>();
29
+
30
+ return (
31
+ <View style={styles.container}>
32
+ <View style={styles.row}>
33
+ {props.label && (
34
+ <View
35
+ style={styles.label}
36
+ onLayout={event => setLabelWidth(event.nativeEvent.layout.width)}
37
+ testID="switch-label-view"
38
+ >
39
+ <Text variation={props.disabled ? "disabled" : "base"}>
40
+ {props.label}
41
+ </Text>
42
+ </View>
43
+ )}
44
+ <BaseSwitch {...switchProps} />
45
+ </View>
46
+ {props.description && (
47
+ <View
48
+ style={[styles.description, { maxWidth: labelWidth }]}
49
+ testID="switch-description-view"
50
+ >
51
+ <Text level="textSupporting" variation="subdued">
52
+ {props.description}
53
+ </Text>
54
+ </View>
55
+ )}
56
+ </View>
57
+ );
58
+ }
@@ -0,0 +1,62 @@
1
+ import React from "react";
2
+ import { fireEvent, render } from "@testing-library/react-native";
3
+ import { BaseSwitch } from "./BaseSwitch";
4
+
5
+ it("renders a Switch with value true", () => {
6
+ const tree = render(<BaseSwitch value={true} />).toJSON();
7
+ expect(tree).toMatchSnapshot();
8
+ });
9
+
10
+ it("renders a Switch with defaultValue true", () => {
11
+ const tree = render(<BaseSwitch defaultValue={true} />).toJSON();
12
+ expect(tree).toMatchSnapshot();
13
+ });
14
+
15
+ it("renders a Switch with value false", () => {
16
+ const tree = render(<BaseSwitch value={false} />).toJSON();
17
+ expect(tree).toMatchSnapshot();
18
+ });
19
+
20
+ it("renders a Switch with defaultValue false", () => {
21
+ const tree = render(<BaseSwitch defaultValue={false} />).toJSON();
22
+ expect(tree).toMatchSnapshot();
23
+ });
24
+
25
+ it("renders a disabled Switch with value true", () => {
26
+ const tree = render(<BaseSwitch value={true} disabled={true} />).toJSON();
27
+ expect(tree).toMatchSnapshot();
28
+ });
29
+
30
+ it("renders a disabled Switch with value false", () => {
31
+ const tree = render(<BaseSwitch value={false} disabled={true} />).toJSON();
32
+ expect(tree).toMatchSnapshot();
33
+ });
34
+
35
+ it("invokes the valueChange callback", () => {
36
+ const valueChangedCallback = jest.fn();
37
+ const tree = render(
38
+ <BaseSwitch
39
+ value={false}
40
+ onValueChange={valueChangedCallback}
41
+ accessibilityLabel={"test switch"}
42
+ />,
43
+ );
44
+ fireEvent(tree.getByLabelText("test switch"), "valueChange", true);
45
+ expect(valueChangedCallback).toHaveBeenCalledWith(true);
46
+ fireEvent(tree.getByLabelText("test switch"), "valueChange", false);
47
+ expect(valueChangedCallback).toHaveBeenCalledWith(false);
48
+ });
49
+
50
+ it("doesn't invoke the valueChange callback if disabled", () => {
51
+ const valueChangedCallback = jest.fn();
52
+ const tree = render(
53
+ <BaseSwitch
54
+ value={false}
55
+ disabled={true}
56
+ onValueChange={valueChangedCallback}
57
+ accessibilityLabel={"test switch"}
58
+ />,
59
+ );
60
+ fireEvent(tree.getByLabelText("test switch"), "valueChange", true);
61
+ expect(valueChangedCallback).not.toHaveBeenCalled();
62
+ });
@@ -0,0 +1,107 @@
1
+ import React from "react";
2
+ import { Platform } from "react-native";
3
+ import { Switch } from "react-native-gesture-handler";
4
+ import { useFormController } from "../../../hooks";
5
+ import { tokens } from "../../../utils/design";
6
+
7
+ export interface BaseSwitchProps {
8
+ /**
9
+ * Value of the switch
10
+ */
11
+ readonly value?: boolean;
12
+
13
+ /**
14
+ * Default value of the switch when uncontrolled
15
+ */
16
+ readonly defaultValue?: boolean;
17
+
18
+ /**
19
+ * Callback to handle value changes
20
+ */
21
+ onValueChange?(val: boolean): void;
22
+
23
+ /**
24
+ * When true, the switch cannot be toggled
25
+ */
26
+ readonly disabled?: boolean;
27
+
28
+ /**
29
+ * Accessibility label for this switch
30
+ */
31
+ readonly accessibilityLabel?: string;
32
+
33
+ /**
34
+ * Name of the input.
35
+ */
36
+ readonly name?: string;
37
+ }
38
+
39
+ export function BaseSwitch({
40
+ value,
41
+ defaultValue,
42
+ onValueChange,
43
+ disabled = false,
44
+ accessibilityLabel,
45
+ name,
46
+ }: BaseSwitchProps): JSX.Element {
47
+ const { field } = useFormController({
48
+ name,
49
+ value: value ?? defaultValue,
50
+ });
51
+
52
+ const internalValue = value ?? field.value;
53
+
54
+ function getThumbColor() {
55
+ if (Platform.OS === "android") {
56
+ if (disabled) {
57
+ return tokens["color-disabled"];
58
+ } else if (internalValue) {
59
+ return tokens["color-interactive"];
60
+ } else {
61
+ return tokens["color-surface--background"];
62
+ }
63
+ }
64
+ return undefined; //use default iOS
65
+ }
66
+
67
+ function getTrackColors() {
68
+ if (Platform.OS === "android") {
69
+ return {
70
+ true: disabled
71
+ ? tokens["color-disabled--secondary"]
72
+ : tokens["color-green--lighter"],
73
+ false: disabled
74
+ ? tokens["color-disabled--secondary"]
75
+ : tokens["color-disabled"],
76
+ };
77
+ } else {
78
+ //iOS
79
+ return {
80
+ true: tokens["color-interactive"],
81
+ false: tokens["color-surface--background"],
82
+ };
83
+ }
84
+ }
85
+
86
+ return (
87
+ <Switch
88
+ value={internalValue}
89
+ onValueChange={(val: boolean) => {
90
+ if (!disabled) {
91
+ onValueChange?.(val);
92
+ field.onChange(val);
93
+ }
94
+ }}
95
+ disabled={disabled}
96
+ thumbColor={getThumbColor()}
97
+ trackColor={getTrackColors()}
98
+ ios_backgroundColor={tokens["color-surface--background"]}
99
+ accessibilityLabel={accessibilityLabel}
100
+ accessibilityRole={"switch"}
101
+ accessibilityState={{
102
+ disabled: disabled,
103
+ checked: internalValue,
104
+ }}
105
+ />
106
+ );
107
+ }
@@ -0,0 +1,217 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`renders a Switch with defaultValue false 1`] = `
4
+ <RCTSwitch
5
+ accessibilityRole="switch"
6
+ accessibilityState={
7
+ {
8
+ "checked": false,
9
+ "disabled": false,
10
+ }
11
+ }
12
+ collapsable={false}
13
+ disabled={false}
14
+ handlerTag={4}
15
+ handlerType="NativeViewGestureHandler"
16
+ onChange={[Function]}
17
+ onGestureHandlerEvent={[Function]}
18
+ onGestureHandlerStateChange={[Function]}
19
+ onResponderTerminationRequest={[Function]}
20
+ onStartShouldSetResponder={[Function]}
21
+ onTintColor="rgb(125, 176, 14)"
22
+ style={
23
+ [
24
+ {
25
+ "height": 31,
26
+ "width": 51,
27
+ },
28
+ {
29
+ "backgroundColor": "rgb(244, 244, 244)",
30
+ "borderRadius": 16,
31
+ },
32
+ ]
33
+ }
34
+ tintColor="rgb(244, 244, 244)"
35
+ value={false}
36
+ />
37
+ `;
38
+
39
+ exports[`renders a Switch with defaultValue true 1`] = `
40
+ <RCTSwitch
41
+ accessibilityRole="switch"
42
+ accessibilityState={
43
+ {
44
+ "checked": true,
45
+ "disabled": false,
46
+ }
47
+ }
48
+ collapsable={false}
49
+ disabled={false}
50
+ handlerTag={2}
51
+ handlerType="NativeViewGestureHandler"
52
+ onChange={[Function]}
53
+ onGestureHandlerEvent={[Function]}
54
+ onGestureHandlerStateChange={[Function]}
55
+ onResponderTerminationRequest={[Function]}
56
+ onStartShouldSetResponder={[Function]}
57
+ onTintColor="rgb(125, 176, 14)"
58
+ style={
59
+ [
60
+ {
61
+ "height": 31,
62
+ "width": 51,
63
+ },
64
+ {
65
+ "backgroundColor": "rgb(244, 244, 244)",
66
+ "borderRadius": 16,
67
+ },
68
+ ]
69
+ }
70
+ tintColor="rgb(244, 244, 244)"
71
+ value={true}
72
+ />
73
+ `;
74
+
75
+ exports[`renders a Switch with value false 1`] = `
76
+ <RCTSwitch
77
+ accessibilityRole="switch"
78
+ accessibilityState={
79
+ {
80
+ "checked": false,
81
+ "disabled": false,
82
+ }
83
+ }
84
+ collapsable={false}
85
+ disabled={false}
86
+ handlerTag={3}
87
+ handlerType="NativeViewGestureHandler"
88
+ onChange={[Function]}
89
+ onGestureHandlerEvent={[Function]}
90
+ onGestureHandlerStateChange={[Function]}
91
+ onResponderTerminationRequest={[Function]}
92
+ onStartShouldSetResponder={[Function]}
93
+ onTintColor="rgb(125, 176, 14)"
94
+ style={
95
+ [
96
+ {
97
+ "height": 31,
98
+ "width": 51,
99
+ },
100
+ {
101
+ "backgroundColor": "rgb(244, 244, 244)",
102
+ "borderRadius": 16,
103
+ },
104
+ ]
105
+ }
106
+ tintColor="rgb(244, 244, 244)"
107
+ value={false}
108
+ />
109
+ `;
110
+
111
+ exports[`renders a Switch with value true 1`] = `
112
+ <RCTSwitch
113
+ accessibilityRole="switch"
114
+ accessibilityState={
115
+ {
116
+ "checked": true,
117
+ "disabled": false,
118
+ }
119
+ }
120
+ collapsable={false}
121
+ disabled={false}
122
+ handlerTag={1}
123
+ handlerType="NativeViewGestureHandler"
124
+ onChange={[Function]}
125
+ onGestureHandlerEvent={[Function]}
126
+ onGestureHandlerStateChange={[Function]}
127
+ onResponderTerminationRequest={[Function]}
128
+ onStartShouldSetResponder={[Function]}
129
+ onTintColor="rgb(125, 176, 14)"
130
+ style={
131
+ [
132
+ {
133
+ "height": 31,
134
+ "width": 51,
135
+ },
136
+ {
137
+ "backgroundColor": "rgb(244, 244, 244)",
138
+ "borderRadius": 16,
139
+ },
140
+ ]
141
+ }
142
+ tintColor="rgb(244, 244, 244)"
143
+ value={true}
144
+ />
145
+ `;
146
+
147
+ exports[`renders a disabled Switch with value false 1`] = `
148
+ <RCTSwitch
149
+ accessibilityRole="switch"
150
+ accessibilityState={
151
+ {
152
+ "checked": false,
153
+ "disabled": true,
154
+ }
155
+ }
156
+ collapsable={false}
157
+ disabled={true}
158
+ handlerTag={6}
159
+ handlerType="NativeViewGestureHandler"
160
+ onChange={[Function]}
161
+ onGestureHandlerEvent={[Function]}
162
+ onGestureHandlerStateChange={[Function]}
163
+ onResponderTerminationRequest={[Function]}
164
+ onStartShouldSetResponder={[Function]}
165
+ onTintColor="rgb(125, 176, 14)"
166
+ style={
167
+ [
168
+ {
169
+ "height": 31,
170
+ "width": 51,
171
+ },
172
+ {
173
+ "backgroundColor": "rgb(244, 244, 244)",
174
+ "borderRadius": 16,
175
+ },
176
+ ]
177
+ }
178
+ tintColor="rgb(244, 244, 244)"
179
+ value={false}
180
+ />
181
+ `;
182
+
183
+ exports[`renders a disabled Switch with value true 1`] = `
184
+ <RCTSwitch
185
+ accessibilityRole="switch"
186
+ accessibilityState={
187
+ {
188
+ "checked": true,
189
+ "disabled": true,
190
+ }
191
+ }
192
+ collapsable={false}
193
+ disabled={true}
194
+ handlerTag={5}
195
+ handlerType="NativeViewGestureHandler"
196
+ onChange={[Function]}
197
+ onGestureHandlerEvent={[Function]}
198
+ onGestureHandlerStateChange={[Function]}
199
+ onResponderTerminationRequest={[Function]}
200
+ onStartShouldSetResponder={[Function]}
201
+ onTintColor="rgb(125, 176, 14)"
202
+ style={
203
+ [
204
+ {
205
+ "height": 31,
206
+ "width": 51,
207
+ },
208
+ {
209
+ "backgroundColor": "rgb(244, 244, 244)",
210
+ "borderRadius": 16,
211
+ },
212
+ ]
213
+ }
214
+ tintColor="rgb(244, 244, 244)"
215
+ value={true}
216
+ />
217
+ `;
@@ -0,0 +1,2 @@
1
+ export { BaseSwitch } from "./BaseSwitch";
2
+ export type { BaseSwitchProps } from "./BaseSwitch";
@@ -0,0 +1 @@
1
+ export { Switch } from "./Switch";
package/src/index.ts CHANGED
@@ -18,5 +18,6 @@ export * from "./InputPressable";
18
18
  export * from "./InputText";
19
19
  export * from "./ProgressBar";
20
20
  export * from "./StatusLabel";
21
+ export * from "./Switch";
21
22
  export * from "./Text";
22
23
  export * from "./Typography";