Skip to content

Commit

Permalink
feat: create gender identity input component tckt-365 (#395)
Browse files Browse the repository at this point in the history
* feat: create gender id pattern tckt-365

* feat: create gender id pattern edit form tckt-365

* feat: add input and schema validation tckt-365

* feat: update gender identity icon svg tckt-365

* refactor: update story and enhanch component file structure for gender id -tckt-365

* fix: resolve checkbox issue to persist the checked state tckt-365

---------

Co-authored-by: kalasgarov <[email protected]>
  • Loading branch information
kalasgarov and kalasgarov authored Dec 9, 2024
1 parent d1c388f commit 193f9cd
Show file tree
Hide file tree
Showing 16 changed files with 741 additions and 0 deletions.
10 changes: 10 additions & 0 deletions packages/common/src/locales/en/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,5 +89,15 @@ export const en = {
hint: 'For example, 555-11-0000',
errorTextMustContainChar: 'String must contain at least 1 character(s)',
},
genderId: {
...defaults,
displayName: 'Gender Identity label',
fieldLabel: 'Gender Identity label',
hintLabel: 'Gender Identity hint label',
hint: 'For example, man, woman, non-binary',
errorTextMustContainChar: 'String must contain at least 1 character(s)',
preferNotToAnswerTextLabel:
'Prefer not to share my gender identity checkbox label',
},
},
};
64 changes: 64 additions & 0 deletions packages/design/src/Form/components/GenderId/GenderId.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import React from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { type Meta, type StoryObj } from '@storybook/react';
import GenderIdPattern from './index.js';

const meta: Meta<typeof GenderIdPattern> = {
title: 'patterns/GenderIdPattern',
component: GenderIdPattern,
decorators: [
(Story, args) => {
const FormDecorator = () => {
const formMethods = useForm();
return (
<FormProvider {...formMethods}>
<Story {...args} />
</FormProvider>
);
};
return <FormDecorator />;
},
],
tags: ['autodocs'],
};

export default meta;

const defaultArgs = {
genderId: 'gender-identity',
label: 'Gender identity',
hint: 'For example, man, woman, non-binary',
required: true,
preferNotToAnswerText: 'Prefer not to share my gender identity',
};

export const Default: StoryObj<typeof GenderIdPattern> = {
args: { ...defaultArgs },
};

export const Optional: StoryObj<typeof GenderIdPattern> = {
args: { ...defaultArgs, required: false },
};

export const WithError: StoryObj<typeof GenderIdPattern> = {
args: {
...defaultArgs,
label: 'Gender identity with error',
error: {
type: 'custom',
message: 'This field has an error',
},
},
};

export const WithHint: StoryObj<typeof GenderIdPattern> = {
args: { ...defaultArgs },
};

export const WithCheckboxChecked: StoryObj<typeof GenderIdPattern> = {
args: { ...defaultArgs, preferNotToAnswerChecked: true },
};

export const WithoutCheckbox: StoryObj<typeof GenderIdPattern> = {
args: { ...defaultArgs, preferNotToAnswerText: undefined },
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* @vitest-environment jsdom
*/
import { describeStories } from '../../../test-helper.js';
import meta, * as stories from './GenderId.stories.js';

describeStories(meta, stories);
98 changes: 98 additions & 0 deletions packages/design/src/Form/components/GenderId/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import React, { useState } from 'react';
import classNames from 'classnames';
import { useFormContext, useWatch } from 'react-hook-form';
import { type GenderIdProps } from '@atj/forms';
import { type PatternComponent } from '../../index.js';

const GenderIdPattern: PatternComponent<GenderIdProps> = ({
genderId,
hint,
label,
required,
error,
value = '',
preferNotToAnswerText,
preferNotToAnswerChecked: initialPreferNotToAnswerChecked = false,
}) => {
const { register, setValue } = useFormContext();
const [preferNotToAnswerChecked, setPreferNotToAnswerChecked] = useState(
initialPreferNotToAnswerChecked
);

const errorId = `input-error-message-${genderId}`;
const hintId = `hint-${genderId}`;
const preferNotToAnswerId = `${genderId}.preferNotToAnswer`;
const inputId = `${genderId}.input`;

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

const handleCheckboxChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const isChecked = event.target.checked;
setPreferNotToAnswerChecked(isChecked);
setValue(genderId, isChecked ? preferNotToAnswerText : value, {
shouldValidate: true,
});
};

return (
<fieldset className="usa-fieldset">
<div className={classNames('usa-form-group margin-top-2')}>
<label
className={classNames('usa-label', {
'usa-label--error': error,
})}
htmlFor={genderId}
>
{label || 'Gender identity'}
{required && <span className="required-indicator">*</span>}
</label>
{hint && (
<div className="usa-hint" id={hintId}>
{hint}
</div>
)}
{error && (
<div className="usa-error-message" id={errorId} role="alert">
{error.message}
</div>
)}
<input
className={classNames('usa-input usa-input--xl', {
'usa-input--error': error,
})}
id={inputId}
type="text"
readOnly={preferNotToAnswerChecked}
disabled={preferNotToAnswerChecked}
defaultValue={preferNotToAnswerChecked ? '' : watchedValue}
{...register(inputId, { required })}
aria-describedby={
`${hint ? `${hintId}` : ''}${error ? ` ${errorId}` : ''}`.trim() ||
undefined
}
/>
{preferNotToAnswerText && (
<div className="usa-checkbox usa-input--xl">
<input
className="usa-checkbox__input"
id={preferNotToAnswerId}
type="checkbox"
defaultValue={preferNotToAnswerText}
defaultChecked={preferNotToAnswerChecked}
{...register(preferNotToAnswerId)}
onChange={handleCheckboxChange}
/>
<label
className="usa-checkbox__label"
htmlFor={preferNotToAnswerId}
>
{preferNotToAnswerText}
</label>
</div>
)}
</div>
</fieldset>
);
};

