diff --git a/packages/common/src/locales/en/app.ts b/packages/common/src/locales/en/app.ts index 969babc3..bb5f7c68 100644 --- a/packages/common/src/locales/en/app.ts +++ b/packages/common/src/locales/en/app.ts @@ -97,7 +97,7 @@ export const en = { hint: 'For example, man, woman, non-binary', errorTextMustContainChar: 'String must contain at least 1 character(s)', preferNotToAnswerTextLabel: - 'Prefer not to share my gender identity label', + 'Prefer not to share my gender identity checkbox label', }, }, }; diff --git a/packages/design/src/Form/components/GenderId/GenderId.stories.tsx b/packages/design/src/Form/components/GenderId/GenderId.stories.tsx index c773c611..9614f1e6 100644 --- a/packages/design/src/Form/components/GenderId/GenderId.stories.tsx +++ b/packages/design/src/Form/components/GenderId/GenderId.stories.tsx @@ -29,17 +29,17 @@ export const Default: StoryObj = { genderId: 'gender-identity', label: 'Gender identity', hint: 'For example, man, woman, non-binary', - required: false, + required: true, preferNotToAnswerText: 'Prefer not to share my gender identity', }, }; -export const WithRequired: StoryObj = { +export const Optional: StoryObj = { args: { genderId: 'gender-identity', label: 'Gender identity', hint: 'For example, man, woman, non-binary', - required: true, + required: false, preferNotToAnswerText: 'Prefer not to share my gender identity', }, }; @@ -73,18 +73,18 @@ export const WithCheckboxChecked: StoryObj = { genderId: 'gender-identity', label: 'Gender identity', hint: 'For example, man, woman, non-binary', - required: false, + required: true, preferNotToAnswerText: 'Prefer not to share my gender identity', preferNotToAnswerChecked: true, }, }; -export const WithoutPreferNotToAnswerText: StoryObj = { +export const WithoutCheckbox: StoryObj = { args: { genderId: 'gender-identity', label: 'Gender identity', hint: 'For example, man, woman, non-binary', - required: false, + required: true, preferNotToAnswerText: undefined, }, -}; +}; \ No newline at end of file diff --git a/packages/design/src/Form/components/GenderId/GenderId.tsx b/packages/design/src/Form/components/GenderId/GenderId.tsx index 56f24a8f..6755e4da 100644 --- a/packages/design/src/Form/components/GenderId/GenderId.tsx +++ b/packages/design/src/Form/components/GenderId/GenderId.tsx @@ -69,6 +69,15 @@ export const GenderIdPattern: PatternComponent = ({ className={classNames('usa-input', { 'usa-input--error': error, })} + style={ + preferNotToAnswerChecked + ? { + backgroundColor: '#e9ecef', + pointerEvents: 'none', + opacity: 0.65, + } + : {} + } id={genderId} type="text" defaultValue={value} @@ -77,7 +86,6 @@ export const GenderIdPattern: PatternComponent = ({ `${hint ? `${hintId}` : ''}${error ? ` ${errorId}` : ''}`.trim() || undefined } - disabled={preferNotToAnswerChecked} /> {preferNotToAnswerText && (
diff --git a/packages/design/src/FormManager/FormEdit/components/GenderIdPatternEdit/GenderIdPatternEdit.stories.tsx b/packages/design/src/FormManager/FormEdit/components/GenderIdPatternEdit/GenderIdPatternEdit.stories.tsx index 891fc8f6..b3858166 100644 --- a/packages/design/src/FormManager/FormEdit/components/GenderIdPatternEdit/GenderIdPatternEdit.stories.tsx +++ b/packages/design/src/FormManager/FormEdit/components/GenderIdPatternEdit/GenderIdPatternEdit.stories.tsx @@ -12,7 +12,7 @@ const pattern: GenderIdPattern = { type: 'gender-id', data: { label: message.patterns.genderId.displayName, - required: false, + required: true, hint: undefined, preferNotToAnswerText: message.patterns.genderId.preferNotToAnswerTextLabel, }, diff --git a/packages/design/src/FormManager/FormEdit/formEditStyles.module.css b/packages/design/src/FormManager/FormEdit/formEditStyles.module.css index 9d7dbef5..fe72a96b 100644 --- a/packages/design/src/FormManager/FormEdit/formEditStyles.module.css +++ b/packages/design/src/FormManager/FormEdit/formEditStyles.module.css @@ -55,6 +55,7 @@ .draggableListItemWrapper .dateOfBirthPattern legend, .draggableListItemWrapper .phoneNumberPattern legend, .draggableListItemWrapper .ssnPattern legend, +.draggableListItemWrapper .genderIdPattern legend, .draggableListItemWrapper .emailInputPattern legend { padding-left: 0; } diff --git a/packages/forms/src/patterns/gender-id/gender-id.test.ts b/packages/forms/src/patterns/gender-id/gender-id.test.ts new file mode 100644 index 00000000..e46caea2 --- /dev/null +++ b/packages/forms/src/patterns/gender-id/gender-id.test.ts @@ -0,0 +1,125 @@ +import { describe, expect, it } from 'vitest'; +import { + createGenderIdSchema, + genderIdConfig, + type GenderIdPattern, +} from './gender-id'; + +describe('GenderIdPattern tests', () => { + describe('createGenderIdSchema', () => { + it('should create schema for required gender identity input', () => { + const data: GenderIdPattern['data'] = { + label: 'Test Gender Identity Label', + required: true, + }; + + const schema = createGenderIdSchema(data); + const validInput = 'Test Gender'; + const invalidInput = ''; + + expect(schema.safeParse(validInput).success).toBe(true); + expect(schema.safeParse(invalidInput).success).toBe(false); + }); + + it('should create schema for optional gender identity input', () => { + const data: GenderIdPattern['data'] = { + label: 'Test Gender Identity Label', + required: false, + }; + + const schema = createGenderIdSchema(data); + const validInput = 'Test Gender'; + const emptyInput = ''; + + expect(schema.safeParse(validInput).success).toBe(true); + expect(schema.safeParse(emptyInput).success).toBe(true); + }); + }); + + describe('genderIdConfig', () => { + it('should parse user input correctly', () => { + const pattern: GenderIdPattern = { + id: 'gender-identity-1', + type: 'gender-id', + data: { + label: 'Test Gender Identity Label', + required: true, + }, + }; + + const inputValue = 'Test Gender'; + if (!genderIdConfig.parseUserInput) { + expect.fail('genderIdConfig.parseUserInput is undefined'); + } + const result = genderIdConfig.parseUserInput(pattern, inputValue); + if (result.success) { + expect(result.data).toEqual(inputValue); + } else { + expect.fail('Unexpected validation failure'); + } + }); + + it('should handle validation error for user input', () => { + const pattern: GenderIdPattern = { + id: 'gender-identity-1', + type: 'gender-id', + data: { + label: 'Test Gender Identity Label', + required: true, + }, + }; + + const inputValue = ''; + if (!genderIdConfig.parseUserInput) { + expect.fail('genderIdConfig.parseUserInput is undefined'); + } + const result = genderIdConfig.parseUserInput(pattern, inputValue); + if (!result.success) { + expect(result.error).toBeDefined(); + } else { + expect.fail('Unexpected validation success'); + } + }); + + it('should parse config data correctly', () => { + const obj = { + label: 'Test Gender Identity Label', + required: true, + hint: 'For example, man, woman, non-binary', + preferNotToAnswerText: 'Prefer not to share my gender identity', + }; + + if (!genderIdConfig.parseConfigData) { + expect.fail('genderIdConfig.parseConfigData is undefined'); + } + const result = genderIdConfig.parseConfigData(obj); + if (result.success) { + expect(result.data.label).toBe('Test Gender Identity Label'); + expect(result.data.required).toBe(true); + expect(result.data.hint).toBe('For example, man, woman, non-binary'); + expect(result.data.preferNotToAnswerText).toBe( + 'Prefer not to share my gender identity' + ); + } else { + expect.fail('Unexpected validation failure'); + } + }); + + it('should handle invalid config data', () => { + const obj = { + label: '', + required: true, + }; + + if (!genderIdConfig.parseConfigData) { + expect.fail('genderIdConfig.parseConfigData is undefined'); + } + const result = genderIdConfig.parseConfigData(obj); + if (!result.success) { + expect(result.error).toBeDefined(); + } else { + expect.fail('Unexpected validation success'); + } + }); + }); +}); diff --git a/packages/forms/src/patterns/gender-id/gender-id.ts b/packages/forms/src/patterns/gender-id/gender-id.ts new file mode 100644 index 00000000..208625c3 --- /dev/null +++ b/packages/forms/src/patterns/gender-id/gender-id.ts @@ -0,0 +1,86 @@ +import * as z from 'zod'; +import { type GenderIdProps } from '../../components.js'; +import { type Pattern, type PatternConfig } from '../../pattern.js'; +import { getFormSessionValue } from '../../session.js'; +import { + safeZodParseFormErrors, + safeZodParseToFormError, +} from '../../util/zod.js'; + +const configSchema = z.object({ + label: z.string().min(1), + required: z.boolean(), + hint: z.string().optional(), + preferNotToAnswerText: z.string().optional(), +}); + +export type GenderIdPattern = Pattern>; + +export type GenderIdPatternOutput = z.infer< + ReturnType +>; + +export const createGenderIdSchema = (data: GenderIdPattern['data']) => { + return z.string().superRefine((value, ctx) => { + if (value === data.preferNotToAnswerText) { + return; + } + if (data.required && value.trim() === '') { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'This field is required', + }); + } + }); +}; + +export const genderIdConfig: PatternConfig< + GenderIdPattern, + GenderIdPatternOutput +> = { + displayName: 'Gender ID', + iconPath: 'gender-id-icon.svg', + initial: { + label: 'Gender identity', + required: true, + hint: 'For example, man, woman, non-binary', + preferNotToAnswerText: 'Prefer not to share my gender identity', + }, + + parseUserInput: (pattern, inputValue) => { + const result = safeZodParseToFormError( + createGenderIdSchema(pattern.data), + inputValue + ); + return result; + }, + + parseConfigData: obj => { + return safeZodParseFormErrors(configSchema, obj); + }, + getChildren() { + return []; + }, + + createPrompt(_, session, pattern, options) { + const extraAttributes: Record = {}; + const sessionValue = getFormSessionValue(session, pattern.id); + const error = session.data.errors[pattern.id]; + + return { + props: { + _patternId: pattern.id, + type: 'gender-id', + label: pattern.data.label, + genderId: pattern.id, + required: pattern.data.required, + hint: pattern.data.hint, + preferNotToAnswerText: pattern.data.preferNotToAnswerText, + value: sessionValue, + error, + ...extraAttributes, + } as GenderIdProps, + children: [], + }; + }, +}; diff --git a/packages/forms/src/patterns/index.ts b/packages/forms/src/patterns/index.ts index bb58d988..dc5f5fbd 100644 --- a/packages/forms/src/patterns/index.ts +++ b/packages/forms/src/patterns/index.ts @@ -7,6 +7,7 @@ import { dateOfBirthConfig } from './date-of-birth/date-of-birth.js'; import { emailInputConfig } from './email-input/email-input.js'; import { fieldsetConfig } from './fieldset/index.js'; import { formSummaryConfig } from './form-summary.js'; +import { genderIdConfig } from './gender-id/gender-id.js'; import { inputConfig } from './input/index.js'; import { packageDownloadConfig } from './package-download/index.js'; import { pageConfig } from './page/index.js'; @@ -31,6 +32,7 @@ export const defaultFormConfig: FormConfig = { 'email-input': emailInputConfig, fieldset: fieldsetConfig, 'form-summary': formSummaryConfig, + 'gender-id': genderIdConfig, input: inputConfig, 'package-download': packageDownloadConfig, page: pageConfig, @@ -55,6 +57,7 @@ export * from './email-input/email-input.js'; export * from './fieldset/index.js'; export { type FieldsetPattern } from './fieldset/config.js'; export * from './form-summary.js'; +export * from './gender-id/gender-id.js'; export * from './input/index.js'; export { type InputPattern } from './input/config.js'; export * from './package-download/index.js';