Skip to content

Commit

Permalink
feat: create textarea (long answer) component tckt-399 (#459)
Browse files Browse the repository at this point in the history
* feat: create long answer textarea component TCKT-399

* test(storybook): add stories for textarea component TCKT-399

* feat: create long answer textarea edit component TCKT-399

* test(storybook): add stories for textarea edit component TCKT-399

* feat: add schema config and validtion logic for textarea TCKT-399

* test: add schema config and validtion tests for textarea TCKT-399

---------

Co-authored-by: Khayal Alasgarov <[email protected]>
  • Loading branch information
kalasgarov and kalasgarov authored Jan 27, 2025
1 parent 8e686e4 commit 8cb4833
Show file tree
Hide file tree
Showing 12 changed files with 641 additions and 0 deletions.
7 changes: 7 additions & 0 deletions packages/common/src/locales/en/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ export const en = {
displayName: 'Short answer',
maxLength: 'Maximum length',
},
textarea: {
...defaults,
displayName: 'Long answer',
maxLength: 'Maximum length',
hintLabel: 'Hint Text (optional)',
hint: 'The more specific you can be, the better. Use the space below and/or attach additional pages.',
},
packageDownload: {
...defaults,
displayName: 'Package download',
Expand Down
81 changes: 81 additions & 0 deletions packages/design/src/Form/components/TextArea/TextArea.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import React from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import type { Meta, StoryObj } from '@storybook/react';

import TextArea from './index.js';

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

export default meta;

export const Required: StoryObj<typeof TextArea> = {
args: {
_patternId: '',
type: 'text-area',
inputId: 'test-textarea',
value: '',
label: 'Please enter your comments',
required: true,
maxLength: 500,
},
};

export const NotRequired: StoryObj<typeof TextArea> = {
args: {
_patternId: '',
type: 'text-area',
inputId: 'test-textarea',
value: '',
label: 'Please enter your comments',
required: false,
maxLength: 500,
},
};

export const ErrorState: StoryObj<typeof TextArea> = {
args: {
_patternId: '',
type: 'text-area',
inputId: 'test-textarea',
value: '',
label: 'Please enter your comments',
required: true,
maxLength: 500,
error: {
type: 'required',
message: 'This field is required',
},
},
};

export const WithHint: StoryObj<typeof TextArea> = {
args: {
_patternId: '',
type: 'text-area',
inputId: 'test-textarea',
value: '',
label: 'Please enter your comments',
required: false,
maxLength: 500,
hint: 'This is a hint that provides additional context to the user.',
},
};
68 changes: 68 additions & 0 deletions packages/design/src/Form/components/TextArea/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import React from 'react';
import classNames from 'classnames';
import { useFormContext } from 'react-hook-form';

import { type TextAreaProps } from '@atj/forms';
import { type PatternComponent } from '../../../Form/index.js';

const TextArea: PatternComponent<TextAreaProps> = ({
inputId,
label,
required,
error,
value,
hint,
maxLength,
}) => {
const { register } = useFormContext();
const errorId = `input-error-message-${inputId}`;
const hintId = `input-hint-${inputId}`;

return (
<fieldset className="usa-fieldset">
<div
className={classNames('usa-form-group margin-top-2', {
'usa-form-group--error': error,
})}
>
<div className={classNames('usa-form-group margin-top-2')}>
<label
className={classNames('usa-label', {
'usa-label--error': error,
})}
htmlFor={`textarea-${inputId}`}
>
{label}
{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>
)}
<textarea
className={classNames('usa-textarea', {
'usa-textarea--error': error,
})}
id={`textarea-${inputId}`}
defaultValue={value}
{...register(inputId, { required })}
aria-describedby={
`${hint ? `${hintId}` : ''}${error ? ` ${errorId}` : ''}`.trim() ||
undefined
}
maxLength={maxLength}
style={{ resize: 'none', overflow: 'auto', height: '100px' }}
/>
</div>
</div>
</fieldset>
);
};

export default TextArea;
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 @@ -21,6 +21,7 @@ import SelectDropdown from './SelectDropdown/index.js';
import SocialSecurityNumber from './SocialSecurityNumber/index.js';
import SubmissionConfirmation from './SubmissionConfirmation/index.js';
import TextInput from './TextInput/index.js';
import TextArea from './TextArea/index.js';

export const defaultPatternComponents: ComponentForPattern = {
attachment: Attachment as PatternComponent,
Expand All @@ -44,4 +45,5 @@ export const defaultPatternComponents: ComponentForPattern = {
sequence: Sequence as PatternComponent,
'social-security-number': SocialSecurityNumber as PatternComponent,
'submission-confirmation': SubmissionConfirmation as PatternComponent,
'text-area': TextArea as PatternComponent,
};
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ const sidebarPatterns: DropdownPattern[] = [
['form-summary', defaultFormConfig.patterns['form-summary']],
['gender-id', defaultFormConfig.patterns['gender-id']],
['input', defaultFormConfig.patterns['input']],
['text-area', defaultFormConfig.patterns['text-area']],
['package-download', defaultFormConfig.patterns['package-download']],
['paragraph', defaultFormConfig.patterns['paragraph']],
['phone-number', defaultFormConfig.patterns['phone-number']],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import type { Meta, StoryObj } from '@storybook/react';

import { enLocale as message } from '@atj/common';
import { type TextAreaPattern } from '@atj/forms';

import {
createPatternEditStoryMeta,
testEmptyFormLabelError,
testUpdateFormFieldOnSubmit,
} from '../common/story-helper.js';
import FormEdit from '../../index.js';

const pattern: TextAreaPattern = {
id: '1',
type: 'text-area',
data: {
label: message.patterns.textarea.displayName,
required: false,
maxLength: 256,
initial: 'Initial text',
},
};

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

export const Basic: StoryObj<typeof FormEdit> = {
play: async ({ canvasElement }) => {
await testUpdateFormFieldOnSubmit(
canvasElement,
message.patterns.textarea.displayName,
message.patterns.textarea.fieldLabel,
'Updated textarea pattern'
);
},
};

export const Error: StoryObj<typeof FormEdit> = {
play: async ({ canvasElement }) => {
await testEmptyFormLabelError(
canvasElement,
message.patterns.textarea.displayName,
message.patterns.textarea.fieldLabel,
message.patterns.textarea.fieldLabelRequired
);
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import classNames from 'classnames';
import React from 'react';

import { PatternId, TextAreaProps } from '@atj/forms';
import { TextAreaPattern } from '@atj/forms';

import TextArea from '../../../../Form/components/TextArea/index.js';
import { useFormManagerStore } from '../../../store.js';
import { PatternEditComponent } from '../../types.js';

import { PatternEditActions } from '../common/PatternEditActions.js';
import { PatternEditForm } from '../common/PatternEditForm.js';
import { usePatternEditFormContext } from '../common/hooks.js';
import { enLocale as message } from '@atj/common';

const TextAreaPatternEdit: PatternEditComponent<TextAreaProps> = ({
context,
focus,
previewProps,
}) => {
return (
<>
{focus ? (
<PatternEditForm
pattern={focus.pattern}
editComponent={<EditComponent patternId={focus.pattern.id} />}
></PatternEditForm>
) : (
<div className="padding-left-3 padding-bottom-3 padding-right-3">
<TextArea context={context} {...previewProps} />
</div>
)}
</>
);
};

const EditComponent = ({ patternId }: { patternId: PatternId }) => {
const pattern = useFormManagerStore<TextAreaPattern>(
state => state.session.form.patterns[patternId]
);
const { fieldId, register, getFieldState } =
usePatternEditFormContext<TextAreaPattern>(patternId);

const label = getFieldState('label');
const maxLength = getFieldState('maxLength');
const hint = getFieldState('hint');
const maxLengthAttributes =
pattern.data.maxLength > 0
? {
defaultValue: pattern.data.maxLength,
}
: {};

return (
<div className="grid-row grid-gap-1">
<div className="tablet:grid-col-12 mobile-lg:grid-col-12">
<label
className={classNames('usa-label', {
'usa-label--error': label.error,
})}
htmlFor={fieldId('label')}
>
{message.patterns.textarea.fieldLabel}
{label.error ? (
<span className="usa-error-message" role="alert">
{label.error.message}
</span>
) : null}
<input
className={classNames('usa-input bg-primary-lighter', {
'usa-input--error': label.error,
})}
id={fieldId('label')}
defaultValue={pattern.data.label}
{...register('label')}
type="text"
autoFocus
></input>
</label>
</div>
<div className="tablet:grid-col-12 mobile-lg:grid-col-12 margin-bottom-2">
<label
className={classNames('usa-label', {
'usa-label--error': hint.error,
})}
>
{message.patterns.textarea.hintLabel}
{hint.error ? (
<span className="usa-error-message" role="alert">
{hint.error.message}
</span>
) : null}
<input
className="usa-input"
id={fieldId('hint')}
defaultValue={pattern.data.hint}
{...register('hint')}
type="text"
/>
</label>
</div>
<div className="tablet:grid-col-6 mobile-lg:grid-col-12">
<label
className={classNames('usa-label', {
'usa-label--error': maxLength.error,
})}
htmlFor={fieldId('maxLength')}
>
{maxLength.error ? (
<span className="usa-error-message" role="alert">
{maxLength.error.message}
</span>
) : null}
{message.patterns.textarea.maxLength}
<input
className="usa-input bg-primary-lighter text-bold"
id={fieldId('maxLength')}
{...maxLengthAttributes}
{...register('maxLength')}
type="text"
></input>
</label>
</div>
<div className="grid-col-12">
<PatternEditActions>
<span className="usa-checkbox">
<input
style={{ display: 'inline-block' }}
className="usa-checkbox__input bg-primary-lighter"
type="checkbox"
id={fieldId('required')}
{...register('required')}
defaultChecked={pattern.data.required}
/>
<label
style={{ display: 'inline-block' }}
className="usa-checkbox__label"
htmlFor={fieldId('required')}
>
Required
</label>
</span>
</PatternEditActions>
</div>
</div>
);
};

export default TextAreaPatternEdit;
Loading

0 comments on commit 8cb4833

Please sign in to comment.