Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/assets/stylesheets/components/all.scss
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
@import 'card';
@import 'container';
@import 'file-input';
@import 'form-steps';
@import 'footer';
@import 'form';
@import 'hr';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { useContext, useState } from 'react';
import { useDidUpdateEffect } from '@18f/identity-react-hooks';
import { FormStepsContext } from '@18f/identity-form-steps';
import FailedCaptureAttemptsContext from '../context/failed-capture-attempts';
import AnalyticsContext from '../context/analytics';
import CallbackOnMount from './callback-on-mount';
import CaptureAdvice from './capture-advice';
import { FormStepsContext } from './form-steps';

/** @typedef {import('react').ReactNode} ReactNode */

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useState, useMemo, useContext } from 'react';
import { Alert } from '@18f/identity-components';
import { useI18n } from '@18f/identity-react-i18n';
import FormSteps from './form-steps';
import { FormSteps, PromptOnNavigate } from '@18f/identity-form-steps';
import { UploadFormEntriesError } from '../services/upload';
import DocumentsStep, { documentsStepValidator } from './documents-step';
import SelfieStep, { selfieStepValidator } from './selfie-step';
Expand All @@ -14,12 +14,10 @@ import { RetrySubmissionError } from './submission-complete';
import { BackgroundEncryptedUploadError } from '../higher-order/with-background-encrypted-upload';
import SuspenseErrorBoundary from './suspense-error-boundary';
import SubmissionInterstitial from './submission-interstitial';
import PromptOnNavigate from './prompt-on-navigate';
import withProps from '../higher-order/with-props';

/** @typedef {import('react').ReactNode} ReactNode */
/** @typedef {import('./form-steps').FormStep} FormStep */
/** @typedef {import('../context/upload').UploadFieldError} UploadFieldError */
/** @typedef {import('@18f/identity-form-steps').FormStep} FormStep */

/**
* Returns a new object with specified keys removed.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { t } from '@18f/identity-i18n';
import { FormError } from '@18f/identity-form-steps';
import AcuantCapture from './acuant-capture';
import { FormError } from './form-steps';

/** @typedef {import('./form-steps').FormStepError<*>} FormStepError */
/** @typedef {import('./form-steps').RegisterFieldCallback} RegisterFieldCallback */
/** @typedef {import('./form-steps').OnErrorCallback} OnErrorCallback */
/** @typedef {import('@18f/identity-form-steps').FormStepError<*>} FormStepError */
/** @typedef {import('@18f/identity-form-steps').RegisterFieldCallback} RegisterFieldCallback */
/** @typedef {import('@18f/identity-form-steps').OnErrorCallback} OnErrorCallback */

