-
-
+ {/* Because the Modal renders into a portal outside the flow form, inputs would not normally
+ emit a submit event. We can reinstate the expected behavior with an empty form. A submit
+ event will bubble through the React portal boundary and be handled by FormSteps. Because
+ the form is not rendered in the same DOM hierarchy, it is not invalid nesting. */}
+
+
>
);
diff --git a/app/javascript/packages/verify-flow/steps/personal-key-confirm/personal-key-input.spec.tsx b/app/javascript/packages/verify-flow/steps/personal-key-confirm/personal-key-input.spec.tsx
index 74369677bdb..70d63c24fb6 100644
--- a/app/javascript/packages/verify-flow/steps/personal-key-confirm/personal-key-input.spec.tsx
+++ b/app/javascript/packages/verify-flow/steps/personal-key-confirm/personal-key-input.spec.tsx
@@ -48,4 +48,18 @@ describe('PersonalKeyInput', () => {
await userEvent.type(input, '12345');
expect(input.value).to.equal('1234-1234-1234-1234');
});
+
+ it('validates the input value against the expected value', async () => {
+ const { getByRole } = render(
);
+
+ const input = getByRole('textbox') as HTMLInputElement;
+
+ await userEvent.type(input, '0000-0000-0000-000');
+ input.checkValidity();
+ expect(input.validationMessage).to.equal('users.personal_key.confirmation_error');
+
+ await userEvent.type(input, '0');
+ input.checkValidity();
+ expect(input.validationMessage).to.be.empty();
+ });
});
diff --git a/app/javascript/packages/verify-flow/steps/personal-key-confirm/personal-key-input.tsx b/app/javascript/packages/verify-flow/steps/personal-key-confirm/personal-key-input.tsx
index 2970a35221b..478244e64bb 100644
--- a/app/javascript/packages/verify-flow/steps/personal-key-confirm/personal-key-input.tsx
+++ b/app/javascript/packages/verify-flow/steps/personal-key-confirm/personal-key-input.tsx
@@ -1,22 +1,52 @@
-import { forwardRef } from 'react';
+import { forwardRef, useCallback } from 'react';
+import type { ForwardedRef } from 'react';
import Cleave from 'cleave.js/react';
import { t } from '@18f/identity-i18n';
+import { ValidatedField } from '@18f/identity-validated-field';
+import type { ValidatedFieldValidator } from '@18f/identity-validated-field';
+
+interface PersonalKeyInputProps {
+ /**
+ * The correct personal key to validate against.
+ */
+ expectedValue?: string;
+
+ /**
+ * Callback invoked when the value of the input has changed.
+ */
+ onChange?: (nextValue: string) => void;
+}
+
+function PersonalKeyInput(
+ { expectedValue, onChange = () => {} }: PersonalKeyInputProps,
+ ref: ForwardedRef
,
+) {
+ const validate = useCallback(
+ (value) => {
+ if (expectedValue && value !== expectedValue) {
+ throw new Error(t('users.personal_key.confirmation_error'));
+ }
+ },
+ [expectedValue],
+ );
-function PersonalKeyInput(_props, ref) {
return (
-
+
+ typeof ref === 'function' && ref(cleaveRef)}
+ aria-label={t('forms.personal_key.confirmation_label')}
+ autoComplete="off"
+ className="width-full field font-family-mono text-uppercase"
+ pattern="[a-zA-Z0-9-]+"
+ spellCheck={false}
+ type="text"
+ onInput={(event) => onChange((event.target as HTMLInputElement).value)}
+ />
+
);
}