@evervault/react-native 2.0.0 → 2.2.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/android/build.gradle +6 -1
- package/build/Card/Root.d.ts +4 -0
- package/build/Card/Root.d.ts.map +1 -1
- package/build/Input.d.ts +4 -0
- package/build/Input.d.ts.map +1 -1
- package/build/index.cjs.js +70 -47
- package/build/index.cjs.js.map +1 -1
- package/build/index.esm.js +71 -48
- package/package.json +2 -1
- package/src/Card/Root.tsx +40 -8
- package/src/Input.test.tsx +204 -6
- package/src/Input.tsx +22 -2
package/build/index.esm.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { TurboModuleRegistry, TextInput, Platform, StyleSheet } from 'react-native';
|
|
2
2
|
import { jsx } from 'react/jsx-runtime';
|
|
3
3
|
import * as React from 'react';
|
|
4
|
-
import React__default, { createContext, useMemo, useCallback, useContext, forwardRef,
|
|
4
|
+
import React__default, { createContext, useMemo, useCallback, useContext, forwardRef, useRef, useImperativeHandle, useEffect, useState } from 'react';
|
|
5
5
|
import { WebView } from 'react-native-webview';
|
|
6
6
|
|
|
7
7
|
const NativeEvervault = TurboModuleRegistry.get("NativeEvervault");
|
|
@@ -7164,51 +7164,6 @@ const r=(t,r,o)=>{if(t&&"reportValidity"in t){const s=get(o,r);t.setCustomValidi
|
|
|
7164
7164
|
|
|
7165
7165
|
function n(r,e){for(var n={};r.length;){var s=r[0],t=s.code,i=s.message,a=s.path.join(".");if(!n[a])if("unionErrors"in s){var u=s.unionErrors[0].errors[0];n[a]={message:u.message,type:u.code};}else n[a]={message:i,type:t};if("unionErrors"in s&&s.unionErrors.forEach(function(e){return e.errors.forEach(function(e){return r.push(e)})}),e){var c=n[a].types,f=c&&c[s.code];n[a]=appendErrors(a,e,n,t,f?[].concat(f,s.message):s.message);}r.shift();}return n}function s(o$1,s,t){return void 0===t&&(t={}),function(i,a,u){try{return Promise.resolve(function(e,n){try{var a=Promise.resolve(o$1["sync"===t.mode?"parse":"parseAsync"](i,s)).then(function(e){return u.shouldUseNativeValidation&&o({},u),{errors:{},values:t.raw?Object.assign({},i):e}});}catch(r){return n(r)}return a&&a.then?a.then(void 0,n):a}(0,function(r){if(function(r){return Array.isArray(null==r?void 0:r.errors)}(r))return {values:{},errors:s$1(n(r.errors,!u.shouldUseNativeValidation&&"all"===u.criteriaMode),u)};throw r}))}catch(r){return Promise.reject(r)}}}
|
|
7166
7166
|
|
|
7167
|
-
const DEFAULT_ACCEPTED_BRANDS = [];
|
|
7168
|
-
const Card$1 = forwardRef(function Card({ children, defaultValues, onChange, acceptedBrands = DEFAULT_ACCEPTED_BRANDS, validationMode = "all", }, ref) {
|
|
7169
|
-
const evervault = useEvervault();
|
|
7170
|
-
const resolver = useMemo(() => {
|
|
7171
|
-
const schema = getCardFormSchema(acceptedBrands);
|
|
7172
|
-
return s(schema);
|
|
7173
|
-
}, [acceptedBrands]);
|
|
7174
|
-
const methods = useForm({
|
|
7175
|
-
defaultValues,
|
|
7176
|
-
resolver,
|
|
7177
|
-
mode: validationMode,
|
|
7178
|
-
shouldUseNativeValidation: false,
|
|
7179
|
-
});
|
|
7180
|
-
useEffect(() => {
|
|
7181
|
-
if (!onChange)
|
|
7182
|
-
return;
|
|
7183
|
-
let abortController;
|
|
7184
|
-
function handleChange(values) {
|
|
7185
|
-
if (abortController) {
|
|
7186
|
-
abortController.abort();
|
|
7187
|
-
}
|
|
7188
|
-
abortController = new AbortController();
|
|
7189
|
-
const signal = abortController.signal;
|
|
7190
|
-
requestAnimationFrame(async () => {
|
|
7191
|
-
const payload = await formatPayload(values, {
|
|
7192
|
-
encrypt: evervault.encrypt,
|
|
7193
|
-
form: methods,
|
|
7194
|
-
});
|
|
7195
|
-
if (signal.aborted)
|
|
7196
|
-
return;
|
|
7197
|
-
onChange?.(payload);
|
|
7198
|
-
});
|
|
7199
|
-
}
|
|
7200
|
-
handleChange(methods.getValues());
|
|
7201
|
-
const subscription = methods.watch(handleChange);
|
|
7202
|
-
return () => subscription.unsubscribe();
|
|
7203
|
-
}, [evervault.encrypt, onChange]);
|
|
7204
|
-
useImperativeHandle(ref, useCallback(() => ({
|
|
7205
|
-
reset() {
|
|
7206
|
-
methods.reset();
|
|
7207
|
-
},
|
|
7208
|
-
}), []));
|
|
7209
|
-
return jsx(FormProvider, { ...methods, children: children });
|
|
7210
|
-
});
|
|
7211
|
-
|
|
7212
7167
|
// Taken from https://github.com/gregberge/react-merge-refs
|
|
7213
7168
|
function mergeRefs(...refs) {
|
|
7214
7169
|
return (value) => {
|
|
@@ -7408,6 +7363,9 @@ var MaskInput = /*#__PURE__*/React.forwardRef(function (props, ref) {
|
|
|
7408
7363
|
}));
|
|
7409
7364
|
});
|
|
7410
7365
|
|
|
7366
|
+
const EvervaultInputContext = createContext({
|
|
7367
|
+
validationMode: "all",
|
|
7368
|
+
});
|
|
7411
7369
|
function useForwardedInputRef(ref) {
|
|
7412
7370
|
const inputRef = useRef(null);
|
|
7413
7371
|
useImperativeHandle(ref, useCallback(() => ({
|
|
@@ -7444,6 +7402,7 @@ function mask(format) {
|
|
|
7444
7402
|
});
|
|
7445
7403
|
}
|
|
7446
7404
|
const EvervaultInput = forwardRef(function EvervaultInput({ name, mask, ...props }, ref) {
|
|
7405
|
+
const { validationMode } = useContext(EvervaultInputContext);
|
|
7447
7406
|
const inputRef = useForwardedInputRef(ref);
|
|
7448
7407
|
const methods = useFormContext();
|
|
7449
7408
|
return (jsx(Controller, { control: methods.control, name: name, shouldUnregister: true, render: ({ field, fieldState }) => (jsx(MaskInput
|
|
@@ -7453,22 +7412,86 @@ const EvervaultInput = forwardRef(function EvervaultInput({ name, mask, ...props
|
|
|
7453
7412
|
id: field.name, ...props,
|
|
7454
7413
|
// Strict props
|
|
7455
7414
|
ref: mergeRefs(inputRef, field.ref), editable: !field.disabled && (props.editable ?? true), onBlur: (evt) => {
|
|
7415
|
+
const shouldValidate = validationMode === "onBlur" ||
|
|
7416
|
+
validationMode === "onTouched" ||
|
|
7417
|
+
validationMode === "all";
|
|
7456
7418
|
methods.setValue(field.name, field.value, {
|
|
7457
7419
|
shouldDirty: true,
|
|
7458
7420
|
shouldTouch: true,
|
|
7459
|
-
shouldValidate
|
|
7421
|
+
shouldValidate,
|
|
7460
7422
|
});
|
|
7461
7423
|
props.onBlur?.(evt);
|
|
7462
7424
|
}, mask: mask, maskAutoComplete: !!mask, value: field.value, onChangeText: (masked, unmasked) => {
|
|
7425
|
+
const shouldValidate = (validationMode === "onTouched" && fieldState.isTouched) ||
|
|
7426
|
+
((validationMode === "onChange" || validationMode === "all") &&
|
|
7427
|
+
(!!fieldState.error || fieldState.isTouched));
|
|
7463
7428
|
methods.setValue(field.name, unmasked, {
|
|
7464
7429
|
shouldDirty: true,
|
|
7465
|
-
shouldValidate
|
|
7430
|
+
shouldValidate,
|
|
7466
7431
|
});
|
|
7467
7432
|
},
|
|
7468
7433
|
// Remove unwanted props
|
|
7469
7434
|
defaultValue: undefined, onChange: undefined })) }));
|
|
7470
7435
|
});
|
|
7471
7436
|
|
|
7437
|
+
const DEFAULT_ACCEPTED_BRANDS = [];
|
|
7438
|
+
const Card$1 = forwardRef(function Card({ children, defaultValues, onChange, onError, acceptedBrands = DEFAULT_ACCEPTED_BRANDS, validationMode = "all", }, ref) {
|
|
7439
|
+
const evervault = useEvervault();
|
|
7440
|
+
const resolver = useMemo(() => {
|
|
7441
|
+
const schema = getCardFormSchema(acceptedBrands);
|
|
7442
|
+
return s(schema);
|
|
7443
|
+
}, [acceptedBrands]);
|
|
7444
|
+
const methods = useForm({
|
|
7445
|
+
defaultValues,
|
|
7446
|
+
resolver,
|
|
7447
|
+
mode: validationMode,
|
|
7448
|
+
shouldUseNativeValidation: false,
|
|
7449
|
+
});
|
|
7450
|
+
const inputContext = useMemo(() => ({
|
|
7451
|
+
validationMode,
|
|
7452
|
+
}), [validationMode]);
|
|
7453
|
+
// Use refs to prevent closures from being captured
|
|
7454
|
+
const onChangeRef = useRef(onChange);
|
|
7455
|
+
onChangeRef.current = onChange;
|
|
7456
|
+
const onErrorRef = useRef(onError);
|
|
7457
|
+
onErrorRef.current = onError;
|
|
7458
|
+
useEffect(() => {
|
|
7459
|
+
if (!onChange)
|
|
7460
|
+
return;
|
|
7461
|
+
let abortController;
|
|
7462
|
+
function handleChange(values) {
|
|
7463
|
+
if (abortController) {
|
|
7464
|
+
abortController.abort();
|
|
7465
|
+
}
|
|
7466
|
+
abortController = new AbortController();
|
|
7467
|
+
const signal = abortController.signal;
|
|
7468
|
+
requestAnimationFrame(async () => {
|
|
7469
|
+
try {
|
|
7470
|
+
const payload = await formatPayload(values, {
|
|
7471
|
+
encrypt: evervault.encrypt,
|
|
7472
|
+
form: methods,
|
|
7473
|
+
});
|
|
7474
|
+
if (signal.aborted)
|
|
7475
|
+
return;
|
|
7476
|
+
onChangeRef.current?.(payload);
|
|
7477
|
+
}
|
|
7478
|
+
catch (error) {
|
|
7479
|
+
onErrorRef.current?.(error);
|
|
7480
|
+
}
|
|
7481
|
+
});
|
|
7482
|
+
}
|
|
7483
|
+
handleChange(methods.getValues());
|
|
7484
|
+
const subscription = methods.watch(handleChange);
|
|
7485
|
+
return () => subscription.unsubscribe();
|
|
7486
|
+
}, [evervault.encrypt]);
|
|
7487
|
+
useImperativeHandle(ref, useCallback(() => ({
|
|
7488
|
+
reset() {
|
|
7489
|
+
methods.reset();
|
|
7490
|
+
},
|
|
7491
|
+
}), []));
|
|
7492
|
+
return (jsx(FormProvider, { ...methods, children: jsx(EvervaultInputContext.Provider, { value: inputContext, children: children }) }));
|
|
7493
|
+
});
|
|
7494
|
+
|
|
7472
7495
|
const CardHolder = forwardRef(function CardHolder(props, ref) {
|
|
7473
7496
|
return (jsx(EvervaultInput, { placeholder: "Johnny Appleseed", ...props, ref: ref, name: "name", inputMode: "text", autoComplete: Platform.select({
|
|
7474
7497
|
ios: "cc-name",
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@evervault/react-native",
|
|
3
3
|
"description": "Evervault SDK for React Native",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.2.0",
|
|
5
5
|
"source": "./src/index.ts",
|
|
6
6
|
"main": "./build/index.cjs.js",
|
|
7
7
|
"module": "./build/index.esm.js",
|
|
@@ -76,6 +76,7 @@
|
|
|
76
76
|
"@evervault/card-validator": "1.3.0"
|
|
77
77
|
},
|
|
78
78
|
"scripts": {
|
|
79
|
+
"prebuild": "pnpm codegen",
|
|
79
80
|
"build": "rollup -c",
|
|
80
81
|
"watch": "rollup -c --watch",
|
|
81
82
|
"codegen": "rimraf build android/app && react-native codegen",
|
package/src/Card/Root.tsx
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
useEffect,
|
|
6
6
|
useImperativeHandle,
|
|
7
7
|
useMemo,
|
|
8
|
+
useRef,
|
|
8
9
|
} from "react";
|
|
9
10
|
import { CardBrandName, CardConfig, CardPayload } from "./types";
|
|
10
11
|
import { DeepPartial, FormProvider, useForm } from "react-hook-form";
|
|
@@ -12,6 +13,8 @@ import { CardFormValues, getCardFormSchema } from "./schema";
|
|
|
12
13
|
import { zodResolver } from "@hookform/resolvers/zod";
|
|
13
14
|
import { useEvervault } from "../useEvervault";
|
|
14
15
|
import { formatPayload } from "./utils";
|
|
16
|
+
import { EvervaultInputContext, EvervaultInputContextValue } from "../Input";
|
|
17
|
+
import { EvervaultContextValue } from "../context";
|
|
15
18
|
|
|
16
19
|
const DEFAULT_ACCEPTED_BRANDS: CardBrandName[] = [];
|
|
17
20
|
|
|
@@ -31,6 +34,11 @@ export interface CardProps extends PropsWithChildren, CardConfig {
|
|
|
31
34
|
*/
|
|
32
35
|
onChange?(payload: CardPayload): void;
|
|
33
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Triggered when a native error occurs.
|
|
39
|
+
*/
|
|
40
|
+
onError?(error: Error): void;
|
|
41
|
+
|
|
34
42
|
/**
|
|
35
43
|
* The validation mode to use for the form.
|
|
36
44
|
*
|
|
@@ -56,6 +64,7 @@ export const Card = forwardRef<Card, CardProps>(function Card(
|
|
|
56
64
|
children,
|
|
57
65
|
defaultValues,
|
|
58
66
|
onChange,
|
|
67
|
+
onError,
|
|
59
68
|
acceptedBrands = DEFAULT_ACCEPTED_BRANDS,
|
|
60
69
|
validationMode = "all",
|
|
61
70
|
},
|
|
@@ -75,6 +84,19 @@ export const Card = forwardRef<Card, CardProps>(function Card(
|
|
|
75
84
|
shouldUseNativeValidation: false,
|
|
76
85
|
});
|
|
77
86
|
|
|
87
|
+
const inputContext = useMemo<EvervaultInputContextValue>(
|
|
88
|
+
() => ({
|
|
89
|
+
validationMode,
|
|
90
|
+
}),
|
|
91
|
+
[validationMode]
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
// Use refs to prevent closures from being captured
|
|
95
|
+
const onChangeRef = useRef<typeof onChange>(onChange);
|
|
96
|
+
onChangeRef.current = onChange;
|
|
97
|
+
const onErrorRef = useRef<typeof onError>(onError);
|
|
98
|
+
onErrorRef.current = onError;
|
|
99
|
+
|
|
78
100
|
useEffect(() => {
|
|
79
101
|
if (!onChange) return;
|
|
80
102
|
|
|
@@ -88,19 +110,23 @@ export const Card = forwardRef<Card, CardProps>(function Card(
|
|
|
88
110
|
const signal = abortController.signal;
|
|
89
111
|
|
|
90
112
|
requestAnimationFrame(async () => {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
113
|
+
try {
|
|
114
|
+
const payload = await formatPayload(values, {
|
|
115
|
+
encrypt: evervault.encrypt,
|
|
116
|
+
form: methods,
|
|
117
|
+
});
|
|
118
|
+
if (signal.aborted) return;
|
|
119
|
+
onChangeRef.current?.(payload);
|
|
120
|
+
} catch (error) {
|
|
121
|
+
onErrorRef.current?.(error as Error);
|
|
122
|
+
}
|
|
97
123
|
});
|
|
98
124
|
}
|
|
99
125
|
|
|
100
126
|
handleChange(methods.getValues());
|
|
101
127
|
const subscription = methods.watch(handleChange);
|
|
102
128
|
return () => subscription.unsubscribe();
|
|
103
|
-
}, [evervault.encrypt
|
|
129
|
+
}, [evervault.encrypt]);
|
|
104
130
|
|
|
105
131
|
useImperativeHandle(
|
|
106
132
|
ref,
|
|
@@ -114,5 +140,11 @@ export const Card = forwardRef<Card, CardProps>(function Card(
|
|
|
114
140
|
)
|
|
115
141
|
);
|
|
116
142
|
|
|
117
|
-
return
|
|
143
|
+
return (
|
|
144
|
+
<FormProvider {...methods}>
|
|
145
|
+
<EvervaultInputContext.Provider value={inputContext}>
|
|
146
|
+
{children}
|
|
147
|
+
</EvervaultInputContext.Provider>
|
|
148
|
+
</FormProvider>
|
|
149
|
+
);
|
|
118
150
|
});
|
package/src/Input.test.tsx
CHANGED
|
@@ -4,10 +4,11 @@ import {
|
|
|
4
4
|
screen,
|
|
5
5
|
userEvent,
|
|
6
6
|
} from "@testing-library/react-native";
|
|
7
|
-
import { EvervaultInput, mask } from "./Input";
|
|
8
|
-
import { FormProvider,
|
|
7
|
+
import { EvervaultInput, EvervaultInputContext, mask } from "./Input";
|
|
8
|
+
import { FieldErrors, FormProvider, Resolver, useForm } from "react-hook-form";
|
|
9
9
|
import { PropsWithChildren } from "react";
|
|
10
|
-
import {
|
|
10
|
+
import { zodResolver } from "@hookform/resolvers/zod";
|
|
11
|
+
import { z } from "zod";
|
|
11
12
|
|
|
12
13
|
describe("mask", () => {
|
|
13
14
|
it("should convert a mask to an array of regex", () => {
|
|
@@ -31,21 +32,38 @@ describe("mask", () => {
|
|
|
31
32
|
describe("EvervaultInput", () => {
|
|
32
33
|
const methodMocks = {
|
|
33
34
|
setValue: vi.fn(),
|
|
35
|
+
setError: vi.fn(),
|
|
34
36
|
};
|
|
35
37
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
+
interface FormProps {
|
|
39
|
+
resolver?: Resolver<any>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function Form({ children, resolver }: PropsWithChildren<FormProps>) {
|
|
43
|
+
const methods = useForm({
|
|
44
|
+
resolver,
|
|
45
|
+
});
|
|
38
46
|
const setValue = (...args: Parameters<typeof methods.setValue>) => {
|
|
39
47
|
methodMocks.setValue(...args);
|
|
40
48
|
methods.setValue(...args);
|
|
41
49
|
};
|
|
50
|
+
const setError = (...args: Parameters<typeof methods.setError>) => {
|
|
51
|
+
methodMocks.setError(...args);
|
|
52
|
+
methods.setError(...args);
|
|
53
|
+
};
|
|
42
54
|
return (
|
|
43
|
-
<FormProvider {...methods} setValue={setValue}>
|
|
55
|
+
<FormProvider {...methods} setValue={setValue} setError={setError}>
|
|
44
56
|
{children}
|
|
45
57
|
</FormProvider>
|
|
46
58
|
);
|
|
47
59
|
}
|
|
48
60
|
|
|
61
|
+
function createForm(options: FormProps) {
|
|
62
|
+
return function (props: PropsWithChildren<FormProps>) {
|
|
63
|
+
return <Form {...options} {...props} />;
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
49
67
|
it("should render", async () => {
|
|
50
68
|
render(<EvervaultInput testID="phone" name="phone" />, {
|
|
51
69
|
wrapper: Form,
|
|
@@ -135,4 +153,184 @@ describe("EvervaultInput", () => {
|
|
|
135
153
|
shouldValidate: true,
|
|
136
154
|
});
|
|
137
155
|
});
|
|
156
|
+
|
|
157
|
+
it("only validates the field when blurred if validationMode=onBlur", async () => {
|
|
158
|
+
render(
|
|
159
|
+
<EvervaultInputContext.Provider value={{ validationMode: "onBlur" }}>
|
|
160
|
+
<EvervaultInput testID="phone" name="phone" />
|
|
161
|
+
</EvervaultInputContext.Provider>,
|
|
162
|
+
{
|
|
163
|
+
wrapper: Form,
|
|
164
|
+
}
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
const input = screen.getByTestId("phone");
|
|
168
|
+
|
|
169
|
+
fireEvent(input, "blur");
|
|
170
|
+
expect(methodMocks.setValue).toHaveBeenLastCalledWith("phone", undefined, {
|
|
171
|
+
shouldDirty: true,
|
|
172
|
+
shouldTouch: true,
|
|
173
|
+
shouldValidate: true,
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const user = userEvent.setup();
|
|
177
|
+
await user.type(input, "1234567890", { skipBlur: true });
|
|
178
|
+
expect(input).toHaveProp("value", "1234567890");
|
|
179
|
+
expect(methodMocks.setValue).toHaveBeenLastCalledWith(
|
|
180
|
+
"phone",
|
|
181
|
+
"1234567890",
|
|
182
|
+
{
|
|
183
|
+
shouldDirty: true,
|
|
184
|
+
shouldValidate: false,
|
|
185
|
+
}
|
|
186
|
+
);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("only validates the field after first touch if validationMode=onTouched", async () => {
|
|
190
|
+
render(
|
|
191
|
+
<EvervaultInputContext.Provider value={{ validationMode: "onTouched" }}>
|
|
192
|
+
<EvervaultInput testID="phone" name="phone" />
|
|
193
|
+
</EvervaultInputContext.Provider>,
|
|
194
|
+
{
|
|
195
|
+
wrapper: Form,
|
|
196
|
+
}
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
const input = screen.getByTestId("phone");
|
|
200
|
+
|
|
201
|
+
const user = userEvent.setup();
|
|
202
|
+
await user.type(input, "1234", { skipBlur: true });
|
|
203
|
+
expect(input).toHaveProp("value", "1234");
|
|
204
|
+
expect(methodMocks.setValue).toHaveBeenLastCalledWith("phone", "1234", {
|
|
205
|
+
shouldDirty: true,
|
|
206
|
+
shouldValidate: false,
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
fireEvent(input, "blur");
|
|
210
|
+
expect(methodMocks.setValue).toHaveBeenLastCalledWith("phone", "1234", {
|
|
211
|
+
shouldDirty: true,
|
|
212
|
+
shouldTouch: true,
|
|
213
|
+
shouldValidate: true,
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
await user.type(input, "567890", { skipBlur: true });
|
|
217
|
+
expect(input).toHaveProp("value", "1234567890");
|
|
218
|
+
expect(methodMocks.setValue).toHaveBeenLastCalledWith(
|
|
219
|
+
"phone",
|
|
220
|
+
"1234567890",
|
|
221
|
+
{ shouldDirty: true, shouldValidate: true }
|
|
222
|
+
);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("only validates the field when changed if validationMode=onChange and the field is touched", async () => {
|
|
226
|
+
render(
|
|
227
|
+
<EvervaultInputContext.Provider value={{ validationMode: "onChange" }}>
|
|
228
|
+
<EvervaultInput testID="phone" name="phone" />
|
|
229
|
+
</EvervaultInputContext.Provider>,
|
|
230
|
+
{
|
|
231
|
+
wrapper: Form,
|
|
232
|
+
}
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
const input = screen.getByTestId("phone");
|
|
236
|
+
|
|
237
|
+
const user = userEvent.setup();
|
|
238
|
+
await user.type(input, "1234567890", { skipBlur: true });
|
|
239
|
+
expect(input).toHaveProp("value", "1234567890");
|
|
240
|
+
expect(methodMocks.setValue).toHaveBeenLastCalledWith(
|
|
241
|
+
"phone",
|
|
242
|
+
"1234567890",
|
|
243
|
+
{ shouldDirty: true, shouldValidate: false }
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
fireEvent(input, "blur");
|
|
247
|
+
expect(methodMocks.setValue).toHaveBeenLastCalledWith(
|
|
248
|
+
"phone",
|
|
249
|
+
"1234567890",
|
|
250
|
+
{ shouldDirty: true, shouldTouch: true, shouldValidate: false }
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
await user.type(input, "1", { skipBlur: true });
|
|
254
|
+
expect(input).toHaveProp("value", "12345678901");
|
|
255
|
+
expect(methodMocks.setValue).toHaveBeenLastCalledWith(
|
|
256
|
+
"phone",
|
|
257
|
+
"12345678901",
|
|
258
|
+
{ shouldDirty: true, shouldValidate: true }
|
|
259
|
+
);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it("only validates the field when changed if validationMode=onChange and the field has errors", async () => {
|
|
263
|
+
render(
|
|
264
|
+
<EvervaultInputContext.Provider value={{ validationMode: "onChange" }}>
|
|
265
|
+
<EvervaultInput testID="phone" name="phone" />
|
|
266
|
+
</EvervaultInputContext.Provider>,
|
|
267
|
+
{
|
|
268
|
+
wrapper: createForm({
|
|
269
|
+
resolver: zodResolver(z.object({ phone: z.string().min(10) })),
|
|
270
|
+
}),
|
|
271
|
+
}
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
const input = screen.getByTestId("phone");
|
|
275
|
+
|
|
276
|
+
const user = userEvent.setup();
|
|
277
|
+
await user.type(input, "1234", { skipBlur: true });
|
|
278
|
+
expect(input).toHaveProp("value", "1234");
|
|
279
|
+
expect(methodMocks.setValue).toHaveBeenLastCalledWith("phone", "1234", {
|
|
280
|
+
shouldDirty: true,
|
|
281
|
+
shouldValidate: false,
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
fireEvent(input, "blur");
|
|
285
|
+
expect(methodMocks.setValue).toHaveBeenLastCalledWith("phone", "1234", {
|
|
286
|
+
shouldDirty: true,
|
|
287
|
+
shouldTouch: true,
|
|
288
|
+
shouldValidate: false,
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
await user.type(input, "5", { skipBlur: true });
|
|
292
|
+
expect(input).toHaveProp("value", "12345");
|
|
293
|
+
expect(methodMocks.setValue).toHaveBeenLastCalledWith("phone", "12345", {
|
|
294
|
+
shouldDirty: true,
|
|
295
|
+
shouldValidate: true,
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it("validates on blur, touch, and change if validationMode=all", async () => {
|
|
300
|
+
render(
|
|
301
|
+
<EvervaultInputContext.Provider value={{ validationMode: "all" }}>
|
|
302
|
+
<EvervaultInput testID="phone" name="phone" />
|
|
303
|
+
</EvervaultInputContext.Provider>,
|
|
304
|
+
{
|
|
305
|
+
wrapper: createForm({
|
|
306
|
+
resolver: zodResolver(z.object({ phone: z.string().min(10) })),
|
|
307
|
+
}),
|
|
308
|
+
}
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
const input = screen.getByTestId("phone");
|
|
312
|
+
|
|
313
|
+
const user = userEvent.setup();
|
|
314
|
+
await user.type(input, "1234", { skipBlur: true });
|
|
315
|
+
expect(input).toHaveProp("value", "1234");
|
|
316
|
+
expect(methodMocks.setValue).toHaveBeenLastCalledWith("phone", "1234", {
|
|
317
|
+
shouldDirty: true,
|
|
318
|
+
shouldValidate: false,
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
fireEvent(input, "blur");
|
|
322
|
+
expect(methodMocks.setValue).toHaveBeenLastCalledWith("phone", "1234", {
|
|
323
|
+
shouldDirty: true,
|
|
324
|
+
shouldTouch: true,
|
|
325
|
+
shouldValidate: true,
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
await user.type(input, "567890", { skipBlur: true });
|
|
329
|
+
expect(input).toHaveProp("value", "1234567890");
|
|
330
|
+
expect(methodMocks.setValue).toHaveBeenLastCalledWith(
|
|
331
|
+
"phone",
|
|
332
|
+
"1234567890",
|
|
333
|
+
{ shouldDirty: true, shouldValidate: true }
|
|
334
|
+
);
|
|
335
|
+
});
|
|
138
336
|
});
|
package/src/Input.tsx
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
|
+
createContext,
|
|
2
3
|
ForwardedRef,
|
|
3
4
|
forwardRef,
|
|
4
5
|
ReactNode,
|
|
5
6
|
Ref,
|
|
6
7
|
RefObject,
|
|
7
8
|
useCallback,
|
|
9
|
+
useContext,
|
|
8
10
|
useImperativeHandle,
|
|
9
11
|
useRef,
|
|
10
12
|
} from "react";
|
|
@@ -13,6 +15,14 @@ import { mergeRefs } from "./utils";
|
|
|
13
15
|
import { Controller, useFormContext } from "react-hook-form";
|
|
14
16
|
import MaskInput, { Mask, MaskArray } from "react-native-mask-input";
|
|
15
17
|
|
|
18
|
+
export interface EvervaultInputContextValue {
|
|
19
|
+
validationMode: "onChange" | "onBlur" | "onTouched" | "all";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const EvervaultInputContext = createContext<EvervaultInputContextValue>({
|
|
23
|
+
validationMode: "all",
|
|
24
|
+
});
|
|
25
|
+
|
|
16
26
|
export type EvervaultInput = Pick<
|
|
17
27
|
TextInput,
|
|
18
28
|
| "isFocused"
|
|
@@ -90,6 +100,8 @@ export const EvervaultInput = forwardRef<
|
|
|
90
100
|
EvervaultInput,
|
|
91
101
|
EvervaultInputProps<Record<string, unknown>>
|
|
92
102
|
>(function EvervaultInput({ name, mask, ...props }, ref) {
|
|
103
|
+
const { validationMode } = useContext(EvervaultInputContext);
|
|
104
|
+
|
|
93
105
|
const inputRef = useForwardedInputRef(ref);
|
|
94
106
|
|
|
95
107
|
const methods = useFormContext();
|
|
@@ -108,10 +120,14 @@ export const EvervaultInput = forwardRef<
|
|
|
108
120
|
ref={mergeRefs(inputRef, field.ref)}
|
|
109
121
|
editable={!field.disabled && (props.editable ?? true)}
|
|
110
122
|
onBlur={(evt) => {
|
|
123
|
+
const shouldValidate =
|
|
124
|
+
validationMode === "onBlur" ||
|
|
125
|
+
validationMode === "onTouched" ||
|
|
126
|
+
validationMode === "all";
|
|
111
127
|
methods.setValue(field.name, field.value, {
|
|
112
128
|
shouldDirty: true,
|
|
113
129
|
shouldTouch: true,
|
|
114
|
-
shouldValidate
|
|
130
|
+
shouldValidate,
|
|
115
131
|
});
|
|
116
132
|
props.onBlur?.(evt);
|
|
117
133
|
}}
|
|
@@ -119,9 +135,13 @@ export const EvervaultInput = forwardRef<
|
|
|
119
135
|
maskAutoComplete={!!mask}
|
|
120
136
|
value={field.value}
|
|
121
137
|
onChangeText={(masked, unmasked) => {
|
|
138
|
+
const shouldValidate =
|
|
139
|
+
(validationMode === "onTouched" && fieldState.isTouched) ||
|
|
140
|
+
((validationMode === "onChange" || validationMode === "all") &&
|
|
141
|
+
(!!fieldState.error || fieldState.isTouched));
|
|
122
142
|
methods.setValue(field.name, unmasked, {
|
|
123
143
|
shouldDirty: true,
|
|
124
|
-
shouldValidate
|
|
144
|
+
shouldValidate,
|
|
125
145
|
});
|
|
126
146
|
}}
|
|
127
147
|
// Remove unwanted props
|