export default GenderIdPattern;
2 changes: 2 additions & 0 deletions packages/design/src/Form/components/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import DateOfBirth from './DateOfBirth/index.js';
import EmailInput from './EmailInput/index.js';
import Fieldset from './Fieldset/index.js';
import FormSummary from './FormSummary/index.js';
import GenderId from './GenderId/index.js';
import PackageDownload from './PackageDownload/index.js';
import Page from './Page/index.js';
import PageSet from './PageSet/index.js';
Expand All @@ -28,6 +29,7 @@ export const defaultPatternComponents: ComponentForPattern = {
'email-input': EmailInput as PatternComponent,
fieldset: Fieldset as PatternComponent,
'form-summary': FormSummary as PatternComponent,
'gender-id': GenderId as PatternComponent,
input: TextInput as PatternComponent,
'package-download': PackageDownload as PatternComponent,
page: Page as PatternComponent,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import dateIcon from './images/date-icon.svg';
import dropDownIcon from './images/dropdown-icon.svg';
import dropDownOptionIcon from './images/dropdownoption-icon.svg';
import emailInputIcon from './images/email-icon.svg';
import genderId from './images/gender-id-icon.svg';
import longanswerIcon from './images/longanswer-icon.svg';
import pageIcon from './images/page-icon.svg';
import phoneIcon from './images/phone-icon.svg';
Expand All @@ -32,6 +33,7 @@ const icons: Record<string, string | any> = {
'dropdown-icon.svg': dropDownIcon,
'dropdownoption-icon.svg': dropDownOptionIcon,
'email-icon.svg': emailInputIcon,
'gender-id-icon.svg': genderId,
'longanswer-icon.svg': longanswerIcon,
'page-icon.svg': pageIcon,
'phone-icon.svg': phoneIcon,
Expand Down Expand Up @@ -101,6 +103,7 @@ const sidebarPatterns: DropdownPattern[] = [
['email-input', defaultFormConfig.patterns['email-input']],
['fieldset', defaultFormConfig.patterns['fieldset']],
['form-summary', defaultFormConfig.patterns['form-summary']],
['gender-id', defaultFormConfig.patterns['gender-id']],
['input', defaultFormConfig.patterns['input']],
['package-download', defaultFormConfig.patterns['package-download']],
['paragraph', defaultFormConfig.patterns['paragraph']],
Expand All @@ -120,6 +123,7 @@ export const fieldsetPatterns: DropdownPattern[] = [
['date-of-birth', defaultFormConfig.patterns['date-of-birth']],
['email-input', defaultFormConfig.patterns['email-input']],
['form-summary', defaultFormConfig.patterns['form-summary']],
['gender-id', defaultFormConfig.patterns['gender-id']],
['input', defaultFormConfig.patterns['input']],
['package-download', defaultFormConfig.patterns['package-download']],
['paragraph', defaultFormConfig.patterns['paragraph']],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import type { Meta, StoryObj } from '@storybook/react';
import { userEvent, expect } from '@storybook/test';
import { within } from '@testing-library/react';

import { type GenderIdPattern } from '@atj/forms';
import { createPatternEditStoryMeta } from '../common/story-helper.js';
import FormEdit from '../../index.js';
import { enLocale as message } from '@atj/common';

const pattern: GenderIdPattern = {
id: 'gender-identity-1',
type: 'gender-id',
data: {
label: message.patterns.genderId.displayName,
required: true,
hint: undefined,
preferNotToAnswerText: message.patterns.genderId.preferNotToAnswerTextLabel,
},
};

const storyConfig: Meta = {
title: 'Edit components/GenderIdPattern',
...createPatternEditStoryMeta({
pattern,
}),
} as Meta<typeof FormEdit>;

export default storyConfig;

export const Basic: StoryObj<typeof FormEdit> = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const updatedLabel = 'Gender identity update';
const updatedHint = 'Updated hint for Gender identity';
const updatedPreferNotToAnswerText =
'Updated prefer not to share my gender identity text';

await userEvent.click(
canvas.getByText(message.patterns.genderId.displayName)
);

const labelInput = canvas.getByLabelText(
message.patterns.genderId.fieldLabel
);
await userEvent.clear(labelInput);
await userEvent.type(labelInput, updatedLabel);

const hintInput = canvas.getByLabelText(
message.patterns.genderId.hintLabel
);
await userEvent.clear(hintInput);
await userEvent.type(hintInput, updatedHint);

const preferNotToAnswerInput = canvas.getByLabelText(
message.patterns.genderId.preferNotToAnswerTextLabel
);
await userEvent.clear(preferNotToAnswerInput);
await userEvent.type(preferNotToAnswerInput, updatedPreferNotToAnswerText);

const form = labelInput?.closest('form');
form?.requestSubmit();

await expect(await canvas.findByText(updatedLabel)).toBeInTheDocument();
await expect(await canvas.findByText(updatedHint)).toBeInTheDocument();
await expect(
await canvas.findByText(updatedPreferNotToAnswerText)
).toBeInTheDocument();
},
};

export const WithoutHint: StoryObj<typeof FormEdit> = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const updatedLabel = 'Gender identity update';
const updatedPreferNotToAnswerText =
'Prefer not to update my gender identity';

await userEvent.click(
canvas.getByText(message.patterns.genderId.displayName)
);

const labelInput = canvas.getByLabelText(
message.patterns.genderId.fieldLabel
);
await userEvent.clear(labelInput);
await userEvent.type(labelInput, updatedLabel);

const preferNotToAnswerInput = canvas.getByLabelText(
message.patterns.genderId.preferNotToAnswerTextLabel
);
await userEvent.clear(preferNotToAnswerInput);
await userEvent.type(preferNotToAnswerInput, updatedPreferNotToAnswerText);

const form = labelInput?.closest('form');
form?.requestSubmit();

await expect(await canvas.findByText(updatedLabel)).toBeInTheDocument();
await expect(
await canvas.findByText(updatedPreferNotToAnswerText)
).toBeInTheDocument();
await expect(
await canvas.queryByLabelText(message.patterns.genderId.hintLabel)
).toBeNull();
},
};

