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

Repeater pattern component #336

Open
wants to merge 48 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
68a1ee4
copy fieldset pattern to use as the basis for the repeater pattern (m…
ethangardner Sep 11, 2024
cbf3c94
add a clone/delete item control for the repeater field to duplicate o…
ethangardner Sep 26, 2024
bf1f21e
formatting
ethangardner Sep 26, 2024
b42cc6a
add presentational component for edit view
ethangardner Sep 27, 2024
0693537
prevent duplicate ids for input fields. Will need to map canonical id…
ethangardner Sep 27, 2024
e80d560
use local storage for storing repeater options on the client
ethangardner Sep 30, 2024
9dae538
add function to mutate ids for cloned elements. need to make it work …
ethangardner Sep 30, 2024
2e16abe
formatting
ethangardner Sep 30, 2024
c9bf5d2
render update radio group components id in repeater
ethangardner Oct 1, 2024
a00acb8
remove empty test language from user-facing component
ethangardner Oct 1, 2024
910faa4
update ids have an optional suffix to ensure unique ids in the repeat…
ethangardner Oct 2, 2024
ce0b362
sensible default for local storage
ethangardner Oct 3, 2024
cef3078
add function to get id for pattern
ethangardner Oct 4, 2024
0fc3b20
update id modifier string
ethangardner Oct 4, 2024
2a5bade
clean up pattern logic for dropdown
ethangardner Oct 4, 2024
31236b1
refactor to use react hook form useFieldsArray
ethangardner Oct 8, 2024
8129a7f
work in progress on repeater validation and structure
ethangardner Oct 8, 2024
63b47ec
ignore .idea dir
ethangardner Oct 8, 2024
a7349f8
dry out add pattern dropdown functions
ethangardner Oct 9, 2024
a801e8c
refactor dropdown buttons and consolidate prop types
ethangardner Oct 9, 2024
53f1b38
update validation to accommodate an array of objects
ethangardner Oct 9, 2024
dbff9f8
turn off results summary table for now
ethangardner Oct 9, 2024
9c5b355
remove debugging and console statements
ethangardner Oct 10, 2024
09aa551
remove function from repeater pattern. validation occurs on individua…
ethangardner Oct 10, 2024
18cd7e3
Create new risk issue template (#328)
JennyRichards-Flexion Oct 11, 2024
31e6439
turn off localstorage on the repeater for now
ethangardner Oct 11, 2024
15a76ef
unified add pattern methods to fieldset and repeaters into a single m…
ethangardner Oct 11, 2024
ef72fa6
Merge branch 'feature/310-copy-fieldset' into feature/310-multiple-en…
ethangardner Oct 11, 2024
9195451
resolve ts issue
ethangardner Oct 11, 2024
5d1ec36
prevent effect hook from running until decision is made about behavior
ethangardner Oct 11, 2024
a029f01
explicitly set playwright version in gh action
ethangardner Oct 11, 2024
a6f5134
rename var for clarity
ethangardner Oct 11, 2024
02c4a8d
cleanup from copy/paste
ethangardner Oct 11, 2024
f31e202
remove unneeded code
ethangardner Oct 11, 2024
858bb1d
variable renaming
ethangardner Oct 11, 2024
662d665
add helper function for compound fields
ethangardner Oct 11, 2024
d020487
formatting
ethangardner Oct 11, 2024
c84bb12
remove the move control if the question is in a repeater or fieldset
ethangardner Oct 14, 2024
af0413a
handle field copy
ethangardner Oct 14, 2024
424225b
rename test
ethangardner Oct 14, 2024
5b11578
Merge branch 'main' into feature/310-repeater-component
ethangardner Oct 16, 2024
142cd35
add better tests for repeater component
ethangardner Oct 16, 2024
8c0c280
default to empty state for repeater
ethangardner Oct 16, 2024
9e77211
update spacing
ethangardner Oct 16, 2024
adfa01d
convert add/delete buttons to submit so they can be caught on backend
ethangardner Oct 16, 2024
d22b289
remove useform hook in repeater component
ethangardner Oct 21, 2024
0714a83
Merge branch 'main' into feature/310-repeater-component
ethangardner Oct 21, 2024
6eeea85
table the submit event name and value for now
ethangardner Oct 21, 2024
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
_site
.turbo/
.vscode/
.idea/
coverage/
html/
node_modules/
Expand Down
5 changes: 5 additions & 0 deletions packages/common/src/locales/en/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,10 @@ export const en = {
fieldLabel: 'Radio group label',
errorTextMustContainChar: 'String must contain at least 1 character(s)',
},
repeater: {
...defaults,
displayName: 'Repeatable Group',
errorTextMustContainChar: 'String must contain at least 1 character(s)',
},
},
};
9 changes: 6 additions & 3 deletions packages/design/src/Form/components/RadioGroup/RadioGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,20 @@ export const RadioGroupPattern: PatternComponent<RadioGroupProps> = props => {
{props.legend}
</legend>
{props.options.map((option, index) => {
const id = props.idSuffix ? `${option.id}${props.idSuffix}` : option.id;
return (
<div key={index} className="usa-radio">
<input
className="usa-radio__input"
type="radio"
id={option.id}
{...register(props.groupId)}
id={`input-${id}`}
{...register(
`${props.groupId}${props.idSuffix ? props.idSuffix : ''}`
)}
value={option.id}
defaultChecked={option.defaultChecked}
/>
<label htmlFor={option.id} className="usa-radio__label">
<label htmlFor={`input-${id}`} className="usa-radio__label">
{option.label}
</label>
</div>
Expand Down
66 changes: 66 additions & 0 deletions packages/design/src/Form/components/Repeater/Repeater.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import type { Meta, StoryObj } from '@storybook/react';
import React from 'react';
import Repeater from './index.js';
import { expect, userEvent } from '@storybook/test';
import { FormProvider, useForm } from 'react-hook-form';

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

const defaultArgs = {
legend: 'Default Heading',
_patternId: 'test-id',
};

export const Default = {
args: {
...defaultArgs,
type: 'repeater',
},
} satisfies StoryObj<typeof Repeater>;

export const WithContents = {
play: async ({ mount, args }) => {
const canvas = await mount(<Repeater {...args} />);

const addButton = canvas.getByRole('button', { name: /Add new item/ });
const deleteButton = canvas.getByRole('button', { name: /Delete item/ });
await userEvent.click(addButton);

let inputs = canvas.queryAllByRole('textbox');
await expect(inputs).toHaveLength(1);

await userEvent.click(deleteButton);
inputs = canvas.queryAllByRole('textbox');
await expect(inputs).toHaveLength(0);
},
args: {
...defaultArgs,
type: 'repeater',
children: [
// eslint-disable-next-line
<div className="usa-form-group-wrapper">
<label className="usa-label" htmlFor="input">
Input
</label>
<input className="usa-input" type="text" id="input" name="input" />
</div>,
],
},
} satisfies StoryObj<typeof Repeater>;
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 './Repeater.stories.js';

describeStories(meta, stories);
19 changes: 19 additions & 0 deletions packages/design/src/Form/components/Repeater/edit.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import React from 'react';
import { type RepeaterProps } from '@atj/forms';
import { type PatternComponent } from '../../../Form/index.js';

const RepeaterEditView: PatternComponent<RepeaterProps> = props => {
return (
<fieldset className="usa-fieldset width-full padding-top-2">
{props.legend !== '' && props.legend !== undefined && (
<legend className="usa-legend text-bold text-uppercase line-height-body-4 width-full margin-top-0 padding-top-3 padding-bottom-1">
{props.legend}
</legend>
)}

{props.children}
</fieldset>
);
};

export default RepeaterEditView;
97 changes: 97 additions & 0 deletions packages/design/src/Form/components/Repeater/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import React from 'react';
import { useFieldArray } from 'react-hook-form';
import { type RepeaterProps } from '@atj/forms';
import { type PatternComponent } from '../../index.js';

const Repeater: PatternComponent<RepeaterProps> = props => {
const { fields, append, remove } = useFieldArray({
name: 'fields',
});

const hasFields = React.Children.toArray(props.children).length > 0;

const renderWithUniqueIds = (children: React.ReactNode, index: number) => {
return React.Children.map(children, child => {
if (React.isValidElement(child) && child?.props?.component?.props) {
return React.cloneElement(child as React.ReactElement, {
component: {
...child.props.component,
props: {
...child.props.component.props,
idSuffix: `.repeater.${index}`,
},
},
});
}
return child;
});
};

return (
<fieldset className="usa-fieldset width-full padding-top-2">
{props.legend && (
<legend className="usa-legend text-bold text-uppercase line-height-body-4 width-full margin-top-0 padding-top-3 padding-bottom-1">
{props.legend}
</legend>
)}
{hasFields && (
<>
{fields.length ? (
<ul className="add-list-reset margin-bottom-4">
{fields.map((field, index) => (
<li
key={field.id}
className="padding-bottom-4 border-bottom border-base-lighter"
>
{renderWithUniqueIds(props.children, index)}
</li>
))}
</ul>
) : (
<div className="usa-prose bg-accent-cool-lighter padding-1 margin-bottom-2">
<p className="margin-top-0">
This section is empty. Start by{' '}
<button
type="submit"
className="usa-button usa-button--secondary usa-button--unstyled"
onClick={e => {
e.preventDefault();
append({});
}}
>
adding an item
</button>
.
</p>
</div>
)}
<div className="usa-button-group margin-bottom-4">
<button
type="submit"
className="usa-button usa-button--outline"
onClick={e => {
e.preventDefault();
append({});
}}
>
Add new item
</button>
<button
type="submit"
className="usa-button usa-button--outline"
onClick={e => {
e.preventDefault();
remove(fields.length - 1);
}}
disabled={fields.length === 0}
>
Delete item
</button>
</div>
</>
)}
</fieldset>
);
};

export default Repeater;
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { type PatternComponent } from '../../../Form/index.js';

const SubmissionConfirmation: PatternComponent<
SubmissionConfirmationProps
> = props => {
> = (/* props */) => {
return (
<>
<legend className="usa-legend usa-legend--large">
Expand Down Expand Up @@ -39,30 +39,35 @@ const SubmissionConfirmation: PatternComponent<
Submission details
</button>
</h4>
<div
id="submission-confirmation-table"
className="usa-accordion__content usa-prose"
hidden={true}
>
<table className="usa-table usa-table--striped width-full">
<thead>
<tr>
<th scope="col">Form field</th>
<th scope="col">Provided value</th>
</tr>
</thead>
<tbody>
{props.table.map((row, index) => {
return (
<tr key={index}>
<th scope="row">{row.label}</th>
<td>{row.value}</td>
</tr>
);
})}
</tbody>
</table>
</div>
{/*
EG: turn this off for now. Will need some design perhaps to see what the presentation
should look like. This was a minimal blocker for the repeater field due to the flat data structure
that was there previously.
*/}
{/*<div*/}
{/* id="submission-confirmation-table"*/}
{/* className="usa-accordion__content usa-prose"*/}
{/* hidden={true}*/}
{/*>*/}
{/* <table className="usa-table usa-table--striped width-full">*/}
{/* <thead>*/}
{/* <tr>*/}
{/* <th scope="col">Form field</th>*/}
{/* <th scope="col">Provided value</th>*/}
{/* </tr>*/}
{/* </thead>*/}
{/* <tbody>*/}
{/* {props.table.map((row, index) => {*/}
{/* return (*/}
{/* <tr key={index}>*/}
{/* <th scope="row">{row.label}</th>*/}
{/* <td>{row.value}</td>*/}
{/* </tr>*/}
{/* );*/}
{/* })}*/}
{/* </tbody>*/}
{/* </table>*/}
{/*</div>*/}
</div>
</>
);
Expand Down
13 changes: 8 additions & 5 deletions packages/design/src/Form/components/TextInput/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import { type PatternComponent } from '../../../Form/index.js';

const TextInput: PatternComponent<TextInputProps> = props => {
const { register } = useFormContext();
const id = props.idSuffix
? `${props.inputId}${props.idSuffix}`
: props.inputId;
return (
<div className="usa-form-group-wrapper" key={props.inputId}>
<div
Expand All @@ -18,13 +21,13 @@ const TextInput: PatternComponent<TextInputProps> = props => {
className={classNames('usa-label', {
'usa-label--error': props.error,
})}
id={`input-message-${props.inputId}`}
id={`input-message-${id}`}
>
{props.label}
{props.error && (
<span
className="usa-error-message"
id={`input-error-message-${props.inputId}`}
id={`input-error-message-${id}`}
role="alert"
>
{props.error.message}
Expand All @@ -34,13 +37,13 @@ const TextInput: PatternComponent<TextInputProps> = props => {
className={classNames('usa-input', {
'usa-input--error': props.error,
})}
id={`input-${props.inputId}`}
id={`input-${id}`}
defaultValue={props.value}
{...register(props.inputId || Math.random().toString(), {
{...register(id || Math.random().toString(), {
//required: props.required,
})}
type="text"
aria-describedby={`input-message-${props.inputId}`}
aria-describedby={`input-message-${id}`}
/>
</label>
</div>
Expand Down
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 @@ -8,6 +8,7 @@ import Page from './Page/index.js';
import PageSet from './PageSet/index.js';
import Paragraph from './Paragraph/index.js';
import RadioGroup from './RadioGroup/index.js';
import Repeater from './Repeater/index.js';
import RichText from './RichText/index.js';
import Sequence from './Sequence/index.js';
import SubmissionConfirmation from './SubmissionConfirmation/index.js';
Expand All @@ -23,6 +24,7 @@ export const defaultPatternComponents: ComponentForPattern = {
'page-set': PageSet as PatternComponent,
paragraph: Paragraph as PatternComponent,
'radio-group': RadioGroup as PatternComponent,
repeater: Repeater as PatternComponent,
'rich-text': RichText as PatternComponent,
sequence: Sequence as PatternComponent,
'submission-confirmation': SubmissionConfirmation as PatternComponent,
Expand Down
Loading
Loading