Skip to content

Commit

Permalink
Edit component focus and error handling improvements (#126)
Browse files Browse the repository at this point in the history
* Remove unused store actions

* Clean up form saving to save a max of every 5 seconds.

* Prep for handling form manager errors by returning a result from updatePatternById

* Replace focussedPattern state with "focus", which is an object that can store multiple bit of edit/focus context.

* With updated focus handling, clean up props on pattern edit components.

* Destructuring

* Create a form errors map type, and wire Zod and react-hook-form usage to it.

* Refactor edit form pattern bindings to an interface provided via a hook wrapping react-hook-form

* Error presentation for checkbox edit

* Maintain edit focus when there are errors.

* Improve how patterns and the edit UI share errors; shore up the interface exposed by usePatternEditFormContext.

* Remove unused import
  • Loading branch information
danielnaab authored May 9, 2024
1 parent 6821eee commit 5eb467e
Show file tree
Hide file tree
Showing 28 changed files with 588 additions and 387 deletions.
53 changes: 27 additions & 26 deletions packages/design/src/FormManager/FormEdit/FormEdit.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ export default {

export const FormEditTest: StoryObj<typeof FormEdit> = {
play: async ({ canvasElement }) => {
await editFieldLabel(canvasElement, 'Pattern 1', 'First field label');
await editFieldLabel(canvasElement, 'Pattern 2', 'Second field label');
await editFieldLabel(canvasElement, 'Pattern 1', 'Pattern 1 (updated)');
await editFieldLabel(canvasElement, 'Pattern 2', 'Pattern 2 (updated)');
},
};

Expand All @@ -51,6 +51,31 @@ export const FormEditAddPattern: StoryObj<typeof FormEdit> = {
},
};

const editFieldLabel = async (
element: HTMLElement,
currentLabel: string,
updatedLabel: string
) => {
const canvas = within(element);

// Give focus to the field matching `currentLabel`
await userEvent.click(await canvas.findByLabelText(currentLabel));

// Enter new text for first field
const input = canvas.getByLabelText('Field label');
await userEvent.clear(input);
await userEvent.type(input, updatedLabel);
await userEvent.click(canvas.getByLabelText('Add a pattern'));

waitFor(
async () => {
const newLabel = await canvas.getByLabelText(updatedLabel);
await expect(newLabel).toBeInTheDocument();
},
{ interval: 5 }
);
};

// This test only works in a real browser, not via JSDOM as we use it.
/*
export const FormEditReorderPattern: StoryObj<typeof FormEdit> = {
Expand Down Expand Up @@ -81,27 +106,3 @@ export const FormEditReorderPattern: StoryObj<typeof FormEdit> = {
},
};
*/

const editFieldLabel = async (
element: HTMLElement,
currentLabel: string,
updatedLabel: string
) => {
const canvas = within(element);

// Give focus to the field matching `currentLabel`
await userEvent.click(await canvas.findByLabelText(currentLabel));

// Enter new text for first field
const input = canvas.getByLabelText('Field label');
await userEvent.clear(input);
await userEvent.type(input, updatedLabel);

waitFor(
async () => {
const newLabel = await canvas.getByLabelText(updatedLabel);
await expect(newLabel).toBeInTheDocument();
},
{ interval: 5 }
);
};
9 changes: 7 additions & 2 deletions packages/design/src/FormManager/FormEdit/PreviewPattern.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,13 @@ export const PreviewPattern: PatternComponent = function PreviewPattern(props) {
const { context, setFocus } = useFormManagerStore(state => ({
context: state.context,
setFocus: state.setFocus,
updatePatternById: state.updatePatternById,
}));
const focus = useFormManagerStore(state => {
if (state.focus?.pattern.id === props._patternId) {
return state.focus;
}
});

const EditComponent = context.editComponents[props.type];

return (
Expand All @@ -26,7 +31,7 @@ export const PreviewPattern: PatternComponent = function PreviewPattern(props) {
}
}}
>
<EditComponent context={context} previewProps={props} />
<EditComponent context={context} previewProps={props} focus={focus} />
</div>
);
};
Original file line number Diff line number Diff line change
@@ -1,90 +1,84 @@
import classnames from 'classnames';
import React from 'react';