export const WithoutCheckbox: StoryObj<typeof FormEdit> = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const updatedLabel = 'Gender identity update';
const updatedHint = 'Updated hint for Gender identity';

await userEvent.click(
canvas.getByText(message.patterns.genderId.displayName)
);

const labelInput = canvas.getByLabelText(
message.patterns.genderId.fieldLabel
);
await userEvent.clear(labelInput);
await userEvent.type(labelInput, updatedLabel);

const hintInput = canvas.getByLabelText(
message.patterns.genderId.hintLabel
);
await userEvent.clear(hintInput);
await userEvent.type(hintInput, updatedHint);

const form = labelInput?.closest('form');
form?.requestSubmit();

await expect(await canvas.findByText(updatedLabel)).toBeInTheDocument();
await expect(
await canvas.queryByLabelText(message.patterns.genderId.hintLabel)
).toBeNull();
},
};

export const Error: StoryObj<typeof FormEdit> = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);

await userEvent.click(
canvas.getByText(message.patterns.genderId.displayName)
);

const labelInput = canvas.getByLabelText(
message.patterns.genderId.fieldLabel
);
await userEvent.clear(labelInput);
labelInput.blur();

await expect(
await canvas.findByText(
message.patterns.selectDropdown.errorTextMustContainChar
)
).toBeInTheDocument();
},
};
Loading

0 comments on commit 193f9cd

Please sign in to comment.