Skip to content
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

Add employer page #56

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
Open
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
16 changes: 16 additions & 0 deletions src/components/form/fields/Address/Address.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
.city {
width: 10rem !important;
margin-right: 2rem !important;
}

.city_error {
padding-right: 2rem;
}

.state {
width: 8rem !important;
}

.zipcode {
width: 8rem !important;
}
Comment on lines +1 to +16
Copy link
Contributor

Choose a reason for hiding this comment

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

Are the !importants really needed? Its usually code smell, so definitely want to avoid that especially this early on a project where we have the luxury and time of doing things right, even if it means banging heads against scss (again 😢)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ahh I recall we added this back on DOL for a reason, but to be honest I don't remember what it is I just kept it the same. I would assume that we have less stuff here so probably the same conflict won't occur.

68 changes: 68 additions & 0 deletions src/components/form/fields/Address/Address.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { yupResolver } from '@hookform/resolvers/yup'
import { Meta, StoryFn } from '@storybook/react'
import { noop } from 'helpers/noop/noop'
import { FormProvider, useForm } from 'react-hook-form'
import { useTranslation } from 'react-i18next'
import * as yup from 'yup'

import Address, { IAddressLabels } from './Address'

export default {
title: 'Components/Form/Address',
component: Address,
} as Meta<typeof Address>

const Template: StoryFn<typeof Address> = (args) => {
const { t: tCommon } = useTranslation('common')
type Address = {
address: IAddressLabels
}
const initialValues = {
address: {
address1: '',
address2: '',
city: '',
state: '',
zipcode: '',
},
}

const validationSchema = yup.object().shape({
address: yup.object().shape({
address1: yup.string().required(tCommon('validation.required')),
address2: yup.string().optional(),
city: yup.string().required(tCommon('validation.required')),
state: yup.string().required(tCommon('validation.required')),

zipcode: yup
.string()
// eslint-disable-next-line security/detect-unsafe-regex
.matches(/^\d{5}(-\d{4})?$/, tCommon('validation.notZipCode'))
.required(tCommon('validation.required')),
}),
})
Comment on lines +30 to +43
Copy link
Contributor

Choose a reason for hiding this comment

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

Does this need to be defined again here when you have src/validations/address.ts?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

They aren't the same schemas. I kept how we had it on DOL because I liked that the address only storybook had its own generic schema just for display and then when we used the object we made it more customized, min/max length. Any objection to keeping it that way?


const hookFormMethods = useForm<Address>({
defaultValues: initialValues,
resolver: yupResolver(validationSchema),
})

return (
<FormProvider {...hookFormMethods}>
<form onSubmit={noop}>
<Address basename={args.basename} optAddress2={args.optAddress2} />
</form>
</FormProvider>
)
}

export const Default = Template.bind({})
Default.args = {
basename: 'address',
}

export const WithSecondAddress = Template.bind({})
WithSecondAddress.args = {
basename: 'address',
optAddress2: true,
}
92 changes: 92 additions & 0 deletions src/components/form/fields/Address/Address.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { FormGroup } from '@trussworks/react-uswds'
import { ChangeEventHandler } from 'react'
import { useTranslation } from 'react-i18next'

import { StateAbbrev, StatesDropdown } from '../StatesDropdown/StatesDropdown'
import TextField from '../TextField/TextField'
import styles from './Address.module.scss'

export interface IAddressLabels {
address: string
address2?: string
city: string
state: string
zipcode: string
}

export interface IAddressProps {
labels?: IAddressLabels
basename: string
optAddress2?: boolean
stateSlice?: StateAbbrev[]
onChange?: ChangeEventHandler<HTMLInputElement | HTMLSelectElement>
}

