Skip to content

Commit

Permalink
Repeater pattern component (#336)
Browse files Browse the repository at this point in the history
* copy fieldset pattern to use as the basis for the repeater pattern (multiple input)

* add a clone/delete item control for the repeater field to duplicate or remove a set of questions

* formatting

* add presentational component for edit view

* prevent duplicate ids for input fields. Will need to map canonical id prop for other field types

* use local storage for storing repeater options on the client

* add function to mutate ids for cloned elements. need to make it work for all input types.
:

* formatting

* render update radio group components id in repeater

* remove empty test language from user-facing component

* update ids have an optional suffix to ensure unique ids in the repeater field

* sensible default for local storage

* add function to get id for pattern

* update id modifier string

* clean up pattern logic for dropdown

* refactor to use react hook form useFieldsArray

* work in progress on repeater validation and structure

* ignore .idea dir

* dry out add pattern dropdown functions

* refactor dropdown buttons and consolidate prop types

* update validation to accommodate an array of objects

* turn off results summary table for now

* remove debugging and console statements

* remove function from repeater pattern. validation occurs on individual components

* turn off localstorage on the repeater for now

* unified add pattern methods to fieldset and repeaters into a single method

* resolve ts issue

* prevent effect hook from running until decision is made about behavior

* rename var for clarity

* cleanup from copy/paste

* remove unneeded code

* remove the move control if the question is in a repeater or fieldset

* handle field copy

* rename test

* add better tests for repeater component

* default to empty state for repeater

* update spacing

* convert add/delete buttons to submit so they can be caught on backend

* remove useform hook in repeater component

* table the submit event name and value for now

* fix unfound import issue

* re-add useform hook. needed for pattern validation

* wip on repeater field

* add todo comments to help provide guidance for handoff

* refactor: update single field component for better value and error handling tckt-310

* refactor: update single field component config files tckt-310

* test: update tests for single field component config files tckt-310

* fix: repeater duplicate children id issue tckt-310

* feat: implement isFormBuilder flag tckt-310

* feat: implement parse user input and submit actions for repeater tckt-310

* storybook: update stories for repeater and edit repeater form tckt-310

* refactor: clean up old files tckt-310

* chore: add db file to gitignore tchkt-310

* feat: update phone component type to tel for better user experience tckt-310

* Fix child rendering in the repeater component

* Don't emit output file on typecheck step, to avoid cluttering up the filesystem

* Use childComponents in PromptComponent to get the patternId

* ensure terraform is installed in apply workflow (#439)

* ensure terraform is installed in apply workflow

* revert testing workflow in prep for launch

* chore: clean up ';' in FormContents  TCKT-310

* chore: remove commented code block in repeater TCKT-310

* chore: update pnpm lock yaml TCKT-310

* feat: add removeChildPattern to repaeter config and clean up types TCKT-310

* storybook: add tests for repeater pattern TCKT-310

---------

Co-authored-by: kalasgarov <[email protected]>
Co-authored-by: Khayal Alasgarov <[email protected]>
Co-authored-by: Daniel Naab <[email protected]>
  • Loading branch information
4 people authored Jan 16, 2025
1 parent 7b27866 commit 7c2e9c5
Show file tree
Hide file tree
Showing 54 changed files with 14,810 additions and 10,620 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
_site
.turbo/
.vscode/
.idea/
coverage/
html/
node_modules/
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"test": "vitest run",
"test:ci": "vitest run # --coverage.enabled --coverage.provider=v8 --coverage.reporter=text --coverage.reporter=json-summary --coverage.reporter=json --coverage.reportOnFailure",
"test:infra": "turbo run --filter=infra-cdktf test",
"typecheck": "tsc --build",
"typecheck": "tsc --build --noEmit",
"prepare": "husky"
},
"hooks": {
Expand Down
5 changes: 5 additions & 0 deletions packages/common/src/locales/en/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,5 +99,10 @@ export const en = {
preferNotToAnswerTextLabel:
'Prefer not to share my gender identity checkbox label',
},
repeater: {
...defaults,
displayName: 'Repeatable Group',
errorTextMustContainChar: 'String must contain at least 1 character(s)',
},
},
};
170 changes: 90 additions & 80 deletions packages/design/src/Form/components/DateOfBirth/DateOfBirth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,94 +35,104 @@ export const DateOfBirthPattern: PatternComponent<DateOfBirthProps> = ({
hint,
required,
error,
value,
}) => {
const { register } = useFormContext();
const errorId = `input-error-message-${monthId}`;
const hintId = `hint-${monthId}`;

return (
<fieldset className="usa-fieldset">
<legend className="usa-legend">
{label}
{required && <span className="required-indicator">*</span>}
</legend>
{hint && (
<span className="usa-hint" id={hintId}>
{hint}
</span>
)}
{error && (
<div className="usa-error-message" id={errorId} role="alert">
{error.message}
</div>
)}
<div className="usa-memorable-date">
<div className="usa-form-group usa-form-group--month usa-form-group--select">
<label className="usa-label" htmlFor={monthId}>
Month
</label>
<select
className={classNames('usa-input', {
'usa-input--error': !!error,
})}
id={monthId}
{...register(monthId)}
aria-describedby={
getAriaDescribedBy(
error ? errorId : null,
hint ? hintId : null
) || undefined
}
>
<option key="default" value="">
- Select -
</option>
{months.map((option, index) => (
<option key={index} value={option.value}>
{option.label}
<div className={classNames('usa-form-group margin-top-2')}>
<legend
className={classNames('usa-legend', {
'usa-legend--error': error,
})}
>
{label}
{required && <span className="required-indicator">*</span>}
</legend>
{hint && (
<div className="usa-hint" id={hintId}>
{hint}
</div>
)}
{error && (
<div className="usa-error-message" id={errorId} role="alert">
{error.message}
</div>
)}
<div className="usa-memorable-date">
<div className="usa-form-group usa-form-group--month usa-form-group--select">
<label className="usa-label" htmlFor={monthId}>
Month
</label>
<select
className={classNames('usa-input', {
'usa-input--error': !!error,
})}
id={monthId}
{...register(monthId)}
defaultValue={value?.month}
aria-describedby={
getAriaDescribedBy(
error ? errorId : null,
hint ? hintId : null
) || undefined
}
>
<option key="default" value="">
- Select -
</option>
))}
</select>
</div>
<div className="usa-form-group usa-form-group--day">
<label className="usa-label" htmlFor={dayId}>
Day
</label>
<input
className={classNames('usa-input', {
'usa-input--error': !!error,
})}
id={dayId}
{...register(dayId, { required })}
minLength={2}
maxLength={2}
pattern="[0-9]*"
inputMode="numeric"
aria-describedby={getAriaDescribedBy(
error ? `input-error-message-${dayId}` : null,
hint ? hintId : null
)}
/>
</div>
<div className="usa-form-group usa-form-group--year">
<label className="usa-label" htmlFor={yearId}>
Year
</label>
<input
className={classNames('usa-input', {
'usa-input--error': !!error,
})}
id={yearId}
{...register(yearId, { required })}
minLength={4}
maxLength={4}
pattern="[0-9]*"
inputMode="numeric"
aria-describedby={getAriaDescribedBy(
error ? `input-error-message-${yearId}` : null,
hint ? hintId : null
)}
/>
{months.map((option, index) => (
<option key={index} value={option.value}>
{option.label}
</option>
))}
</select>
</div>
<div className="usa-form-group usa-form-group--day">
<label className="usa-label" htmlFor={dayId}>
Day
</label>
<input
className={classNames('usa-input', {
'usa-input--error': !!error,
})}
id={dayId}
{...register(dayId, { required })}
minLength={2}
maxLength={2}
pattern="[0-9]*"
inputMode="numeric"
aria-describedby={getAriaDescribedBy(
error ? `input-error-message-${dayId}` : null,
hint ? hintId : null
)}
defaultValue={value?.day}
/>
</div>
<div className="usa-form-group usa-form-group--year">
<label className="usa-label" htmlFor={yearId}>
Year
</label>
<input
className={classNames('usa-input', {
'usa-input--error': !!error,
})}
id={yearId}
{...register(yearId, { required })}
minLength={4}
maxLength={4}
pattern="[0-9]*"
inputMode="numeric"
aria-describedby={getAriaDescribedBy(
error ? `input-error-message-${yearId}` : null,
hint ? hintId : null
)}
defaultValue={value?.year}
/>
</div>
</div>
</div>
</fieldset>
Expand Down
13 changes: 10 additions & 3 deletions packages/design/src/Form/components/EmailInput/EmailInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,20 @@ export const EmailInputPattern: PatternComponent<EmailInputProps> = ({
label,
required,
error,
value,
}) => {
const { register } = useFormContext();
const errorId = `input-error-message-${emailId}`;

return (
<fieldset className="usa-fieldset">
<div className="usa-form-group">
<label className="usa-label" htmlFor={emailId}>
<div className={classNames('usa-form-group margin-top-2')}>
<label
className={classNames('usa-label', {
'usa-label--error': error,
})}
htmlFor={emailId}
>
{label}
{required && <span className="required-indicator">*</span>}
</label>
Expand All @@ -26,14 +32,15 @@ export const EmailInputPattern: PatternComponent<EmailInputProps> = ({
</div>
)}
<input
className={classNames('usa-input margin-bottom-1', {
className={classNames('usa-input usa-input--xl', {
'usa-input--error': error,
})}
id={emailId}
type="email"
autoCapitalize="off"
autoCorrect="off"
{...register(emailId, { required })}
defaultValue={value?.email || ''}
aria-describedby={error ? errorId : undefined}
/>
</div>
Expand Down
4 changes: 2 additions & 2 deletions packages/design/src/Form/components/GenderId/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const GenderIdPattern: PatternComponent<GenderIdProps> = ({
label,
required,
error,
value = '',
value,
preferNotToAnswerText,
preferNotToAnswerChecked: initialPreferNotToAnswerChecked = false,
}) => {
Expand All @@ -22,7 +22,7 @@ const GenderIdPattern: PatternComponent<GenderIdProps> = ({
const errorId = `input-error-message-${genderId}`;
const hintId = `hint-${genderId}`;
const preferNotToAnswerId = `${genderId}.preferNotToAnswer`;
const inputId = `${genderId}.input`;
const inputId = `${genderId}.gender`;

const watchedValue = useWatch({ name: inputId, defaultValue: value });

Expand Down
19 changes: 17 additions & 2 deletions packages/design/src/Form/components/PhoneNumber/PhoneNumber.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@ import { useFormContext } from 'react-hook-form';
import { type PhoneNumberProps } from '@atj/forms';
import { type PatternComponent } from '../../index.js';

const formatPhoneNumber = (value: string) => {
const rawValue = value.replace(/[^\d]/g, ''); // Remove non-digit characters

if (rawValue.length <= 3) return rawValue;
if (rawValue.length <= 6)
return `${rawValue.slice(0, 3)}-${rawValue.slice(3)}`;
return `${rawValue.slice(0, 3)}-${rawValue.slice(3, 6)}-${rawValue.slice(6, 10)}`;
};

export const PhoneNumberPattern: PatternComponent<PhoneNumberProps> = ({
phoneId,
hint,
Expand All @@ -12,10 +21,15 @@ export const PhoneNumberPattern: PatternComponent<PhoneNumberProps> = ({
error,
value,
}) => {
const { register } = useFormContext();
const { register, setValue } = useFormContext();
const errorId = `input-error-message-${phoneId}`;
const hintId = `hint-${phoneId}`;

const handlePhoneChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const formattedPhone = formatPhoneNumber(e.target.value);
setValue(phoneId, formattedPhone, { shouldValidate: true });
};

return (
<fieldset className="usa-fieldset">
<div className={classNames('usa-form-group margin-top-2')}>
Expand All @@ -39,13 +53,14 @@ export const PhoneNumberPattern: PatternComponent<PhoneNumberProps> = ({
</div>
)}
<input
className={classNames('usa-input', {
className={classNames('usa-input usa-input--xl', {
'usa-input--error': error,
})}
id={phoneId}
type="tel"
defaultValue={value}
{...register(phoneId, { required })}
onChange={handlePhoneChange}
aria-describedby={
`${hint ? `${hintId}` : ''}${error ? ` ${errorId}` : ''}`.trim() ||
undefined
Expand Down
7 changes: 4 additions & 3 deletions packages/design/src/Form/components/RadioGroup/RadioGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,18 @@ export const RadioGroupPattern: PatternComponent<RadioGroupProps> = props => {
{props.legend}
</legend>
{props.options.map((option, index) => {
const id = option.id;
return (
<div key={index} className="usa-radio">
<input
className="usa-radio__input"
type="radio"
id={option.id}
{...register(props.groupId)}
id={`input-${id}`}
{...register(`${props.groupId}`)}
value={option.id}
defaultChecked={option.defaultChecked}
/>
<label htmlFor={option.id} className="usa-radio__label">
<label htmlFor={`input-${id}`} className="usa-radio__label">
{option.label}
</label>
</div>
Expand Down
Loading

0 comments on commit 7c2e9c5

Please sign in to comment.