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

feat: create gender identity input component tckt-365 #395

Merged
merged 6 commits into from
Dec 9, 2024
Merged
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
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;
kalasgarov marked this conversation as resolved.
Show resolved Hide resolved
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
Loading