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
10 changes: 10 additions & 0 deletions app/controllers/idv/cancellations_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,16 @@ def destroy
end
end

def exit
analytics.idv_cancellation_confirmed(step: params[:step])
cancel_session
if hybrid_session?
render :destroy
else
redirect_to cancelled_redirect_path
end
end

private

def barcode_step?
Expand Down
49 changes: 49 additions & 0 deletions app/javascript/packages/components/checkbox.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { render } from '@testing-library/react';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests look really thorough here. 👍🏻

import Checkbox from './checkbox';

describe('Checkbox', () => {
it('renders given checkbox', () => {
const { getByRole, getByText } = render(
<Checkbox id="checkbox1" label="A checkbox" labelDescription="A checkbox for testing" />,
);

const checkbox = getByRole('checkbox');
expect(checkbox.classList.contains('usa-checkbox__input')).to.be.true();
expect(checkbox.classList.contains('usa-button__input-title')).to.be.false();
expect(checkbox.id).to.eq('checkbox1');

const label = getByText('A checkbox');
expect(label).to.be.ok();
expect(label.classList.contains('usa-checkbox__label')).to.be.true();
expect(label.getAttribute('for')).eq('checkbox1');

const labelDescription = getByText('A checkbox for testing');
expect(labelDescription).to.be.ok();
expect(labelDescription.classList.contains('usa-checkbox__label-description')).to.be.true();
});

context('with isTitle', () => {
it('renders with correct style', () => {
const { getByRole } = render(
<Checkbox isTitle label="A checkbox" labelDescription="A checkbox for testing" />,
);
const checkbox = getByRole('checkbox');
expect(checkbox.classList.contains('usa-button__input-title')).to.be.true();
});
});

context('with hint', () => {
it('renders hint', () => {
const { getByText } = render(
<Checkbox
isTitle
label="A checkbox"
labelDescription="A checkbox for testing"
hint="Please check this box"
/>,
);
const hint = getByText('Please check this box');
expect(hint).to.be.ok();
});
});
});
74 changes: 74 additions & 0 deletions app/javascript/packages/components/checkbox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import type { InputHTMLAttributes } from 'react';
import { useInstanceId } from '@18f/identity-react-hooks';

export interface CheckboxProps extends InputHTMLAttributes<HTMLInputElement> {
/**
* Whether checkbox is considered title, a box around it, with optional description for the label
*/
isTitle?: boolean;
/**
* Whether is focused, a focus box around the checkbox
*/
isFocus?: boolean;
/**
* Optional id for the element
*/
id?: string;
/**
* Optional additional class names.
*/
className?: string;
/**
* Label text for the checkbox
*/
label: string;
/**
* Optional description for the label, used with isTitle
*/
labelDescription?: string;
/**
* Muted explainer text sitting below the label.
*/
hint?: string;
}

function Checkbox({
id,
isTitle,
isFocus,
className,
label,
labelDescription,
hint,
...inputProps
}: CheckboxProps) {
const instanceId = useInstanceId();
const inputId = id ?? `check-input-${instanceId}`;
const hintId = id ?? `check-input-hint-${instanceId}`;
const classes = [
'usa-checkbox__input',
isTitle && 'usa-button__input-title',
isFocus && 'usa-focus',
className,
]
.filter(Boolean)
.join(' ');

return (
<div className="usa-checkbox">
<input id={inputId} className={classes} type="checkbox" {...inputProps} />
<label className="usa-checkbox__label" htmlFor={inputId}>
{label}
{labelDescription && (
<span className="usa-checkbox__label-description">{labelDescription}</span>
)}
</label>
{hint && (
<div id={hintId} className="usa-hint">
{hint}
</div>
)}
</div>
);
}
export default Checkbox;
30 changes: 30 additions & 0 deletions app/javascript/packages/components/field-set-spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { render } from '@testing-library/react';
import FieldSet from './field-set';