import { type CheckboxProps, type PatternId } from '@atj/forms';
import { type CheckboxPattern } from '@atj/forms/src/patterns/checkbox';

import Checkbox from '../../../Form/components/Checkbox';
import { useFormManagerStore } from '../../store';
import { PatternEditComponent } from '../types';

import { PatternEditActions } from './common/PatternEditActions';
import {
PatternEditForm,
usePatternEditFormContext,
} from './common/PatternEditForm';
import { PatternEditForm } from './common/PatternEditForm';
import { usePatternEditFormContext } from './common/hooks';

const CheckboxPatternEdit: PatternEditComponent<CheckboxProps> = props => {
const isSelected = useFormManagerStore(
state => state.focusedPattern?.id === props.previewProps._patternId
);
const CheckboxPatternEdit: PatternEditComponent<CheckboxProps> = ({
focus,
previewProps,
}) => {
return (
<>
{isSelected ? (
{focus ? (
<PatternEditForm
patternId={props.previewProps._patternId}
editComponent={
<CheckboxEditComponent patternId={props.previewProps._patternId} />
}
pattern={focus.pattern}
editComponent={<CheckboxEditComponent patternId={focus.pattern.id} />}
></PatternEditForm>
) : (
<Checkbox {...props.previewProps} />
<Checkbox {...previewProps} />
)}
</>
);
};

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

const label = getFieldState('label');

return (
<div className="grid-row grid-gap">
<div className="tablet:grid-col-6 mobile-lg:grid-col-12">
<label className="usa-label" htmlFor={`${pattern.id}.data.label`}>
<label
className={classnames('usa-label', {
'usa-label--error': label.error,
})}
htmlFor={fieldId('label')}
>
Field label
</label>
{label.error ? (
<span className="usa-error-message" role="alert">
{label.error.message}
</span>
) : null}
<input
className="usa-input"
id={`${pattern.id}.data.label`}
defaultValue={`${pattern.id}`}
{...methods.register(`${pattern.id}.data.label`)}
id={fieldId('label')}
defaultValue={pattern.data.label}
{...register('label')}
type="text"
></input>
</div>
<div className="tablet:grid-col-6 mobile-lg:grid-col-12">
<div className="usa-checkbox">
<input
id={`${pattern.id}.data.defaultChecked`}
id={fieldId('defaultChecked')}
type="checkbox"
{...methods.register(`${pattern.id}.data.defaultChecked`)}
{...register('defaultChecked')}
className="usa-checkbox__input"
/>
<label
className="usa-checkbox__label"
htmlFor={`${pattern.id}.data.defaultChecked`}
htmlFor={fieldId('defaultChecked')}
>
Default field value
</label>
</div>
</div>
<div className="grid-col-12">
<PatternEditActions>
<span className="usa-checkbox">
<input
style={{ display: 'inline-block' }}
className="usa-checkbox__input"
type="checkbox"
id={`${pattern.id}.data.required`}
{...methods.register(`${pattern.id}.data.required`)}
/>
<label
style={{ display: 'inline-block' }}
className="usa-checkbox__label"
htmlFor={`${pattern.id}.data.required`}
>
Required
</label>
</span>
</PatternEditActions>
<PatternEditActions />
</div>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,23 @@ import { useFormManagerStore } from '../../store';
import { PatternEditComponent } from '../types';

import { PatternEditActions } from './common/PatternEditActions';
import {
PatternEditForm,
usePatternEditFormContext,
} from './common/PatternEditForm';
import { PatternEditForm } from './common/PatternEditForm';
import { usePatternEditFormContext } from './common/hooks';
import { FieldsetPattern } from '@atj/forms/src/patterns/fieldset';

const FieldsetEdit: PatternEditComponent<FieldsetProps> = props => {
const isSelected = useFormManagerStore(
state => state.focusedPattern?.id === props.previewProps._patternId
);
const FieldsetEdit: PatternEditComponent<FieldsetProps> = ({
focus,
previewProps,
}) => {
return (
<>
{isSelected ? (
{focus ? (
<PatternEditForm
patternId={props.previewProps._patternId}
editComponent={
<EditComponent patternId={props.previewProps._patternId} />
}
pattern={focus.pattern}
editComponent={<EditComponent patternId={focus.pattern.id} />}
></PatternEditForm>
) : (
<FieldsetPreview {...props.previewProps} />
<FieldsetPreview {...previewProps} />
)}
</>
);
Expand All @@ -39,38 +36,42 @@ const FieldsetPreview = (props: FieldsetProps) => {
return (
<>
<Fieldset {...(props as FieldsetProps)}>
{pattern && pattern.data.patterns.length === 0 &&
{pattern && pattern.data.patterns.length === 0 && (
<div className="usa-alert usa-alert--warning usa-alert--no-icon margin-bottom-3">
<div className="usa-alert__body">
<p className="usa-alert__text">
<span className="alert-text display-inline-block text-top margin-right-2">Empty sections will not display.</span>
<span className="alert-text display-inline-block text-top margin-right-2">
Empty sections will not display.
</span>
<span className="action-text add-question display-inline-block margin-right-2">
<a className="usa-link" href="#">Add question</a>
<a className="usa-link" href="#">
Add question
</a>
</span>
<span className="action-text remove-section display-inline-block text-top margin-right-2">
<a className="usa-link" href="#">Remove section</a>
<a className="usa-link" href="#">
Remove section
</a>
</span>
</p>
</div>
</div>
}
)}
</Fieldset>

</>
);
};

const EditComponent = ({ patternId }: { patternId: PatternId }) => {
const { register } = usePatternEditFormContext();
const { register } = usePatternEditFormContext<FieldsetPattern>(patternId);
return (

<div className="grid-row edit-component-panel">
<div className="grid-col-12 margin-bottom-3 flex-align-self-end">
<label className="usa-label width-full maxw-full">
Legend Text Element
<input
className="usa-input bg-primary-lighter text-bold"
{...register(`${patternId}.data.legend`)}
{...register('legend')}
type="text"
></input>
</label>
Expand All @@ -81,4 +82,4 @@ const EditComponent = ({ patternId }: { patternId: PatternId }) => {
);
};

export default FieldsetEdit;
export default FieldsetEdit;
Original file line number Diff line number Diff line change
Expand Up @@ -3,44 +3,39 @@ import React from 'react';
import { type FormSummaryProps, type PatternId } from '@atj/forms';

import FormSummary from '../../../Form/components/FormSummary';
import { useFormManagerStore } from '../../store';
import { PatternEditComponent } from '../types';

import {
PatternEditForm,
usePatternEditFormContext,
} from './common/PatternEditForm';
import { PatternEditForm } from './common/PatternEditForm';
import { usePatternEditFormContext } from './common/hooks';

const FormSummaryEdit: PatternEditComponent<FormSummaryProps> = props => {
const isSelected = useFormManagerStore(
state => state.focusedPattern?.id === props.previewProps._patternId
);
const FormSummaryEdit: PatternEditComponent<FormSummaryProps> = ({
focus,
previewProps,
}) => {
return (
<>
{isSelected ? (
{focus ? (
<PatternEditForm
patternId={props.previewProps._patternId}
editComponent={
<EditComponent patternId={props.previewProps._patternId} />
}
pattern={focus.pattern}
editComponent={<EditComponent patternId={focus.pattern.id} />}
></PatternEditForm>
) : (
<FormSummary {...props.previewProps} />
<FormSummary {...previewProps} />
)}
</>
);
};

const EditComponent = ({ patternId }: { patternId: PatternId }) => {
const { register } = usePatternEditFormContext();
const { register } = usePatternEditFormContext(patternId);
return (
<div className="grid-row grid-gap-1 edit-component-panel">
<div className="desktop:grid-col-4 mobile:grid-col-12">
<label className="usa-label">
Title
<input
className="usa-input bg-primary-lighter text-bold"
{...register(`${patternId}.data.title`)}
{...register('title')}
type="text"
></input>
</label>
Expand All @@ -50,7 +45,7 @@ const EditComponent = ({ patternId }: { patternId: PatternId }) => {
Description
<textarea
className="usa-textarea bg-primary-lighter text-bold"
{...register(`${patternId}.data.description`)}
{...register('description')}
></textarea>
</label>
</div>
Expand Down
Loading

0 comments on commit 5eb467e

Please sign in to comment.