diff --git a/apps/modernization-ui/src/apps/patient/add/basic/phoneEmail/BasicPhoneEmailFields.spec.tsx b/apps/modernization-ui/src/apps/patient/add/basic/phoneEmail/BasicPhoneEmailFields.spec.tsx
index c3818fe45d..5b1338b142 100644
--- a/apps/modernization-ui/src/apps/patient/add/basic/phoneEmail/BasicPhoneEmailFields.spec.tsx
+++ b/apps/modernization-ui/src/apps/patient/add/basic/phoneEmail/BasicPhoneEmailFields.spec.tsx
@@ -72,4 +72,19 @@ describe('PhoneEmailEntryFields', () => {
}
});
});
+
+ it('should verify email field', async () => {
+ const { getByLabelText, getByText } = render();
+ const email = getByLabelText('Email');
+
+ userEvent.paste(email, 'invalid-email');
+ userEvent.tab();
+
+ await waitFor(() => {
+ const validationError = getByText(
+ 'Please enter Email as an email address (example: youremail@website.com).'
+ );
+ expect(validationError).toBeInTheDocument();
+ });
+ });
});
diff --git a/apps/modernization-ui/src/apps/patient/add/basic/phoneEmail/BasicPhoneEmailFields.tsx b/apps/modernization-ui/src/apps/patient/add/basic/phoneEmail/BasicPhoneEmailFields.tsx
index 6221f9bfc0..ee6fc768ce 100644
--- a/apps/modernization-ui/src/apps/patient/add/basic/phoneEmail/BasicPhoneEmailFields.tsx
+++ b/apps/modernization-ui/src/apps/patient/add/basic/phoneEmail/BasicPhoneEmailFields.tsx
@@ -2,7 +2,7 @@ import { Controller, useFormContext } from 'react-hook-form';
import { EntryFieldsProps } from 'design-system/entry';
import { maxLengthRule } from 'validation/entry';
import { Verification } from 'libs/verification';
-import { EmailField, validateEmail, PhoneNumberInputField, validPhoneNumberRule } from 'libs/demographics/contact';
+import { EmailField, maybeValidateEmail, PhoneNumberInputField, validPhoneNumberRule } from 'libs/demographics/contact';
import { BasicPhoneEmail } from '../entry';
import { MaskedTextInputField } from 'design-system/input/text';
@@ -13,7 +13,10 @@ const EMAIL_LABEL = 'Email';
type BasicPhoneEmailFieldsProps = EntryFieldsProps;
-export const BasicPhoneEmailFields = ({ orientation = 'horizontal' }: BasicPhoneEmailFieldsProps) => {
+export const BasicPhoneEmailFields = ({
+ orientation = 'horizontal',
+ sizing = 'compact'
+}: BasicPhoneEmailFieldsProps) => {
const { control } = useFormContext<{ phoneEmail: BasicPhoneEmail }>();
return (
@@ -25,12 +28,12 @@ export const BasicPhoneEmailFields = ({ orientation = 'horizontal' }: BasicPhone
render={({ field: { onChange, onBlur, value, name }, fieldState: { error } }) => (
)}
@@ -42,11 +45,11 @@ export const BasicPhoneEmailFields = ({ orientation = 'horizontal' }: BasicPhone
render={({ field: { onChange, onBlur, value, name }, fieldState: { error } }) => (
@@ -69,9 +72,9 @@ export const BasicPhoneEmailFields = ({ orientation = 'horizontal' }: BasicPhone
mask="____________________"
pattern="^\+?\d{1,20}$"
value={value}
- sizing="compact"
onBlur={onBlur}
onChange={onChange}
+ sizing={sizing}
orientation={orientation}
error={error?.message}
/>
@@ -85,11 +88,11 @@ export const BasicPhoneEmailFields = ({ orientation = 'horizontal' }: BasicPhone
render={({ field: { onChange, onBlur, value, name }, fieldState: { error } }) => (
@@ -102,17 +105,20 @@ export const BasicPhoneEmailFields = ({ orientation = 'horizontal' }: BasicPhone
rules={maxLengthRule(100, EMAIL_LABEL)}
render={({ field: { onChange, onBlur, value, name }, fieldState: { error } }) => (
(
{
- verify(value);
+ verify();
onBlur();
}}
onChange={onChange}
value={value}
+ sizing={sizing}
orientation={orientation}
error={error?.message}
warning={violation}
diff --git a/apps/modernization-ui/src/apps/patient/add/contactFields/ContactFields.tsx b/apps/modernization-ui/src/apps/patient/add/contactFields/ContactFields.tsx
index 2456859644..2d10e82a77 100644
--- a/apps/modernization-ui/src/apps/patient/add/contactFields/ContactFields.tsx
+++ b/apps/modernization-ui/src/apps/patient/add/contactFields/ContactFields.tsx
@@ -5,7 +5,7 @@ import FormCard from 'components/FormCard/FormCard';
import { Input } from 'components/FormInputs/Input';
import { maxLengthRule } from 'validation/entry';
import { Verification } from 'libs/verification';
-import { EmailField, validateEmail, PhoneNumberInputField, validPhoneNumberRule } from 'libs/demographics/contact';
+import { EmailField, maybeValidateEmail, PhoneNumberInputField, validPhoneNumberRule } from 'libs/demographics/contact';
const HOME_PHONE_LABEL = 'Home phone';
const WORK_PHONE_LABEL = 'Work phone';
@@ -123,13 +123,15 @@ export default function ContactFields({ id, title }: Props) {
rules={maxLengthRule(100, EMAIL_LABEL)}
render={({ field: { onChange, onBlur, value, name }, fieldState: { error } }) => (
(
{
- verify(value);
+ verify();
onBlur();
}}
onChange={onChange}
diff --git a/apps/modernization-ui/src/apps/patient/data/phoneEmail/PhoneEmailEntryFields.spec.tsx b/apps/modernization-ui/src/apps/patient/data/phoneEmail/PhoneEmailEntryFields.spec.tsx
index 168f3f2f76..b7d266f9d3 100644
--- a/apps/modernization-ui/src/apps/patient/data/phoneEmail/PhoneEmailEntryFields.spec.tsx
+++ b/apps/modernization-ui/src/apps/patient/data/phoneEmail/PhoneEmailEntryFields.spec.tsx
@@ -157,4 +157,19 @@ describe('when entering patient phone & email demographics', () => {
}
});
});
+
+ it('should verify email field', async () => {
+ const { getByLabelText, getByText } = render();
+ const email = getByLabelText('Email');
+
+ userEvent.paste(email, 'invalid-email');
+ userEvent.tab();
+
+ await waitFor(() => {
+ const validationError = getByText(
+ 'Please enter Email as an email address (example: youremail@website.com).'
+ );
+ expect(validationError).toBeInTheDocument();
+ });
+ });
});
diff --git a/apps/modernization-ui/src/apps/patient/data/phoneEmail/PhoneEmailEntryFields.tsx b/apps/modernization-ui/src/apps/patient/data/phoneEmail/PhoneEmailEntryFields.tsx
index cbde14579c..ba8645ae9e 100644
--- a/apps/modernization-ui/src/apps/patient/data/phoneEmail/PhoneEmailEntryFields.tsx
+++ b/apps/modernization-ui/src/apps/patient/data/phoneEmail/PhoneEmailEntryFields.tsx
@@ -4,9 +4,8 @@ import { SingleSelect } from 'design-system/select';
import { EntryFieldsProps } from 'design-system/entry';
import { maxLengthRule, validateRequiredRule } from 'validation/entry';
import { Verification } from 'libs/verification';
-import { EmailField, validateEmail, PhoneNumberInputField, validPhoneNumberRule } from 'libs/demographics/contact';
-import { MaskedTextInputField } from 'design-system/input/text';
-import { Input } from 'components/FormInputs/Input';
+import { EmailField, PhoneNumberInputField, validPhoneNumberRule, maybeValidateEmail } from 'libs/demographics/contact';
+import { MaskedTextInputField, TextInputField } from 'design-system/input/text';
import { PhoneEmailEntry } from 'apps/patient/data';
import { usePatientPhoneCodedValues } from 'apps/patient/profile/phoneEmail/usePatientPhoneCodedValues';
import { TextAreaField } from 'design-system/input/text/TextAreaField';
@@ -152,13 +151,15 @@ export const PhoneEmailEntryFields = ({ orientation = 'horizontal' }: PhoneEmail
rules={maxLengthRule(100, EMAIL_LABEL)}
render={({ field: { onChange, onBlur, value, name }, fieldState: { error } }) => (
(
{
- verify(value);
+ verify();
onBlur();
}}
onChange={onChange}
@@ -176,16 +177,13 @@ export const PhoneEmailEntryFields = ({ orientation = 'horizontal' }: PhoneEmail
name="url"
rules={maxLengthRule(100, URL_LABEL)}
render={({ field: { onChange, onBlur, value, name }, fieldState: { error } }) => (
-
)}
diff --git a/apps/modernization-ui/src/apps/patient/data/phoneEmail/entry.ts b/apps/modernization-ui/src/apps/patient/data/phoneEmail/entry.ts
index 08c33d9b9c..1ce2262397 100644
--- a/apps/modernization-ui/src/apps/patient/data/phoneEmail/entry.ts
+++ b/apps/modernization-ui/src/apps/patient/data/phoneEmail/entry.ts
@@ -19,12 +19,12 @@ const initial = (asOf: string = today()): Partial => ({
asOf,
type: undefined,
use: undefined,
- countryCode: '',
- phoneNumber: '',
- extension: '',
- email: '',
- url: '',
- comment: ''
+ countryCode: undefined,
+ phoneNumber: undefined,
+ extension: undefined,
+ email: undefined,
+ url: undefined,
+ comment: undefined
});
export { initial };
diff --git a/apps/modernization-ui/src/design-system/entry/index.ts b/apps/modernization-ui/src/design-system/entry/index.ts
index 0a57021171..a13ea234c9 100644
--- a/apps/modernization-ui/src/design-system/entry/index.ts
+++ b/apps/modernization-ui/src/design-system/entry/index.ts
@@ -1,7 +1,8 @@
-import { Orientation } from 'components/Entry';
+import { Orientation, Sizing } from 'components/Entry';
type EntryFieldsProps = {
orientation?: Orientation;
+ sizing?: Sizing;
};
export type { EntryFieldsProps };
diff --git a/apps/modernization-ui/src/libs/demographics/contact/email/maybeValidateEmail.ts b/apps/modernization-ui/src/libs/demographics/contact/email/maybeValidateEmail.ts
new file mode 100644
index 0000000000..381069a30a
--- /dev/null
+++ b/apps/modernization-ui/src/libs/demographics/contact/email/maybeValidateEmail.ts
@@ -0,0 +1,13 @@
+import { validateIfPresent } from 'validation';
+import { validateEmail } from './validateEmail';
+
+/**
+ * Validates that the given string represents a valid email. Any failed
+ * validations will include the name of the field being validated.
+ *
+ * @param {string} name The name of the field being validated
+ * @return {Validator}
+ */
+const maybeValidateEmail = (name: string) => validateIfPresent(validateEmail(name));
+
+export { maybeValidateEmail };
diff --git a/apps/modernization-ui/src/libs/demographics/contact/email/validateEmailRule.ts b/apps/modernization-ui/src/libs/demographics/contact/email/validateEmailRule.ts
index 82a54f153d..50f6596961 100644
--- a/apps/modernization-ui/src/libs/demographics/contact/email/validateEmailRule.ts
+++ b/apps/modernization-ui/src/libs/demographics/contact/email/validateEmailRule.ts
@@ -1,9 +1,8 @@
-import { validateIfPresent } from 'validation';
import { maxLengthRule } from 'validation/entry';
-import { validateEmail } from './validateEmail';
+import { maybeValidateEmail } from './maybeValidateEmail';
const validEmailRule = (name: string) => ({
- validate: validateIfPresent(validateEmail(name)),
+ validate: maybeValidateEmail(name),
...maxLengthRule(100, name)
});
diff --git a/apps/modernization-ui/src/libs/demographics/contact/index.ts b/apps/modernization-ui/src/libs/demographics/contact/index.ts
index 574fe487a5..3edaa0f8c5 100644
--- a/apps/modernization-ui/src/libs/demographics/contact/index.ts
+++ b/apps/modernization-ui/src/libs/demographics/contact/index.ts
@@ -3,4 +3,5 @@ export { validPhoneNumberRule } from './phone/validPhoneNumberRule';
export { EmailField } from './email/EmailField';
export { validateEmail } from './email/validateEmail';
+export { maybeValidateEmail } from './email/maybeValidateEmail';
export { validEmailRule } from './email/validateEmailRule';
diff --git a/apps/modernization-ui/src/libs/verification/Verification.tsx b/apps/modernization-ui/src/libs/verification/Verification.tsx
index 2b25702393..dfa9396425 100644
--- a/apps/modernization-ui/src/libs/verification/Verification.tsx
+++ b/apps/modernization-ui/src/libs/verification/Verification.tsx
@@ -1,20 +1,27 @@
-import { Validator } from 'validation';
-import { useVerification } from './useVerification';
+import { FieldPath, FieldValues } from 'react-hook-form';
+import { useVerification, VerificationOptions } from './useVerification';
-type VerificationRenderProps = {
+type VerificationRenderProps = {
violation?: string;
- verify: (value?: T) => void;
+ verify: () => void;
};
-type VerificationProps = {
- constraint: Validator;
- render: (result: VerificationRenderProps) => JSX.Element;
+type VerificationProps<
+ Values extends FieldValues = FieldValues,
+ Name extends FieldPath = FieldPath
+> = VerificationOptions & {
+ render: (result: VerificationRenderProps) => JSX.Element;
};
-const Verification = ({ constraint, render }: VerificationProps) => {
- const { violation, verify } = useVerification({ constraint });
+const Verification = = FieldPath>({
+ name,
+ control,
+ constraint,
+ render
+}: VerificationProps) => {
+ const { violation, verify } = useVerification({ name, control, constraint });
- return render({ violation, verify });
+ return render({ verify, violation: violation?.message });
};
export { Verification };
diff --git a/apps/modernization-ui/src/libs/verification/useVerification.ts b/apps/modernization-ui/src/libs/verification/useVerification.ts
index ca223a29d5..8127e079eb 100644
--- a/apps/modernization-ui/src/libs/verification/useVerification.ts
+++ b/apps/modernization-ui/src/libs/verification/useVerification.ts
@@ -1,36 +1,57 @@
-import { useCallback, useState } from 'react';
-import { validateIfPresent, Validator } from 'validation';
+import { useCallback, useEffect, useState } from 'react';
+import { Control, FieldPath, FieldPathValue, FieldValues, useWatch } from 'react-hook-form';
+import { Validator } from 'validation';
-type VerificationOptions = {
- constraint: Validator;
+type Violation = {
+ value: V;
+ message: string | undefined;
};
-type VerificationInteraction = {
- verify: (value?: T) => void;
- violation?: string;
+type VerificationOptions<
+ Values extends FieldValues = FieldValues,
+ Name extends FieldPath = FieldPath
+> = {
+ name: Name;
+ control: Control;
+ constraint: Validator>;
+};
+
+type VerificationInteraction = {
+ verify: () => void;
+ violation?: Violation;
};
/**
- * Applies a constraint to a value and provides any violations if the constraint fails.
+ * Applies a constraint to a value providing any violations if the constraint fails.
*
- * @param {VerificationOptions} param0 The options for the verification, including the constraint to apply for verification.
+ * @param {VerificationOptions} param0 The options for the verification, including
+ * - the name of the field that is being verified.
+ * - the control of the form that is being verified.
+ * - the constraint to apply for verification.
* @return {VerificationInteraction} The interactions with the hook providing the verify function and any violations.
*/
-const useVerification = ({ constraint }: VerificationOptions): VerificationInteraction => {
- const [violation, setViolation] = useState();
-
- const verify = useCallback(
- (value?: V) => {
- const result = validateIfPresent(constraint)(value);
-
- if (typeof result === 'string') {
- setViolation(result);
- } else {
- setViolation(undefined);
- }
- },
- [setViolation, constraint]
- );
+const useVerification = = FieldPath>({
+ name,
+ control,
+ constraint
+}: VerificationOptions): VerificationInteraction => {
+ const [violation, setViolation] = useState>>();
+
+ const current = useWatch({ control, name });
+
+ useEffect(() => {
+ setViolation((existing) => (current !== existing?.value ? undefined : existing));
+ }, [current, setViolation]);
+
+ const verify = useCallback(() => {
+ const result = constraint(current);
+
+ if (typeof result === 'string') {
+ setViolation({ value: current, message: result });
+ } else {
+ setViolation(undefined);
+ }
+ }, [setViolation, constraint, current]);
return {
verify,
@@ -39,3 +60,4 @@ const useVerification = ({ constraint }: VerificationOptions): Verificatio
};
export { useVerification };
+export type { VerificationOptions };
diff --git a/apps/modernization-ui/src/libs/verification/userVerification.spec.ts b/apps/modernization-ui/src/libs/verification/userVerification.spec.ts
deleted file mode 100644
index 3c7060b674..0000000000
--- a/apps/modernization-ui/src/libs/verification/userVerification.spec.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import { renderHook } from '@testing-library/react-hooks';
-import { useVerification } from './useVerification';
-import { act } from '@testing-library/react';
-
-describe('when verifying values', () => {
- it('should provide violations when the constraint fails', () => {
- const { result } = renderHook(() => useVerification({ constraint: () => 'value is unverified.' }));
-
- act(() => {
- result.current.verify('value');
- });
-
- expect(result.current.violation).toContain('value is unverified');
- });
-
- it('should not provide violations until asked to verify', () => {
- const { result } = renderHook(() => useVerification({ constraint: () => 'value is unverified.' }));
-
- expect(result.current.violation).toBeUndefined();
- });
-
- it('should not provide violations when the constraint is met', () => {
- const { result } = renderHook(() => useVerification({ constraint: () => true }));
-
- act(() => {
- result.current.verify('value');
- });
-
- expect(result.current.violation).toBeUndefined();
- });
-});