describe('FieldSet', () => {
it('renders given fieldset', () => {
const { getByRole, getByText } = render(
<FieldSet>
<p>Inner text</p>
</FieldSet>,
);
const fieldSet = getByRole('group');
expect(fieldSet).to.be.ok();
expect(fieldSet.classList.contains('usa-fieldset')).to.be.true();

const child = getByText('Inner text');
expect(child).to.be.ok();
});
context('with legend', () => {
it('renders legend', () => {
const { getByText } = render(
<FieldSet legend="Legend text">
<p>Inner text</p>
</FieldSet>,
);
const legend = getByText('Legend text');
expect(legend).to.be.ok();
expect(legend.classList.contains('usa-legend')).to.be.true();
});
});
});
21 changes: 21 additions & 0 deletions app/javascript/packages/components/field-set.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { FieldsetHTMLAttributes, ReactNode } from 'react';

export interface FieldSetProps extends FieldsetHTMLAttributes<HTMLFieldSetElement> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really nice work with these reusable components 👍

Interesting on the difference in capitalization between "Fieldset" and "FieldSet", even between React and TypeScript's built-in DOM typings. I'm not sure which is "correct", though I think I'd agree with you that its spelled-out "field set" as two words would make sense capitalized as you've proposed.

/**
* Footer contents.
*/
children: ReactNode;

legend?: string;
}

function FieldSet({ legend, children }: FieldSetProps) {
return (
<fieldset className="usa-fieldset">
{legend && <legend className="usa-legend">{legend}</legend>}
{children}
</fieldset>
);
}

export default FieldSet;
3 changes: 3 additions & 0 deletions app/javascript/packages/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,11 @@ export { default as StatusPage } from './status-page';
export { default as Tag } from './tag';
export { default as TextInput } from './text-input';
export { default as TroubleshootingOptions } from './troubleshooting-options';
export { default as Checkbox } from './checkbox';
export { default as FieldSet } from './field-set';

export type { ButtonProps } from './button';
export type { FullScreenRefHandle } from './full-screen';
export type { LinkProps } from './link';
export type { TextInputProps } from './text-input';
export type { CheckboxProps } from './checkbox';
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { Tag, Checkbox, FieldSet, Button, Link } from '@18f/identity-components';
import { useI18n } from '@18f/identity-react-i18n';
import { useContext, useState } from 'react';
import FlowContext from '@18f/identity-verify-flow/context/flow-context';
import formatHTML from '@18f/identity-react-i18n/format-html';
import { addSearchParams, forceRedirect, Navigate } from '@18f/identity-url';
import { getConfigValue } from '@18f/identity-config';
import AnalyticsContext from '../context/analytics';
import { ServiceProviderContext } from '../context';

function formatContentHtml({ msg, url }) {
return formatHTML(msg, {
a: ({ children }) => (
<Link href={url} isExternal={false}>
{children}
</Link>
),
strong: ({ children }) => <strong>{children}</strong>,
});
}

export interface DocumentCaptureAbandonProps {
navigate?: Navigate;
}

