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 45 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
8e2dfa2
copy fieldset pattern to use as the basis for the repeater pattern (m…
ethangardner Sep 11, 2024
90ab2e3
add a clone/delete item control for the repeater field to duplicate o…
ethangardner Sep 26, 2024
c1986be
formatting
ethangardner Sep 26, 2024
ced9da5
add presentational component for edit view
ethangardner Sep 27, 2024
0714bb8
prevent duplicate ids for input fields. Will need to map canonical id…
ethangardner Sep 27, 2024
4612dd3
use local storage for storing repeater options on the client
ethangardner Sep 30, 2024
e768c65
add function to mutate ids for cloned elements. need to make it work …
ethangardner Sep 30, 2024
58f614d
formatting
ethangardner Sep 30, 2024
c88a26c
render update radio group components id in repeater
ethangardner Oct 1, 2024
e9b4a37
remove empty test language from user-facing component
ethangardner Oct 1, 2024
71429e5
update ids have an optional suffix to ensure unique ids in the repeat…
ethangardner Oct 2, 2024
2d9efa6
sensible default for local storage
ethangardner Oct 3, 2024
b7a57f1
add function to get id for pattern
ethangardner Oct 4, 2024
9d2c6c1
update id modifier string
ethangardner Oct 4, 2024
111a534
clean up pattern logic for dropdown
ethangardner Oct 4, 2024
d93d97c
refactor to use react hook form useFieldsArray
ethangardner Oct 8, 2024
438839d
work in progress on repeater validation and structure
ethangardner Oct 8, 2024
23cb5d5
ignore .idea dir
ethangardner Oct 8, 2024
f5fced9
dry out add pattern dropdown functions
ethangardner Oct 9, 2024
88fee17
refactor dropdown buttons and consolidate prop types
ethangardner Oct 9, 2024
7a9312a
update validation to accommodate an array of objects
ethangardner Oct 9, 2024
124df4f
turn off results summary table for now
ethangardner Oct 9, 2024
47b1d2d
remove debugging and console statements
ethangardner Oct 10, 2024
71736aa
remove function from repeater pattern. validation occurs on individua…
ethangardner Oct 10, 2024
6db85b3
turn off localstorage on the repeater for now
ethangardner Oct 11, 2024
b3d28c3
unified add pattern methods to fieldset and repeaters into a single m…
ethangardner Oct 11, 2024
562b6b1
resolve ts issue
ethangardner Oct 11, 2024
268002e
prevent effect hook from running until decision is made about behavior
ethangardner Oct 11, 2024
50f5b8b
rename var for clarity
ethangardner Oct 11, 2024
a9bc2b3
cleanup from copy/paste
ethangardner Oct 11, 2024
6b646d8
remove unneeded code
ethangardner Oct 11, 2024
53a52b7
remove the move control if the question is in a repeater or fieldset
ethangardner Oct 14, 2024
2ad25ad
handle field copy
ethangardner Oct 14, 2024
009d002
rename test
ethangardner Oct 14, 2024
4290f6f
add better tests for repeater component
ethangardner Oct 16, 2024
b3913bc
default to empty state for repeater
ethangardner Oct 16, 2024
05e871e
update spacing
ethangardner Oct 16, 2024
1e326c5
convert add/delete buttons to submit so they can be caught on backend
ethangardner Oct 16, 2024
f10c048
remove useform hook in repeater component
ethangardner Oct 21, 2024
f2c2a45
table the submit event name and value for now
ethangardner Oct 21, 2024
e959ed3
fix unfound import issue
ethangardner Dec 5, 2024
317b750
re-add useform hook. needed for pattern validation
ethangardner Oct 23, 2024
2b4eff5
wip on repeater field
ethangardner Oct 24, 2024
f7e838e
add todo comments to help provide guidance for handoff
ethangardner Dec 6, 2024
76f0b6d
Merge branch 'main' into feature/310-repeater-component
ethangardner Dec 6, 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
3 changes: 3 additions & 0 deletions apps/spotlight/src/components/SplashPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default () => {
return <h1>Hero</h1>
}
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 @@ -89,5 +89,10 @@ export const en = {
hint: 'For example, 555-11-0000',
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;
110 changes: 110 additions & 0 deletions packages/design/src/Form/components/Repeater/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import React from 'react';
import { useFieldArray, useForm } from 'react-hook-form';
import { type RepeaterProps } from '@atj/forms';
import { type PatternComponent } from '../../index.js';

const Repeater: PatternComponent<RepeaterProps> = props => {
const { control } = useForm();

const { fields, append, remove } = useFieldArray({
control,
name: 'fields',
});

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

/**
* TODO: we want to have an array of objects so it is grouped correctly when submitted
* child components when submitted need to escalate validation logic to the repeater and rows without
* any input should not be considered fields that we care about for validation.
*
* Each row of the repeater should have its own unique index
*/

const renderWithUniqueIds = (children: React.ReactNode) => {
return React.Children.map(children, child => {
if (React.isValidElement(child) && child?.props?.component?.props) {
console.group('renderwithuniqueids');
console.log(child.props);
console.groupEnd();
return React.cloneElement(child as React.ReactElement, {
component: {
...child.props.component,
props: {
...child.props.component.props,
},
},
});
}
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 @@ -40,30 +40,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 @@ -13,6 +13,7 @@ import PageSet from './PageSet/index.js';
import Paragraph from './Paragraph/index.js';
import PhoneNumber from './PhoneNumber/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 SelectDropdown from './SelectDropdown/index.js';
Expand All @@ -35,6 +36,7 @@ export const defaultPatternComponents: ComponentForPattern = {
paragraph: Paragraph as PatternComponent,
'phone-number': PhoneNumber as PatternComponent,
'radio-group': RadioGroup as PatternComponent,
repeater: Repeater as PatternComponent,
'rich-text': RichText as PatternComponent,
'select-dropdown': SelectDropdown as PatternComponent,
sequence: Sequence as PatternComponent,
Expand Down
Loading
Loading