Skip to content

Commit 92e145d

Browse files
authored
feat: adds ability to reorder choice option components on Choice edit stage (#613)
1 parent 210aa17 commit 92e145d

7 files changed

Lines changed: 286 additions & 137 deletions

File tree

packages/design/src/FormManager/FormEdit/components/CheckboxGroupPatternEdit/CheckboxGroupPatternEdit.stories.tsx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,35 @@ export const AddField: StoryObj<typeof FormEdit> = {
7878
},
7979
};
8080

81+
export const DeleteField: StoryObj<typeof FormEdit> = {
82+
play: async ({ canvasElement }) => {
83+
const canvas = within(canvasElement);
84+
85+
await userEvent.click(
86+
canvas.getByText(message.patterns.checkboxGroup.displayName)
87+
);
88+
89+
await expect(canvas.getByLabelText('Option 2 label')).toBeInTheDocument();
90+
91+
const option2Element = canvas.getByLabelText('Option 2 label');
92+
const option2Row = option2Element.closest('div');
93+
const deleteButton = within(option2Row as HTMLElement).getByRole('button', {
94+
name: /delete/i,
95+
});
96+
97+
const originalConfirm = window.confirm;
98+
window.confirm = () => true;
99+
100+
await userEvent.click(deleteButton);
101+
102+
await new Promise(resolve => setTimeout(resolve, 3000));
103+
window.confirm = originalConfirm;
104+
105+
await expect(canvas.getByLabelText('Option 1 label')).toBeInTheDocument();
106+
await expect(canvas.getByDisplayValue('Option 3')).toBeInTheDocument();
107+
},
108+
};
109+
81110
export const Error: StoryObj<typeof CheckboxGroupPatternEdit> = {
82111
play: async ({ canvasElement }) => {
83112
userEvent.setup();

packages/design/src/FormManager/FormEdit/components/CheckboxGroupPatternEdit/index.tsx

Lines changed: 62 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import classnames from 'classnames';
22
import React from 'react';
33

4+
import { UniqueIdentifier } from '@dnd-kit/core';
5+
import { DraggableList } from '../PreviewSequencePattern/DraggableList.js';
6+
47
import { type CheckboxGroupProps } from '@gsa-tts/forms-core';
58
import { type CheckboxGroupPattern } from '@gsa-tts/forms-core';
69

@@ -48,6 +51,15 @@ const EditComponent = ({ pattern }: { pattern: CheckboxGroupPattern }) => {
4851
const label = getFieldState('label');
4952
const hint = getFieldState('hint');
5053

54+
const optionIds = options.map(option => option.id as UniqueIdentifier);
55+
56+
const updateOptionOrder = (newOrder: UniqueIdentifier[]) => {
57+
const reorderedOptions = newOrder.map(
58+
id => options.find(option => option.id === id)!
59+
);
60+
setOptions(reorderedOptions);
61+
};
62+
5163
return (
5264
<div className="grid-row grid-gap">
5365
<div className="mobile-lg:grid-col-12 margin-bottom-2">
@@ -106,51 +118,57 @@ const EditComponent = ({ pattern }: { pattern: CheckboxGroupPattern }) => {
106118
</label>
107119
</div>
108120
<div className="tablet:grid-col-6 mobile-lg:grid-col-12">
109-
{options.map((option, index) => {
110-
const optionId = getFieldState(`options.${index}.id`);
111-
const optionLabel = getFieldState(`options.${index}.label`);
112-
return (
113-
<div key={index}>
114-
{optionId.error ? (
115-
<span className="usa-error-message" role="alert">
116-
{optionId.error.message}
117-
</span>
118-
) : null}
119-
{optionLabel.error ? (
120-
<span className="usa-error-message" role="alert">
121-
{optionLabel.error.message}
122-
</span>
123-
) : null}
124-
<div className="display-flex margin-bottom-2">
125-
<input
126-
className={classnames('hide', 'usa-input', {
127-
'usa-label--error': label.error,
128-
})}
129-
id={fieldId(`options.${index}.id`)}
130-
{...register(`options.${index}.id`)}
131-
defaultValue={option.id}
132-
aria-label={`Option ${index + 1} id`}
133-
/>
134-
<label
135-
htmlFor={`options-${index}.id`}
136-
className={`usa-checkbox__label ${styles.optionCircle}`}
137-
></label>
138-
<input
139-
className="usa-input bg-primary-lighter"
140-
id={fieldId(`options.${index}.label`)}
141-
{...register(`options.${index}.label`)}
142-
value={option.label}
143-
onChange={e => updateOptionLabel(index, e.target.value)}
144-
aria-label={`Option ${index + 1} label`}
145-
/>
146-
<PatternOptionActions
147-
optionId={option.id}
148-
onDelete={deleteOption}
149-
/>
121+
<DraggableList
122+
order={optionIds}
123+
updateOrder={updateOptionOrder}
124+
presentation="compact-center"
125+
>
126+
{options.map((option, index) => {
127+
const optionId = getFieldState(`options.${index}.id`);
128+
const optionLabel = getFieldState(`options.${index}.label`);
129+
return (
130+
<div key={index}>
131+
{optionId.error ? (
132+
<span className="usa-error-message" role="alert">
133+
{optionId.error.message}
134+
</span>
135+
) : null}
136+
{optionLabel.error ? (
137+
<span className="usa-error-message" role="alert">
138+
{optionLabel.error.message}
139+
</span>
140+
) : null}
141+
<div className="display-flex margin-bottom-2">
142+
<input
143+
className={classnames('hide', 'usa-input', {
144+
'usa-label--error': label.error,
145+
})}
146+
id={fieldId(`options.${index}.id`)}
147+
{...register(`options.${index}.id`)}
148+
defaultValue={option.id}
149+
aria-label={`Option ${index + 1} id`}
150+
/>
151+
<label
152+
htmlFor={`options-${index}.id`}
153+
className={`usa-checkbox__label ${styles.optionCircle}`}
154+
></label>
155+
<input
156+
className="usa-input bg-primary-lighter"
157+
id={fieldId(`options.${index}.label`)}
158+
{...register(`options.${index}.label`)}
159+
value={option.label}
160+
onChange={e => updateOptionLabel(index, e.target.value)}
161+
aria-label={`Option ${index + 1} label`}
162+
/>
163+
<PatternOptionActions
164+
optionId={option.id}
165+
onDelete={deleteOption}
166+
/>
167+
</div>
150168
</div>
151-
</div>
152-
);
153-
})}
169+
);
170+
})}
171+
</DraggableList>
154172
<button
155173
className={`usa-link ${styles.addMorePatternButton}`}
156174
type="button"

packages/design/src/FormManager/FormEdit/components/PreviewSequencePattern/DraggableList.tsx

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,10 @@ import { useFormManagerStore } from '../../../store.js';
2222
import styles from '../../formEditStyles.module.css';
2323
import classNames from 'classnames';
2424

25-
export type DraggableListPresentation = 'compact' | 'default';
25+
export type DraggableListPresentation =
26+
| 'compact'
27+
| 'compact-center'
28+
| 'default';
2629
export type DraggableListProps = React.PropsWithChildren<{
2730
order: UniqueIdentifier[];
2831
updateOrder: (order: UniqueIdentifier[]) => void;
@@ -131,6 +134,7 @@ const SortableItemOverlay = ({
131134
<div
132135
className={classNames('draggable-list-button', {
133136
'width-5 padding-1': presentation === 'compact',
137+
'width-5 padding-1 margin-y-2': presentation == 'compact-center',
134138
'grid-col-12 width-full padding-2': presentation === 'default',
135139
})}
136140
style={{
@@ -150,7 +154,8 @@ const SortableItemOverlay = ({
150154
</div>
151155
<div
152156
className={classNames('grid-col', {
153-
'flex-fill': presentation === 'compact',
157+
'flex-fill':
158+
presentation === 'compact' || presentation == 'compact-center',
154159
'grid-col-12': presentation === 'default',
155160
})}
156161
>
@@ -187,7 +192,8 @@ const SortableItem = ({
187192
'cursor-pointer',
188193
{
189194
'margin-bottom-3': presentation === 'default',
190-
'border-top-1px': presentation === 'compact',
195+
'border-top-1px':
196+
presentation === 'compact' || presentation == 'compact-center',
191197
'border-base-lighter': presentation === 'compact',
192198
}
193199
)}
@@ -204,12 +210,14 @@ const SortableItem = ({
204210
>
205211
<div
206212
className={classNames('grid-row', {
207-
'display-flex': presentation === 'compact',
213+
'display-flex':
214+
presentation === 'compact' || presentation == 'compact-center',
208215
})}
209216
>
210217
<div
211218
className={classNames({
212219
'width-5 padding-1': presentation === 'compact',
220+
'width-5 padding-1 margin-y-2': presentation == 'compact-center',
213221
'grid-col-12 width-full padding-2': presentation === 'default',
214222
})}
215223
{...listeners}
@@ -232,7 +240,8 @@ const SortableItem = ({
232240
</div>
233241
<div
234242
className={classNames('grid-col', {
235-
'flex-fill': presentation === 'compact',
243+
'flex-fill':
244+
presentation === 'compact' || presentation == 'compact-center',
236245
'grid-col-12': presentation === 'default',
237246
})}
238247
>

packages/design/src/FormManager/FormEdit/components/RadioGroupPatternEdit/RadioGroupPatternEdit.stories.tsx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,35 @@ export const AddField: StoryObj<typeof FormEdit> = {
8282
},
8383
};
8484

85+
export const DeleteField: StoryObj<typeof FormEdit> = {
86+
play: async ({ canvasElement }) => {
87+
const canvas = within(canvasElement);
88+
89+
await userEvent.click(
90+
canvas.getByText(message.patterns.radioGroup.displayName)
91+
);
92+
93+
await expect(canvas.getByLabelText('Option 2 label')).toBeInTheDocument();
94+
95+
const option2Element = canvas.getByLabelText('Option 2 label');
96+
const option2Row = option2Element.closest('div');
97+
const deleteButton = within(option2Row as HTMLElement).getByRole('button', {
98+
name: /delete/i,
99+
});
100+
101+
const originalConfirm = window.confirm;
102+
window.confirm = () => true;
103+
104+
await userEvent.click(deleteButton);
105+
106+
await new Promise(resolve => setTimeout(resolve, 3000));
107+
window.confirm = originalConfirm;
108+
109+
await expect(canvas.getByLabelText('Option 1 label')).toBeInTheDocument();
110+
await expect(canvas.getByDisplayValue('Option 3')).toBeInTheDocument();
111+
},
112+
};
113+
85114
export const Error: StoryObj<typeof CheckboxPatternEdit> = {
86115
play: async ({ canvasElement }) => {
87116
userEvent.setup();

packages/design/src/FormManager/FormEdit/components/RadioGroupPatternEdit/index.tsx

Lines changed: 61 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import classnames from 'classnames';
22
import React from 'react';
33

4+
import { UniqueIdentifier } from '@dnd-kit/core';
5+
import { DraggableList } from '../PreviewSequencePattern/DraggableList.js';
46
import { type RadioGroupProps } from '@gsa-tts/forms-core';
57
import { type RadioGroupPattern } from '@gsa-tts/forms-core';
68

@@ -48,6 +50,15 @@ const EditComponent = ({ pattern }: { pattern: RadioGroupPattern }) => {
4850
const label = getFieldState('label');
4951
const hint = getFieldState('hint');
5052

53+
const optionIds = options.map(option => option.id as UniqueIdentifier);
54+
55+
const updateOptionOrder = (newOrder: UniqueIdentifier[]) => {
56+
const reorderedOptions = newOrder.map(
57+
id => options.find(option => option.id === id)!
58+
);
59+
setOptions(reorderedOptions);
60+
};
61+
5162
return (
5263
<div className="grid-row grid-gap">
5364
<div className="mobile-lg:grid-col-12 margin-bottom-2">
@@ -106,51 +117,57 @@ const EditComponent = ({ pattern }: { pattern: RadioGroupPattern }) => {
106117
</label>
107118
</div>
108119
<div className="tablet:grid-col-6 mobile-lg:grid-col-12">
109-
{options.map((option, index) => {
110-
const optionId = getFieldState(`options.${index}.id`);
111-
const optionLabel = getFieldState(`options.${index}.label`);
112-
return (
113-
<div key={index}>
114-
{optionId.error ? (
115-
<span className="usa-error-message" role="alert">
116-
{optionId.error.message}
117-
</span>
118-
) : null}
119-
{optionLabel.error ? (
120-
<span className="usa-error-message" role="alert">
121-
{optionLabel.error.message}
122-
</span>
123-
) : null}
124-
<div className="display-flex margin-bottom-2">
125-
<input
126-
className={classnames('hide', 'usa-input', {
127-
'usa-label--error': label.error,
128-
})}
129-
id={fieldId(`options.${index}.id`)}
130-
{...register(`options.${index}.id`)}
131-
defaultValue={option.id}
132-
aria-label={`Option ${index + 1} id`}
133-
/>
134-
<label
135-
htmlFor={`options-${index}.id`}
136-
className={`usa-radio__label ${styles.optionCircle}`}
137-
></label>
138-
<input
139-
className="usa-input bg-primary-lighter"
140-
id={fieldId(`options.${index}.label`)}
141-
{...register(`options.${index}.label`)}
142-
value={option.label}
143-
onChange={e => updateOptionLabel(index, e.target.value)}
144-
aria-label={`Option ${index + 1} label`}
145-
/>
146-
<PatternOptionActions
147-
optionId={option.id}
148-
onDelete={deleteOption}
149-
/>
120+
<DraggableList
121+
order={optionIds}
122+
updateOrder={updateOptionOrder}
123+
presentation="compact-center"
124+
>
125+
{options.map((option, index) => {
126+
const optionId = getFieldState(`options.${index}.id`);
127+
const optionLabel = getFieldState(`options.${index}.label`);
128+
return (
129+
<div key={index}>
130+
{optionId.error ? (
131+
<span className="usa-error-message" role="alert">
132+
{optionId.error.message}
133+
</span>
134+
) : null}
135+
{optionLabel.error ? (
136+
<span className="usa-error-message" role="alert">
137+
{optionLabel.error.message}
138+
</span>
139+
) : null}
140+
<div className="display-flex margin-bottom-2">
141+
<input
142+
className={classnames('hide', 'usa-input', {
143+
'usa-label--error': label.error,
144+
})}
145+
id={fieldId(`options.${index}.id`)}
146+
{...register(`options.${index}.id`)}
147+
defaultValue={option.id}
148+
aria-label={`Option ${index + 1} id`}
149+
/>
150+
<label
151+
htmlFor={`options-${index}.id`}
152+
className={`usa-radio__label ${styles.optionCircle}`}
153+
></label>
154+
<input
155+
className="usa-input bg-primary-lighter"
156+
id={fieldId(`options.${index}.label`)}
157+
{...register(`options.${index}.label`)}
158+
value={option.label}
159+
onChange={e => updateOptionLabel(index, e.target.value)}
160+
aria-label={`Option ${index + 1} label`}
161+
/>
162+
<PatternOptionActions
163+
optionId={option.id}
164+
onDelete={deleteOption}
165+
/>
166+
</div>
150167
</div>
151-
</div>
152-
);
153-
})}
168+
);
169+
})}
170+
</DraggableList>
154171
<button
155172
className={`usa-link ${styles.addMorePatternButton}`}
156173
type="button"

0 commit comments

Comments
 (0)