@jobber/components-native 0.28.1 → 0.30.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/FormField/FormField.js +10 -0
- package/dist/src/FormField/index.js +1 -0
- package/dist/src/InputPassword/InputPassword.js +45 -0
- package/dist/src/InputPassword/index.js +1 -0
- package/dist/src/InputPassword/messages.js +8 -0
- package/dist/src/index.js +2 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/src/FormField/FormField.d.ts +24 -0
- package/dist/types/src/FormField/index.d.ts +1 -0
- package/dist/types/src/InputPassword/InputPassword.d.ts +12 -0
- package/dist/types/src/InputPassword/index.d.ts +1 -0
- package/dist/types/src/InputPassword/messages.d.ts +7 -0
- package/dist/types/src/index.d.ts +2 -0
- package/package.json +2 -2
- package/src/FormField/FormField.test.tsx +135 -0
- package/src/FormField/FormField.tsx +50 -0
- package/src/FormField/index.ts +1 -0
- package/src/InputPassword/InputPassword.test.tsx +92 -0
- package/src/InputPassword/InputPassword.tsx +68 -0
- package/src/InputPassword/index.ts +1 -0
- package/src/InputPassword/messages.ts +9 -0
- package/src/index.ts +2 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { ControllerRenderProps, FieldError, FieldValues, RegisterOptions } from "react-hook-form";
|
|
3
|
+
interface FormFieldProps<T> {
|
|
4
|
+
/**
|
|
5
|
+
* Name of the field.
|
|
6
|
+
*/
|
|
7
|
+
name: string;
|
|
8
|
+
/**
|
|
9
|
+
* The initial value of the form field.
|
|
10
|
+
*/
|
|
11
|
+
readonly defaultValue?: T;
|
|
12
|
+
/**
|
|
13
|
+
* Children to render.
|
|
14
|
+
*/
|
|
15
|
+
children: (field: ControllerRenderProps<FieldValues, string>, error?: FieldError) => React.ReactNode;
|
|
16
|
+
/**
|
|
17
|
+
* Rules for returning an error when validations are violated.
|
|
18
|
+
* WARNING: This component needs to be nested inside a FormProvider
|
|
19
|
+
* for validations to work.
|
|
20
|
+
*/
|
|
21
|
+
readonly validations?: RegisterOptions;
|
|
22
|
+
}
|
|
23
|
+
export declare function FormField<T>({ name, children, defaultValue: value, validations, }: FormFieldProps<T>): JSX.Element;
|
|
24
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { FormField } from "./FormField";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { InputTextProps, InputTextRef } from "../InputText";
|
|
3
|
+
export declare const InputPassword: React.ForwardRefExoticComponent<InputPasswordProps & React.RefAttributes<InputTextRef>>;
|
|
4
|
+
interface InputPasswordProps extends Omit<InputTextProps, "keyboard" | "secureTextEntry" | "textContentType" | "clearable"> {
|
|
5
|
+
/**
|
|
6
|
+
* Determines if InputPassword uses privacy eye suffix
|
|
7
|
+
*
|
|
8
|
+
* @default true
|
|
9
|
+
*/
|
|
10
|
+
usePrivacyEye?: boolean;
|
|
11
|
+
}
|
|
12
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { InputPassword } from "./InputPassword";
|
|
@@ -12,10 +12,12 @@ export * from "./Divider";
|
|
|
12
12
|
export * from "./EmptyState";
|
|
13
13
|
export * from "./ErrorMessageWrapper";
|
|
14
14
|
export * from "./Flex";
|
|
15
|
+
export * from "./FormField";
|
|
15
16
|
export * from "./Heading";
|
|
16
17
|
export * from "./Icon";
|
|
17
18
|
export * from "./IconButton";
|
|
18
19
|
export * from "./InputFieldWrapper";
|
|
20
|
+
export * from "./InputPassword";
|
|
19
21
|
export * from "./InputPressable";
|
|
20
22
|
export * from "./InputSearch";
|
|
21
23
|
export * from "./InputText";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jobber/components-native",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.30.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"main": "dist/src/index.js",
|
|
6
6
|
"module": "dist/src/index.js",
|
|
@@ -57,5 +57,5 @@
|
|
|
57
57
|
"react": "^18",
|
|
58
58
|
"react-native": ">=0.69.2"
|
|
59
59
|
},
|
|
60
|
-
"gitHead": "
|
|
60
|
+
"gitHead": "362d0d3914aa440bcbb40f9eed388d447cd205e0"
|
|
61
61
|
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { fireEvent, render, waitFor } from "@testing-library/react-native";
|
|
3
|
+
import { FormProvider, useForm } from "react-hook-form";
|
|
4
|
+
import { Button } from "react-native";
|
|
5
|
+
import { FormField } from ".";
|
|
6
|
+
import { InputText } from "../InputText";
|
|
7
|
+
|
|
8
|
+
const mockOnSubmit = jest.fn();
|
|
9
|
+
const inputAccessibilityLabel = "textInput";
|
|
10
|
+
const saveButtonText = "Save Me";
|
|
11
|
+
|
|
12
|
+
describe("when a component is wrapped in a FormField within a Form", () => {
|
|
13
|
+
function SimpleFormWithProvider({ children }) {
|
|
14
|
+
const formMethods = useForm();
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<FormProvider {...formMethods}>
|
|
18
|
+
{children}
|
|
19
|
+
<Button
|
|
20
|
+
onPress={formMethods.handleSubmit(values => mockOnSubmit(values))}
|
|
21
|
+
title={saveButtonText}
|
|
22
|
+
accessibilityLabel={saveButtonText}
|
|
23
|
+
/>
|
|
24
|
+
</FormProvider>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe("when the component's value is changed", () => {
|
|
29
|
+
it("updates the form value for that component", async () => {
|
|
30
|
+
const defaultInputValue = "Sonkey";
|
|
31
|
+
const newInputValue = "Donic";
|
|
32
|
+
|
|
33
|
+
const { getByLabelText } = render(
|
|
34
|
+
<SimpleFormWithProvider>
|
|
35
|
+
<FormField name="formTextInput" defaultValue={defaultInputValue}>
|
|
36
|
+
{field => {
|
|
37
|
+
return (
|
|
38
|
+
<InputText
|
|
39
|
+
name={field.name}
|
|
40
|
+
accessibilityLabel={inputAccessibilityLabel}
|
|
41
|
+
value={field.value}
|
|
42
|
+
onChangeText={field.onChange}
|
|
43
|
+
/>
|
|
44
|
+
);
|
|
45
|
+
}}
|
|
46
|
+
</FormField>
|
|
47
|
+
</SimpleFormWithProvider>,
|
|
48
|
+
);
|
|
49
|
+
const textInput = getByLabelText(inputAccessibilityLabel);
|
|
50
|
+
fireEvent.changeText(textInput, newInputValue);
|
|
51
|
+
|
|
52
|
+
const saveButton = getByLabelText(saveButtonText);
|
|
53
|
+
await waitFor(() => {
|
|
54
|
+
fireEvent.press(saveButton);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
expect(mockOnSubmit).toHaveBeenCalledWith({
|
|
58
|
+
formTextInput: newInputValue,
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe("when the validations of the component are violated", () => {
|
|
64
|
+
it("returns an error from the FormField wrapper", async () => {
|
|
65
|
+
const newInputValue = "Exceeding 5 characters";
|
|
66
|
+
const maxLengthErrorMessage = "error: max length exceeded";
|
|
67
|
+
|
|
68
|
+
const { getByLabelText, getAllByText } = render(
|
|
69
|
+
<SimpleFormWithProvider>
|
|
70
|
+
<FormField
|
|
71
|
+
name="formTextInput"
|
|
72
|
+
validations={{
|
|
73
|
+
maxLength: {
|
|
74
|
+
value: 5,
|
|
75
|
+
message: maxLengthErrorMessage,
|
|
76
|
+
},
|
|
77
|
+
}}
|
|
78
|
+
>
|
|
79
|
+
{field => {
|
|
80
|
+
return (
|
|
81
|
+
<>
|
|
82
|
+
<InputText
|
|
83
|
+
name={field.name}
|
|
84
|
+
accessibilityLabel={inputAccessibilityLabel}
|
|
85
|
+
value={field.value}
|
|
86
|
+
onChangeText={field.onChange}
|
|
87
|
+
/>
|
|
88
|
+
</>
|
|
89
|
+
);
|
|
90
|
+
}}
|
|
91
|
+
</FormField>
|
|
92
|
+
</SimpleFormWithProvider>,
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const textInput = getByLabelText(inputAccessibilityLabel);
|
|
96
|
+
fireEvent.changeText(textInput, newInputValue);
|
|
97
|
+
|
|
98
|
+
const saveButton = getByLabelText(saveButtonText);
|
|
99
|
+
await waitFor(() => {
|
|
100
|
+
fireEvent.press(saveButton);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
expect(
|
|
104
|
+
getAllByText(maxLengthErrorMessage, { includeHiddenElements: true }),
|
|
105
|
+
).toHaveLength(1);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe("when a component is not in a Form with a Form ProviderField within a Form", () => {
|
|
111
|
+
describe("when the component's value is changed", () => {
|
|
112
|
+
it("updates the form value for that component", async () => {
|
|
113
|
+
const defaultInputValue = "Sonkey";
|
|
114
|
+
const newInputValue = "Donic";
|
|
115
|
+
|
|
116
|
+
const { getByDisplayValue, getByLabelText } = render(
|
|
117
|
+
<FormField name="formTextInput" defaultValue={defaultInputValue}>
|
|
118
|
+
{field => {
|
|
119
|
+
return (
|
|
120
|
+
<InputText
|
|
121
|
+
name={field.name}
|
|
122
|
+
accessibilityLabel={inputAccessibilityLabel}
|
|
123
|
+
value={field.value}
|
|
124
|
+
onChangeText={field.onChange}
|
|
125
|
+
/>
|
|
126
|
+
);
|
|
127
|
+
}}
|
|
128
|
+
</FormField>,
|
|
129
|
+
);
|
|
130
|
+
const textInput = getByLabelText(inputAccessibilityLabel);
|
|
131
|
+
fireEvent.changeText(textInput, newInputValue);
|
|
132
|
+
expect(getByDisplayValue(newInputValue)).toBeDefined();
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import {
|
|
3
|
+
ControllerRenderProps,
|
|
4
|
+
FieldError,
|
|
5
|
+
FieldValues,
|
|
6
|
+
RegisterOptions,
|
|
7
|
+
} from "react-hook-form";
|
|
8
|
+
import { useFormController } from "../hooks";
|
|
9
|
+
|
|
10
|
+
interface FormFieldProps<T> {
|
|
11
|
+
/**
|
|
12
|
+
* Name of the field.
|
|
13
|
+
*/
|
|
14
|
+
name: string;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The initial value of the form field.
|
|
18
|
+
*/
|
|
19
|
+
readonly defaultValue?: T;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Children to render.
|
|
23
|
+
*/
|
|
24
|
+
children: (
|
|
25
|
+
field: ControllerRenderProps<FieldValues, string>,
|
|
26
|
+
error?: FieldError,
|
|
27
|
+
) => React.ReactNode;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Rules for returning an error when validations are violated.
|
|
31
|
+
* WARNING: This component needs to be nested inside a FormProvider
|
|
32
|
+
* for validations to work.
|
|
33
|
+
*/
|
|
34
|
+
readonly validations?: RegisterOptions;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function FormField<T>({
|
|
38
|
+
name,
|
|
39
|
+
children,
|
|
40
|
+
defaultValue: value,
|
|
41
|
+
validations,
|
|
42
|
+
}: FormFieldProps<T>): JSX.Element {
|
|
43
|
+
const { error, field } = useFormController({
|
|
44
|
+
name,
|
|
45
|
+
value,
|
|
46
|
+
validations,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
return <>{children({ ...field }, error)}</>;
|
|
50
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { FormField } from "./FormField";
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { fireEvent, render, waitFor } from "@testing-library/react-native";
|
|
3
|
+
import { InputPassword } from "./InputPassword";
|
|
4
|
+
import { messages } from "./messages";
|
|
5
|
+
import { InputFieldWrapperProps } from "../InputFieldWrapper";
|
|
6
|
+
|
|
7
|
+
const MockInputFieldWrapper = jest.fn();
|
|
8
|
+
jest.mock("../InputFieldWrapper", () => ({
|
|
9
|
+
...jest.requireActual("../InputFieldWrapper"),
|
|
10
|
+
InputFieldWrapper: function Mock(props: InputFieldWrapperProps) {
|
|
11
|
+
MockInputFieldWrapper(props);
|
|
12
|
+
return jest.requireActual("../InputFieldWrapper").InputFieldWrapper(props);
|
|
13
|
+
},
|
|
14
|
+
}));
|
|
15
|
+
describe("InputPassword", () => {
|
|
16
|
+
it("renders an InputPassword", () => {
|
|
17
|
+
const value = "password";
|
|
18
|
+
const { getByDisplayValue } = render(<InputPassword value={value} />);
|
|
19
|
+
expect(getByDisplayValue(value)).toBeTruthy();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("displays the validation message when an invalid password is entered", async () => {
|
|
23
|
+
const a11yLabel = "InputPasswordTest";
|
|
24
|
+
const { getByText, getByLabelText } = render(
|
|
25
|
+
<InputPassword value="" accessibilityLabel={a11yLabel} />,
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
await waitFor(() => {
|
|
29
|
+
fireEvent(getByLabelText(a11yLabel), "blur");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
expect(
|
|
33
|
+
getByText(messages.passwordRequired.defaultMessage, {
|
|
34
|
+
includeHiddenElements: true,
|
|
35
|
+
}),
|
|
36
|
+
).toBeDefined();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe("InputPassword gets the expected props", () => {
|
|
40
|
+
it("renders an invalid InputPassword", () => {
|
|
41
|
+
const props = { invalid: true };
|
|
42
|
+
render(<InputPassword {...props} />);
|
|
43
|
+
expect(MockInputFieldWrapper).toHaveBeenCalledWith(
|
|
44
|
+
expect.objectContaining(props),
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe("Privacy eye", () => {
|
|
50
|
+
it("renders when usePrivaryEye is true", () => {
|
|
51
|
+
const testId = "eye";
|
|
52
|
+
const { getByTestId } = render(
|
|
53
|
+
<InputPassword
|
|
54
|
+
value="password"
|
|
55
|
+
suffix={{
|
|
56
|
+
icon: testId,
|
|
57
|
+
}}
|
|
58
|
+
/>,
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
expect(getByTestId("eye")).toBeDefined();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("does not render when usePrivaryEye is false", () => {
|
|
65
|
+
const testId = "eye";
|
|
66
|
+
const { queryByTestId } = render(
|
|
67
|
+
<InputPassword
|
|
68
|
+
value="password"
|
|
69
|
+
usePrivacyEye={false}
|
|
70
|
+
suffix={{
|
|
71
|
+
icon: testId,
|
|
72
|
+
}}
|
|
73
|
+
/>,
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
expect(queryByTestId("eye")).toBeNull();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("obscures the password by default", () => {
|
|
80
|
+
const { getByDisplayValue } = render(
|
|
81
|
+
<InputPassword
|
|
82
|
+
value="password"
|
|
83
|
+
suffix={{
|
|
84
|
+
icon: "eye",
|
|
85
|
+
}}
|
|
86
|
+
/>,
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
expect(getByDisplayValue("password").props.secureTextEntry).toBeTruthy();
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import React, { Ref, forwardRef, useState } from "react";
|
|
2
|
+
import { useIntl } from "react-intl";
|
|
3
|
+
import { IconNames } from "@jobber/design";
|
|
4
|
+
import { messages } from "./messages";
|
|
5
|
+
import { InputText, InputTextProps, InputTextRef } from "../InputText";
|
|
6
|
+
|
|
7
|
+
export const InputPassword = forwardRef(InputPasswordInternal);
|
|
8
|
+
|
|
9
|
+
interface InputPasswordProps
|
|
10
|
+
extends Omit<
|
|
11
|
+
InputTextProps,
|
|
12
|
+
"keyboard" | "secureTextEntry" | "textContentType" | "clearable"
|
|
13
|
+
> {
|
|
14
|
+
/**
|
|
15
|
+
* Determines if InputPassword uses privacy eye suffix
|
|
16
|
+
*
|
|
17
|
+
* @default true
|
|
18
|
+
*/
|
|
19
|
+
usePrivacyEye?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function InputPasswordInternal(
|
|
23
|
+
{ usePrivacyEye = true, ...props }: InputPasswordProps,
|
|
24
|
+
ref: Ref<InputTextRef>,
|
|
25
|
+
): JSX.Element {
|
|
26
|
+
const { formatMessage } = useIntl();
|
|
27
|
+
const [passwordHidden, setPasswordHidden] = useState(true);
|
|
28
|
+
const [privacyEye, setPrivacyEye] = useState<IconNames>("eye");
|
|
29
|
+
|
|
30
|
+
const handleOnPress = () => {
|
|
31
|
+
if (privacyEye === "eye") {
|
|
32
|
+
setPrivacyEye("eyeCrossed");
|
|
33
|
+
setPasswordHidden(false);
|
|
34
|
+
} else if (privacyEye === "eyeCrossed") {
|
|
35
|
+
setPrivacyEye("eye");
|
|
36
|
+
setPasswordHidden(true);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const privacyEyeSuffix = () => {
|
|
41
|
+
if (usePrivacyEye === true) {
|
|
42
|
+
return {
|
|
43
|
+
icon: privacyEye,
|
|
44
|
+
onPress: handleOnPress,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
return undefined;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<InputText
|
|
52
|
+
{...props}
|
|
53
|
+
ref={ref}
|
|
54
|
+
keyboard="default"
|
|
55
|
+
secureTextEntry={passwordHidden}
|
|
56
|
+
textContentType="password"
|
|
57
|
+
clearable="never"
|
|
58
|
+
suffix={privacyEyeSuffix()}
|
|
59
|
+
validations={{
|
|
60
|
+
required: {
|
|
61
|
+
value: true,
|
|
62
|
+
message: formatMessage(messages.passwordRequired),
|
|
63
|
+
},
|
|
64
|
+
...props.validations,
|
|
65
|
+
}}
|
|
66
|
+
/>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { InputPassword } from "./InputPassword";
|
package/src/index.ts
CHANGED
|
@@ -12,10 +12,12 @@ export * from "./Divider";
|
|
|
12
12
|
export * from "./EmptyState";
|
|
13
13
|
export * from "./ErrorMessageWrapper";
|
|
14
14
|
export * from "./Flex";
|
|
15
|
+
export * from "./FormField";
|
|
15
16
|
export * from "./Heading";
|
|
16
17
|
export * from "./Icon";
|
|
17
18
|
export * from "./IconButton";
|
|
18
19
|
export * from "./InputFieldWrapper";
|
|
20
|
+
export * from "./InputPassword";
|
|
19
21
|
export * from "./InputPressable";
|
|
20
22
|
export * from "./InputSearch";
|
|
21
23
|
export * from "./InputText";
|