export const Address = ({
labels,
basename,
stateSlice,
optAddress2,
onChange,
}: IAddressProps) => {
const { t } = useTranslation('common')
const defaultLabels: IAddressLabels = {
address: t('address.address.label'),
address2: t('address.address2.label'),
city: t('address.city.label'),
state: t('address.state.label'),
zipcode: t('address.zipcode.label'),
}

return (
<FormGroup>
<TextField
name={`${basename}.address`}
label={labels ? labels.address : defaultLabels.address}
type="text"
data-testid={`${basename}.address`}
onChange={onChange}
/>
{optAddress2 && (
<TextField
name={`${basename}.address2`}
label={labels ? labels.address2 : defaultLabels.address2}
type="text"
data-testid={`${basename}.address2`}
onChange={onChange}
/>
)}
<div className="display-flex" data-testid={`${basename}.parent-div`}>
<TextField
name={`${basename}.city`}
label={labels ? labels.city : defaultLabels.city}
type="text"
data-testid={`${basename}.city`}
className={styles.city}
errorClassName={styles.city_error}
onChange={onChange}
/>
<StatesDropdown
name={`${basename}.state`}
label={labels ? labels.state : defaultLabels.state}
data-testid={`${basename}.state`}
startEmpty
stateSlice={stateSlice}
onChange={onChange}
className={styles.state}
/>
</div>
<TextField
name={`${basename}.zipcode`}
label={labels ? labels.zipcode : defaultLabels.zipcode}
type="text"
inputMode="numeric"
data-testid={`${basename}.zipcode`}
className={styles.zipcode}
onChange={onChange}
/>
</FormGroup>
)
}

export default Address
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,11 @@ import {
FormGroup,
Label,
} from '@trussworks/react-uswds'
import { EMPTY_DROPDOWN_OPTION } from 'constants/formOptions'
import React, { ChangeEventHandler, FocusEventHandler } from 'react'
import { useController } from 'react-hook-form'
import { useTranslation } from 'react-i18next'

export const EMPTY_DROPDOWN_OPTION = ''

