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 select dropdown patterns for form builders and form users tckt-360 #370

Merged
merged 9 commits into from
Nov 1, 2024
6 changes: 6 additions & 0 deletions packages/common/src/locales/en/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,11 @@ export const en = {
fieldLabel: 'Radio group label',
errorTextMustContainChar: 'String must contain at least 1 character(s)',
},
selectDropdown: {
...defaults,
displayName: 'Select Dropdown label',
fieldLabel: 'Select Dropdown label',
errorTextMustContainChar: 'String must contain at least 1 character(s)',
},
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import React from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { type Meta, type StoryObj } from '@storybook/react';

import { SelectDropdownPattern } from './SelectDropdown.js';

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

export default meta;
export const Default: StoryObj<typeof SelectDropdownPattern> = {
args: {
_patternId: '',
type: 'select-dropdown',
selectId: 'select-1',
label: 'Select an option',
required: false,
options: [
{ value: 'value1', label: 'Option-1' },
{ value: 'value2', label: 'Option-2' },
{ value: 'value3', label: 'Option-3' },
],
},
};

export const WithError: StoryObj<typeof SelectDropdownPattern> = {
args: {
_patternId: '',
type: 'select-dropdown',
selectId: 'select-with-error',
label: 'Select an option with error',
required: false,
options: [
{ value: 'value1', label: 'Option-1' },
{ value: 'value2', label: 'Option-2' },
{ value: 'value3', label: 'Option-3' },
],
error: {
type: 'custom',
message: 'This field has an error',
},
},
};

export const Required: StoryObj<typeof SelectDropdownPattern> = {
args: {
_patternId: '',
type: 'select-dropdown',
selectId: 'select-required',
label: 'Select a required option',
required: true,
options: [
{ value: 'value1', label: 'Option-1' },
{ value: 'value2', label: 'Option-2' },
{ value: 'value3', label: 'Option-3' },
],
},
};
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 './SelectDropdown.stories.js';

describeStories(meta, stories);
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import React from 'react';
import { useFormContext } from 'react-hook-form';

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

export const SelectDropdownPattern: PatternComponent<SelectDropdownProps> = ({
selectId,
label,
required,
options,
error,
}) => {
const { register } = useFormContext();
return (
<div className="usa-fieldset padding-top-2">
<form className="usa-form">
<label className="usa-label" htmlFor={selectId}>
{label}
{required && <span className="required-indicator">*</span>}
</label>
<select className="usa-select" id={selectId} {...register(selectId)}>
<option key="default" value="">
- Select -
</option>
{options.map((option, index) => (
<option key={index} value={option.value}>
{option.label}
</option>
))}
</select>
{error && (
<span className="error-message" style={{ color: 'red' }}>
{error.message}
</span>
)}
</form>
</div>
);
};
3 changes: 3 additions & 0 deletions packages/design/src/Form/components/SelectDropdown/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { SelectDropdownPattern } from './SelectDropdown.js';

export default SelectDropdownPattern;
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 @@ -11,6 +11,7 @@ import Paragraph from './Paragraph/index.js';
import RadioGroup from './RadioGroup/index.js';
import RichText from './RichText/index.js';
import Sequence from './Sequence/index.js';
import SelectDropdown from './SelectDropdown/index.js';
import SubmissionConfirmation from './SubmissionConfirmation/index.js';
import TextInput from './TextInput/index.js';

Expand All @@ -26,6 +27,7 @@ export const defaultPatternComponents: ComponentForPattern = {
paragraph: Paragraph as PatternComponent,
'radio-group': RadioGroup as PatternComponent,
'rich-text': RichText as PatternComponent,
'select-dropdown': SelectDropdown as PatternComponent,
sequence: Sequence as PatternComponent,
'submission-confirmation': SubmissionConfirmation as PatternComponent,
};
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ const sidebarPatterns: DropdownPattern[] = [
['rich-text', defaultFormConfig.patterns['rich-text']],
['radio-group', defaultFormConfig.patterns['radio-group']],
['package-download', defaultFormConfig.patterns['package-download']],
['select-dropdown', defaultFormConfig.patterns['select-dropdown']],
] as const;
export const fieldsetPatterns: DropdownPattern[] = [
['form-summary', defaultFormConfig.patterns['form-summary']],
Expand All @@ -104,6 +105,7 @@ export const fieldsetPatterns: DropdownPattern[] = [
['rich-text', defaultFormConfig.patterns['rich-text']],
['radio-group', defaultFormConfig.patterns['radio-group']],
['package-download', defaultFormConfig.patterns['package-download']],
['select-dropdown', defaultFormConfig.patterns['select-dropdown']],
] as const;

export const SidebarAddPatternMenuItem = ({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import type { Meta, StoryObj } from '@storybook/react';

import { type SelectDropdownPattern } from '@atj/forms';

import { createPatternEditStoryMeta } from './common/story-helper.js';
import FormEdit from '../index.js';
import CheckboxPatternEdit from './CheckboxPatternEdit.js';
import { enLocale as message } from '@atj/common';
import { expect, userEvent } from '@storybook/test';
import { within } from '@testing-library/react';

const pattern: SelectDropdownPattern = {
id: 'select-dropdown-1',
type: 'select-dropdown',
data: {
label: message.patterns.selectDropdown.displayName,
required: false,
options: [
{ value: 'value1', label: 'Option-1' },
{ value: 'value2', label: 'Option-2' },
{ value: 'value3', label: 'Option-3' },
],
},
};

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

export const Basic: StoryObj<typeof FormEdit> = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const updatedLabel = 'Select Dropdown update';

await userEvent.click(
canvas.getByText(message.patterns.selectDropdown.displayName)
);
const input = canvas.getByLabelText(
message.patterns.selectDropdown.fieldLabel
);
const optionLabel = canvas.getByLabelText('Option 1 label');

// Enter new text for the field
await userEvent.clear(input);
await userEvent.type(input, updatedLabel);
await userEvent.clear(optionLabel);
await userEvent.type(optionLabel, '- Select an option -');

const form = input?.closest('form');
/**
* The <enter> key behavior outside of Storybook submits the form, which commits the pending edit.
* Here, we want to simulate the <enter> keypress in the story since Storybook manipulates
* the default behavior and does not register the enter key if it's in the `userEvent.type` function arg.
*/
form?.requestSubmit();

await expect(await canvas.findByText(updatedLabel)).toBeInTheDocument();
await expect(await canvas.findByText('- Select an option -')).toBeVisible();
},
};

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

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

await userEvent.click(
canvas.getByRole('button', {
name: /add new/i,
})
);

await expect(
await canvas.findByLabelText('Option 3 label')
).toBeInTheDocument();
},
};

export const Error: StoryObj<typeof CheckboxPatternEdit> = {
play: async ({ canvasElement }) => {
userEvent.setup();

const canvas = within(canvasElement);

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

const input = canvas.getByLabelText(
message.patterns.selectDropdown.fieldLabel
);
const optionLabel = canvas.getByLabelText('Option 1 label');

// Clear input, remove focus, and wait for error
await userEvent.clear(input);
input.blur();

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

/*
Repopulate the input value since the error text is indistinguishable from
the error text from the option label below
*/
await userEvent.type(input, message.patterns.selectDropdown.fieldLabel);

await userEvent.clear(optionLabel);
optionLabel.blur();

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

await userEvent.clear(input);
input.blur();
},
};
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 './SelectDropdownPatternEdit.stories.js';

describeStories(meta, stories);
Loading
Loading