-
Notifications
You must be signed in to change notification settings - Fork 166
LG-11139: question upon exit #9392
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
9037055
3803cb1
6eba519
15855ea
b25fd18
7854f0a
c889c41
b2a2395
ef354fe
c1cd972
525f5e0
458cd54
c72ab75
7732f63
51631a7
ca6dd38
bcf4c4b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| import { render } from '@testing-library/react'; | ||
| 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(); | ||
| }); | ||
| }); | ||
| }); | ||
| 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; |
| 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(); | ||
| }); | ||
| }); | ||
| }); |
| 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> { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
| 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 |
|---|---|---|
| @@ -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 | ||
| } | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
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. 👍🏻