const mapOptions = (options: DropdownOption[]) => {
return options.map(({ label, value }, index) => (
<option key={`${index}_${label}_${value}`} value={value}>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { yupResolver } from '@hookform/resolvers/yup'
import { Meta, StoryFn } from '@storybook/react'
import { noop } from 'helpers/noop/noop'
import { FormProvider, useForm } from 'react-hook-form'
import { PhoneInput } from 'types/input'
import { yupPhone } from 'validations/phone'
import * as yup from 'yup'

import { PhoneNumberField } from './PhoneNumberField'

export default {
title: 'Components/Form/PhoneNumberField',
component: PhoneNumberField,
} as Meta<typeof PhoneNumberField>

const Template: StoryFn<typeof PhoneNumberField> = (args) => {
const initialValues = {
[args.name]: {
number: '',
sms: true,
},
}

const validationSchema = yup
.object()
.shape({ [args.name]: yupPhone(true, true) })
const hookFormMethods = useForm<PhoneInput>({
defaultValues: initialValues,
resolver: yupResolver(validationSchema),
})

return (
<FormProvider {...hookFormMethods}>
<form onSubmit={noop}>
<PhoneNumberField {...args} />
</form>
</FormProvider>
)
}

export const Default = Template.bind({})
Default.args = {
name: 'sample_phone',
label: 'Enter your phone number:',
}
Comment on lines +41 to +45
Copy link
Contributor

Choose a reason for hiding this comment

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

So my note about the older syntax for stories can be seen in this example. The documentation now provides updated syntax (SB 7), while these examples are from storybook 6 and before. SB changes teh syntax every year it seems like, but lets go ahead and keep it all uniform with the latest style: https://storybook.js.org/docs/react/writing-stories/introduction

StoryFn isn't used anymore, and the Template.bind is also not used.

e.g this part would be something like

Suggested change
export const Default = Template.bind({})
Default.args = {
name: 'sample_phone',
label: 'Enter your phone number:',
}
export const PhoneNumberField: Story = {
render: () => // Your StoryFn contents here, or a call to it
}

Yes, the name PhoneNumberField is already taken, so import PhoneNumberField as PhoneNumberFieldComponent in this file (see ssn.stories.tsx). This naming convention matching the storybook story name displays the example without nesting it, which we should do if our only story is the default story

Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { ChangeEventHandler, ReactNode } from 'react'
import { useTranslation } from 'react-i18next'

import TextField from '../TextField/TextField'
import { YesNoQuestion } from '../YesNoQuestion/YesNoQuestion'

type PhoneNumberFieldProps = {
id?: string
name: string
label: ReactNode
showSMS?: boolean
onChange?: ChangeEventHandler<HTMLInputElement>
}

export const PhoneNumberField = ({
id: idProp,
name,
label,
showSMS = true,
onChange,
}: PhoneNumberFieldProps) => {
const { t } = useTranslation('components', {
keyPrefix: 'phoneNumberField',
})

const id = idProp || name

return (
<>
<TextField
id={`${id}.number`}
name={`${name}.number`}
label={label}
type="tel"
onChange={onChange}
/>
{showSMS && (
<YesNoQuestion
question={t('sms.label')}
hint={t('sms.help_text')}
name={`${name}.sms`}
/>
)}
</>
)
}
42 changes: 42 additions & 0 deletions src/components/form/fields/StatesDropdown/StatesDropdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import DropdownField from 'components/form/fields/DropdownField/DropdownField'
import { statesAndTerritories } from 'fixtures/states_and_territories'
import { ComponentProps } from 'react'

export type StateAbbrev = keyof typeof statesAndTerritories

type DropdownProps = ComponentProps<typeof DropdownField>

type StatesDropdownProps = {
stateSlice?: StateAbbrev[]
} & Omit<DropdownProps, 'options'>

const allStates = Object.entries(statesAndTerritories).map(([key, value]) => ({
label: value,
value: key,
}))

export const StatesDropdown = ({
label,
id,
name,
startEmpty,
stateSlice,
className,
...remainingProps
}: StatesDropdownProps) => (
<DropdownField
label={label}
id={id}
name={name}
startEmpty={startEmpty}
options={
stateSlice
? allStates.filter((opt) =>
stateSlice.includes(opt.value as StateAbbrev)
)
: allStates
}
className={className}
{...remainingProps}
/>
)
5 changes: 5 additions & 0 deletions src/constants/formOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const UNTOUCHED_RADIO_VALUE = undefined
export type UntouchedRadioValue = typeof UNTOUCHED_RADIO_VALUE

export const EMPTY_DROPDOWN_OPTION = '' as const
export type EmptyOption = typeof EMPTY_DROPDOWN_OPTION
10 changes: 5 additions & 5 deletions src/dev/example-form.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
import { yupResolver } from '@hookform/resolvers/yup'
import { Meta, StoryObj } from '@storybook/react'
import { DateInputField } from 'components/form/fields/DateInputField/DateInputField'
import CheckboxField from 'components/form/fields/CheckboxField/CheckboxField'
import { CheckboxGroupField } from 'components/form/fields/CheckboxGroupField/CheckboxGroupField'
import DropdownField, {
EMPTY_DROPDOWN_OPTION,
} from 'components/form/fields/DropdownField/DropdownField'
import { DateInputField } from 'components/form/fields/DateInputField/DateInputField'
import DropdownField from 'components/form/fields/DropdownField/DropdownField'
import { RadioField } from 'components/form/fields/RadioField/RadioField'
import TextField from 'components/form/fields/TextField/TextField'
import { YesNoQuestion } from 'components/form/fields/YesNoQuestion/YesNoQuestion'
import { ImportedField } from 'components/ImportedInputBox/ImportedField/ImportedField'
import { ImportedInputBox } from 'components/ImportedInputBox/ImportedInputBox'
import { EMPTY_DROPDOWN_OPTION } from 'constants/formOptions'
import { ChangeEventHandler } from 'react'
import {
FormProvider,
SubmitErrorHandler,
SubmitHandler,
useForm,
} from 'react-hook-form'
import { YesNoInput } from 'types/input'
import { yupDate } from 'utils/validations/date'
import * as yup from 'yup'
import { mixed } from 'yup'
Expand Down Expand Up @@ -53,7 +53,7 @@ const schema = yup
.required()

type ExampleFieldValues = {
doYouLikeForms?: boolean
doYouLikeForms?: YesNoInput
formLibraryPreference?: FormLibraryPreferenceOption
whyIsFormikBad?: string
subscribe?: boolean
Expand Down
12 changes: 12 additions & 0 deletions src/fixtures/provinces.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"AB": "Alberta",
"BC": "British Columbia",
"MB": "Manitoba",
"NB": "New Brunswick",
"NL": "Newfoundland and Labrador",
"NS": "Nova Scotia",
"ON": "Ontario",
"PE": "Prince Edward Island",
"QC": "Quebec",
"SK": "Saskatchewan"
}
Loading