/**
* @typedef DocumentSideAcuantCaptureProps
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useContext } from 'react';
import { useI18n } from '@18f/identity-react-i18n';
import { FormStepsContinueButton } from './form-steps';
import { FormStepsContinueButton } from '@18f/identity-form-steps';
import DocumentSideAcuantCapture from './document-side-acuant-capture';
import DeviceContext from '../context/device';
import withBackgroundEncryptedUpload from '../higher-order/with-background-encrypted-upload';
Expand Down Expand Up @@ -37,7 +37,7 @@ function documentsStepValidator(value = {}) {
}

/**
* @param {import('./form-steps').FormStepComponentProps<DocumentsStepValue>} props Props object.
* @param {import('@18f/identity-form-steps').FormStepComponentProps<DocumentsStepValue>} props Props object.
*/
function DocumentsStep({
value = {},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useContext, useState } from 'react';
import { hasMediaAccess } from '@18f/identity-device';
import { useI18n } from '@18f/identity-react-i18n';
import { useDidUpdateEffect } from '@18f/identity-react-hooks';
import { FormStepsContext, FormStepsContinueButton } from './form-steps';
import { FormStepsContext, FormStepsContinueButton } from '@18f/identity-form-steps';
import DeviceContext from '../context/device';
import DocumentSideAcuantCapture from './document-side-acuant-capture';
import AcuantCapture from './acuant-capture';
Expand Down Expand Up @@ -54,7 +54,7 @@ function reviewIssuesStepValidator(value = {}) {
}

/**
* @param {import('./form-steps').FormStepComponentProps<ReviewIssuesStepValue> & {
* @param {import('@18f/identity-form-steps').FormStepComponentProps<ReviewIssuesStepValue> & {
* remainingAttempts: number,
* captureHints: boolean,
* }} props Props object.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useContext } from 'react';
import { hasMediaAccess } from '@18f/identity-device';
import { useI18n } from '@18f/identity-react-i18n';
import { FormStepsContinueButton } from './form-steps';
import { FormStepsContinueButton } from '@18f/identity-form-steps';
import DeviceContext from '../context/device';
import AcuantCapture from './acuant-capture';
import SelfieCapture from './selfie-capture';
Expand All @@ -23,7 +23,7 @@ function selfieStepValidator(value = {}) {
}

/**
* @param {import('./form-steps').FormStepComponentProps<SelfieStepValue>} props Props object.
* @param {import('@18f/identity-form-steps').FormStepComponentProps<SelfieStepValue>} props Props object.
*/
function SelfieStep({
value = {},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { useContext } from 'react';
import { t } from '@18f/identity-i18n';
import { FormError } from '@18f/identity-form-steps';
import UploadContext from '../context/upload';
import AnalyticsContext from '../context/analytics';
import { FormError } from '../components/form-steps';

/**
* @typedef {import('../components/form-steps').FormStepComponentProps<V>} FormStepComponentProps
* @typedef {import('@18f/identity-form-steps').FormStepComponentProps<V>} FormStepComponentProps
* @template V
*/

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { FormError } from '../components/form-steps';
import { FormError } from '@18f/identity-form-steps';

/** @typedef {import('../context/upload').UploadSuccessResponse} UploadSuccessResponse */
/** @typedef {import('../context/upload').UploadErrorResponse} UploadErrorResponse */
Expand Down
1 change: 0 additions & 1 deletion app/javascript/packages/document-capture/styles.scss
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
@import 'identity-style-guide/dist/assets/scss/packages/required';
@import './components/acuant-capture';
@import './components/acuant-capture-canvas';
@import './components/form-steps';
@import './components/full-screen';
@import './components/review-issues-step';
@import './components/selfie-capture';
19 changes: 19 additions & 0 deletions app/javascript/packages/form-steps/form-error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export interface FormErrorOptions {
/**
* Whether error message is to be presented in a context which accommodates a detailed
* text description.
*/
isDetail?: boolean;
}

class FormError extends Error {
isDetail: boolean;

constructor(options?: { isDetail: boolean }) {
super();

this.isDetail = Boolean(options?.isDetail);
}
}

export default FormError;
26 changes: 26 additions & 0 deletions app/javascript/packages/form-steps/form-steps-context.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { createContext } from 'react';

interface FormStepsContextValue {
/**
* Whether the current step is the last step in the flow.
*/
isLastStep: boolean;

/**
* Whether the user can proceed to the next step.
*/
canContinueToNextStep: boolean;

/**
* Callback invoked when content is reset in a page transition.
*/
onPageTransition: () => void;
}

const FormStepsContext = createContext({
isLastStep: true,
canContinueToNextStep: true,
onPageTransition: () => {},
} as FormStepsContextValue);

export default FormStepsContext;
23 changes: 23 additions & 0 deletions app/javascript/packages/form-steps/form-steps-continue-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { useContext } from 'react';
import { Button } from '@18f/identity-components';
import { useI18n } from '@18f/identity-react-i18n';
import FormStepsContext from './form-steps-context';

function FormStepsContinueButton() {
const { t } = useI18n();
const { canContinueToNextStep, isLastStep } = useContext(FormStepsContext);

return (
<Button
type="submit"
isBig
isWide
className="display-block margin-y-5"
isVisuallyDisabled={!canContinueToNextStep}
>
{isLastStep ? t('forms.buttons.submit.default') : t('forms.buttons.continue')}
</Button>
);
}

export default FormStepsContinueButton;
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
import { useContext } from 'react';
import { render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { waitFor } from '@testing-library/dom';
import sinon from 'sinon';
import PageHeading from '@18f/identity-document-capture/components/page-heading';
import FormSteps, {
FormStepsContext,
getStepIndexByName,
FormStepsContinueButton,
} from '@18f/identity-document-capture/components/form-steps';
import { toFormEntryError } from '@18f/identity-document-capture/services/upload';
import { render } from '../../../support/document-capture';
import { useSandbox } from '../../../support/sinon';

describe('document-capture/components/form-steps', () => {
const { spy } = useSandbox();
import FormSteps, { FormStepComponentProps, getStepIndexByName } from './form-steps';
import FormError from './form-error';
import FormStepsContext from './form-steps-context';
import FormStepsContinueButton from './form-steps-continue-button';

interface StepValues {
secondInputOne?: string;

secondInputTwo?: string;

changed?: boolean;
}

describe('FormSteps', () => {
const { spy } = sinon.createSandbox();

const STEPS = [
{
Expand All @@ -29,7 +34,13 @@ describe('document-capture/components/form-steps', () => {
},
{
name: 'second',
form: ({ value = {}, errors = [], onChange, onError, registerField }) => (
form: ({
value = {},
errors = [],
onChange,
onError,
registerField,
}: FormStepComponentProps<StepValues>) => (
<>
<PageHeading>Second Title</PageHeading>
<input
Expand Down Expand Up @@ -232,8 +243,8 @@ describe('document-capture/components/form-steps', () => {
window.history.forward();

expect(await findByText('Second Title')).to.be.ok();
expect(getByLabelText('Second Input One').value).to.equal('one');
expect(getByLabelText('Second Input Two').value).to.equal('two');
expect((getByLabelText('Second Input One') as HTMLInputElement).value).to.equal('one');
expect((getByLabelText('Second Input Two') as HTMLInputElement).value).to.equal('two');
expect(window.location.hash).to.equal('#step=second');
});

Expand Down Expand Up @@ -286,7 +297,7 @@ describe('document-capture/components/form-steps', () => {
);

userEvent.click(getByText('forms.buttons.continue'));
const input = getByLabelText('Second Input One');
const input = getByLabelText('Second Input One') as HTMLInputElement;

expect(input.value).to.equal('prefilled');
});
Expand All @@ -301,14 +312,14 @@ describe('document-capture/components/form-steps', () => {
expect(document.activeElement).to.equal(getByLabelText('Second Input One'));
expect(container.querySelectorAll('[data-is-error]')).to.have.lengthOf(2);

await userEvent.type(document.activeElement, 'one');
await userEvent.type(document.activeElement as HTMLInputElement, 'one');
expect(container.querySelectorAll('[data-is-error]')).to.have.lengthOf(1);

userEvent.click(getByText('forms.buttons.continue'));
expect(document.activeElement).to.equal(getByLabelText('Second Input Two'));
expect(container.querySelectorAll('[data-is-error]')).to.have.lengthOf(1);

await userEvent.type(document.activeElement, 'two');
await userEvent.type(document.activeElement as HTMLInputElement, 'two');
expect(container.querySelectorAll('[data-is-error]')).to.have.lengthOf(0);
userEvent.click(getByText('forms.buttons.continue'));

Expand All @@ -335,11 +346,11 @@ describe('document-capture/components/form-steps', () => {
initialActiveErrors={[
{
field: 'unknown',
error: toFormEntryError({ field: 'unknown', message: 'An unknown error occurred' }),
error: new FormError(),
},
{
field: 'secondInputOne',
error: toFormEntryError({ field: 'secondInputOne', message: 'Bad input' }),
error: new FormError(),
},
]}
onComplete={onComplete}
Expand Down Expand Up @@ -384,7 +395,7 @@ describe('document-capture/components/form-steps', () => {
const steps = [STEPS[1]];

const { getByLabelText } = render(<FormSteps steps={steps} />);
const inputOne = getByLabelText('Second Input One');
const inputOne = getByLabelText('Second Input One') as HTMLInputElement;
inputOne.setCustomValidity('uh oh');
userEvent.type(inputOne, 'one');

Expand All @@ -404,7 +415,7 @@ describe('document-capture/components/form-steps', () => {
it('provides context', () => {
const { getByTestId, getByRole, getByLabelText } = render(<FormSteps steps={STEPS} />);

expect(JSON.parse(getByTestId('context-value').textContent)).to.deep.equal({
expect(JSON.parse(getByTestId('context-value').textContent!)).to.deep.equal({
isLastStep: false,
canContinueToNextStep: true,
});
Expand All @@ -415,7 +426,7 @@ describe('document-capture/components/form-steps', () => {
// Trigger validation errors on second step.
userEvent.click(getByRole('button', { name: 'forms.buttons.continue' }));
expect(window.location.hash).to.equal('#step=second');
expect(JSON.parse(getByTestId('context-value').textContent)).to.deep.equal({
expect(JSON.parse(getByTestId('context-value').textContent!)).to.deep.equal({
isLastStep: false,
canContinueToNextStep: false,
});
Expand All @@ -425,7 +436,7 @@ describe('document-capture/components/form-steps', () => {

userEvent.click(getByRole('button', { name: 'forms.buttons.continue' }));
expect(window.location.hash).to.equal('#step=last');
expect(JSON.parse(getByTestId('context-value').textContent)).to.deep.equal({
expect(JSON.parse(getByTestId('context-value').textContent!)).to.deep.equal({
isLastStep: true,
canContinueToNextStep: true,
});
Expand Down
Loading