function DocumentCaptureAbandon({ navigate }: DocumentCaptureAbandonProps) {
const { t } = useI18n();
const { trackEvent } = useContext(AnalyticsContext);
const { currentStep, exitURL, cancelURL } = useContext(FlowContext);
const { name: spName } = useContext(ServiceProviderContext);
const appName = getConfigValue('appName');
const header = <h2 className="h3">{t('doc_auth.exit_survey.header')}</h2>;

const content = (
<p>
{formatContentHtml({
msg: spName?.trim()
? t('doc_auth.exit_survey.content_html', {
sp_name: spName,
app_name: appName,
})
: t('doc_auth.exit_survey.content_nosp_html', {
app_name: appName,
}),
url: addSearchParams(spName?.trim() ? exitURL : cancelURL, {
step: currentStep,
location: 'optional_question',
}),
})}
</p>
);
const optionalTag = (
<Tag isBig={false} isInformative>
{t('doc_auth.exit_survey.optional.tag', { app_name: appName })}
</Tag>
);
const optionalText = (
<p className="margin-top-2">
<strong>{t('doc_auth.exit_survey.optional.content', { app_name: appName })}</strong>
</p>
);

const idTypeLabels = [
t('doc_auth.exit_survey.optional.id_types.us_passport'),
t('doc_auth.exit_survey.optional.id_types.resident_card'),
t('doc_auth.exit_survey.optional.id_types.military_id'),
t('doc_auth.exit_survey.optional.id_types.tribal_id'),
t('doc_auth.exit_survey.optional.id_types.voter_registration_card'),
t('doc_auth.exit_survey.optional.id_types.other'),
];

const allIdTypeOptions = [
{ name: 'us_passport', checked: false },
{ name: 'resident_card', checked: false },
{ name: 'military_id', checked: false },
{ name: 'tribal_id', checked: false },
{ name: 'voter_registration_card', checked: false },
{ name: 'other', checked: false },
];

const [idTypeOptions, setIdTypeOptions] = useState(allIdTypeOptions);

const updateCheckStatus = (index: number) => {
setIdTypeOptions(
idTypeOptions.map((idOption, currentIndex) =>
currentIndex === index ? { ...idOption, checked: !idOption.checked } : { ...idOption },
),
);
};

const checkboxes = (
<>
{idTypeOptions.map((idType, idx) => (
<Checkbox
key={idType.name}
name={idType.name}
value={idType.name}
label={idTypeLabels[idx]}
onChange={() => updateCheckStatus(idx)}
/>
))}
</>
);

const handleExit = () => {
trackEvent('IdV: exit optional questions', { ids: idTypeOptions });
forceRedirect(
addSearchParams(spName ? exitURL : cancelURL, {
step: currentStep,
location: 'optional_question',
}),
navigate,
);
};

return (
<>
{header}
{content}
<div className="document-capture-optional-questions">
{optionalTag}
{optionalText}
<FieldSet legend={t('doc_auth.exit_survey.optional.legend')}>{checkboxes}</FieldSet>
<Button isOutline className="margin-top-3" onClick={handleExit}>
{t('doc_auth.exit_survey.optional.button', { app_name: appName })}
</Button>
<div className="usa-prose margin-top-3">
{t('idv.legal_statement.information_collection')}
</div>
</div>
</>
);
}

export default DocumentCaptureAbandon;
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { useI18n } from '@18f/identity-react-i18n';
import UnknownError from './unknown-error';
import TipList from './tip-list';
import DocumentSideAcuantCapture from './document-side-acuant-capture';
import DocumentCaptureAbandon from './document-capture-abandon';

interface DocumentCaptureReviewIssuesProps {
isFailedDocType: boolean;
Expand Down Expand Up @@ -78,6 +79,7 @@ function DocumentCaptureReviewIssues({
/>
))}
<FormStepsButton.Submit />
<DocumentCaptureAbandon />
<Cancel />
</>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import DocumentSideAcuantCapture from './document-side-acuant-capture';
import DeviceContext from '../context/device';
import UploadContext from '../context/upload';
import TipList from './tip-list';
import DocumentCaptureAbandon from './document-capture-abandon';

/**
* @typedef {'front'|'back'} DocumentSide
Expand Down Expand Up @@ -70,6 +71,8 @@ function DocumentsStep({
/>
))}
{isLastStep ? <FormStepsButton.Submit /> : <FormStepsButton.Continue />}

<DocumentCaptureAbandon />
<Cancel />
</>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
@use 'uswds-core' as *;

.document-capture-optional-questions {
margin-top: 1.5em;
.usa-fieldset {
margin-top: 0;
.usa-legend {
text-transform: none;
margin-top: 0;
font-size: 1rem;
line-height: 1.4;
display: block;
border-bottom: none;
padding-bottom: 0;
padding-top: 0;
}
.usa-checkbox {
.usa-checkbox__label {
display: block; //collapsing margin
}
}
}
}
Loading